// Copyright Keysight Technologies 2012-2019 // This Source Code Form is subject to the terms of the Mozilla Public // License, v. 2.0. If a copy of the MPL was not distributed with this // file, you can obtain one at http://mozilla.org/MPL/2.0/. using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Reflection; using System.Text; using System.Text.RegularExpressions; using System.Xml; using System.Xml.Linq; using DotNet.Globbing; using DotNet.Globbing.Token; using Microsoft.Build.Framework; using Microsoft.Build.Utilities; using Task = Microsoft.Build.Utilities.Task; namespace Keysight.OpenTap.Sdk.MSBuild { internal class GlobWrapper : IComparable { internal bool Include { get; } private static GlobOptions _globOptions = new DotNet.Globbing.GlobOptions {Evaluation = {CaseInsensitive = false}}; internal Glob Globber { get; } internal GlobWrapper(string pattern, bool include) { Include = include; Globber = Glob.Parse(pattern, _globOptions); } public int CompareTo(GlobWrapper other) { bool isLiteral(Glob glob) { var pattern = glob.ToString(); return ((pattern.EndsWith(".dll") || pattern.EndsWith(".dll$")) && !(pattern.EndsWith("*.dll") || pattern.EndsWith("*.dll$"))) || glob.Tokens.All(x => x is LiteralToken); } var t1IsLiteral = isLiteral(Globber); var t2IsLiteral = isLiteral(other.Globber); // If one is literal, put it before the other; if (t1IsLiteral || t2IsLiteral) { var literalComp = t2IsLiteral.CompareTo(t1IsLiteral); if (literalComp != 0) return literalComp; } // Otherwise, put the one with the most tokens first var lengthComp = other.Globber.Tokens.Length.CompareTo(Globber.Tokens.Length); if (lengthComp != 0) return lengthComp; // Put includes before excludes in case of ties return other.Include.CompareTo(Include); } } internal class GlobTask { internal void LogWarningWithLineNumber(string msg) { Owner.LogWarningWithLineNumber(msg, TaskItem); } internal List Globs { get; } internal static List Parse(ITaskItem[] tasks, AddAssemblyReferencesFromPackage caller) { var result = new List(); foreach (var task in tasks) { var includeAssemblies = task.GetMetadata("IncludeAssemblies"); var excludeAssemblies = task.GetMetadata("ExcludeAssemblies"); var includeGlobs = string.IsNullOrWhiteSpace(includeAssemblies) ? new[] {DefaultInclude} : includeAssemblies.Split(':', ';'); var excludeGlobs = string.IsNullOrWhiteSpace(excludeAssemblies) ? new[] {DefaultExclude} : excludeAssemblies.Split(':', ';'); result.Add(new GlobTask(task, includeGlobs, excludeGlobs, caller)); } return result; } internal const string DefaultExclude = "Dependencies/**"; internal const string DefaultInclude = "**"; internal string PackageName { get; } private ITaskItem TaskItem { get; } private AddAssemblyReferencesFromPackage Owner { get; set; } private GlobTask(ITaskItem item, IEnumerable includePatterns, IEnumerable excludePatterns, AddAssemblyReferencesFromPackage owner) { this.Owner = owner; TaskItem = item; PackageName = item.ItemSpec; includePatterns = includePatterns.Distinct().Where(x => !string.IsNullOrWhiteSpace(x)); excludePatterns = excludePatterns.Distinct().Where(x => !string.IsNullOrWhiteSpace(x)); Globs = includePatterns.Select(x => new GlobWrapper(x, true)).Concat( excludePatterns.Select(x => new GlobWrapper(x, false))).ToList(); Globs.Sort(); } internal bool Includes(string file, TaskLoggingHelper log) { foreach (var glob in Globs) { try { if (glob.Globber.IsMatch(file)) { return glob.Include; } } catch (IndexOutOfRangeException) { log.LogError(null, "OpenTAP Reference", null, Owner.SourceFile, Owner.GetLineNumber(TaskItem), 0, 0, 0, $"Error in glob pattern {glob.Globber} when testing against '{file}'"); } } // Default to excluding something that isn't explicitly included; // Note that everything is "explicitly included" when no patterns are specified, since we then default to including the "**" pattern return false; } } /// /// MSBuild Task to help package plugin. This task is used by the OpenTAP SDK project template /// [Serializable] public class AddAssemblyReferencesFromPackage : Task { private XElement _document; private BuildVariableExpander Expander { get; set; } internal XElement Document { get { if (_document != null) return _document; var expander = new BuildVariableExpander(SourceFile); // Expand all the build variables in the document to accurately identify which element corresponds to 'item' _document = XElement.Parse(expander.ExpandBuildVariables(File.ReadAllText(SourceFile)), LoadOptions.SetLineInfo); return _document; } } internal void LogWarningWithLineNumber(string msg, ITaskItem item) { Log.LogWarning(null, "OpenTAP Reference", null, SourceFile, GetLineNumber(item), 0, 0, 0, msg); } internal int GetLineNumber(ITaskItem item) { try { var packageName = item.ItemSpec; var version = item.GetMetadata("Version"); var repository = item.GetMetadata("Repository"); var includeAssemblies = item.GetMetadata("IncludeAssemblies"); var excludeAssemblies = item.GetMetadata("ExcludeAssemblies"); includeAssemblies = string.IsNullOrWhiteSpace(includeAssemblies) ? GlobTask.DefaultInclude : includeAssemblies; excludeAssemblies = string.IsNullOrWhiteSpace(excludeAssemblies) ? GlobTask.DefaultExclude : excludeAssemblies; foreach (var elem in Document.GetPackageElements(packageName)) { if (elem is IXmlLineInfo lineInfo && lineInfo.HasLineInfo()) { var elemVersion = elem.ElemOrAttributeValue("Version", ""); var elemRepo = elem.ElemOrAttributeValue("Repository", ""); var elemIncludeAssemblies = elem.ElemOrAttributeValue("IncludeAssemblies", GlobTask.DefaultInclude); var elemExcludeAssemblies = elem.ElemOrAttributeValue("ExcludeAssemblies", GlobTask.DefaultExclude); if (elemVersion != version || elemRepo != repository || elemIncludeAssemblies != includeAssemblies || elemExcludeAssemblies != excludeAssemblies) continue; return lineInfo.LineNumber; } } } catch { // Ignore exception } return 0; } public string SourceFile { get; set; } public AddAssemblyReferencesFromPackage() { _added = new HashSet(); } private HashSet _added; private Regex dllRx = new Regex(".+\\.dll)\""); [Output] public string[] Assemblies { get; set; } [Required] public string PackageInstallDir { get; set; } public string TargetMsBuildFile { get; set; } public ITaskItem[] OpenTapPackagesToReference { get; set; } private StringBuilder Result { get; } = new StringBuilder(); private void Write() { using (var writer = File.CreateText(TargetMsBuildFile)) { writer.WriteLine(""); writer.WriteLine( ""); writer.Write(Result.ToString()); writer.WriteLine(""); } } /// /// Get the preferred path separator, based on what is used in the expansion of '$(OutDir)'. /// This is necessary because dotnet core throws up in certain circumstances when path separators are repeatedly flipped /// private string Separator { get; set; } private bool ShouldAppendSeparator { get; set; } private void WriteItemGroup(IEnumerable assembliesInPackage) { Result.AppendLine(" "); var OutDir = "$(OutDir)"; if (ShouldAppendSeparator) OutDir += Separator; foreach (var asmPath in assembliesInPackage) { // Replace all path separators the preferred path separator based on '$(OutDir)'. var asm = Separator == "\\" ? asmPath.Replace("/", Separator) : asmPath.Replace("\\", Separator); Result.AppendLine($" "); Result.AppendLine($" {OutDir}{asm}"); Result.AppendLine(" "); } Result.AppendLine(" "); } public override bool Execute() { if (OpenTapPackagesToReference == null || OpenTapPackagesToReference.Length == 0) { // This happens when a .csproj file does not specify any OpenTapPackageReferences -- simply ignore it Write(); // write an "empty" file in this case, so msbuild does not think that the task failed, and re runs it endlessly Log.LogMessage("Got 0 OpenTapPackageReference targets."); return true; } // Compute some values related to path separators in the generated props file { Expander = new BuildVariableExpander(SourceFile); string[] pathSeparators = { "\\", "/" }; var outDir = Expander.ExpandBuildVariables("$(OutDir)"); // A separator should be appended if '$(OutDir)' does not end with a path separator already ShouldAppendSeparator = pathSeparators.Contains(outDir.Last().ToString()) == false; if (ShouldAppendSeparator) { // If it does not end with a path separator, use the last path separator in '$(OutDir)' as the separator. var separatorCharIndex = pathSeparators.Max(outDir.LastIndexOf); Separator = separatorCharIndex > -1 ? outDir[separatorCharIndex].ToString() : "/"; } else Separator = "/"; } if (TargetMsBuildFile == null) throw new Exception("TargetMsBuildFile is null"); // Task instances are sometimes reused by the build engine -- ensure 'document' is reinstantiated _document = null; Assemblies = new string[] { }; try { // Distincting the tasks is not necessary, but it is a much more helpful warning than // 'No references added from package' which would be given in 'HandlePackage'. var distinctTasks = new List(OpenTapPackagesToReference.Length); foreach (var task in OpenTapPackagesToReference) { if (distinctTasks.Contains(task)) LogWarningWithLineNumber($"Duplicate entry detected.", task); else { distinctTasks.Add(task); } } if (distinctTasks.Count != OpenTapPackagesToReference.Length) Log.LogWarning("Skipped duplicate entries."); var globTasks = GlobTask.Parse(distinctTasks.ToArray(), this); Log.LogMessage($"Got {globTasks.Count} OpenTapPackageReference targets."); foreach (var task in globTasks) { var includeGlobs = task.Globs.Where(x => x.Include).Select(x => x.Globber.ToString()); var excludeGlobs = task.Globs.Where(x => x.Include == false).Select(x => x.Globber.ToString()); var includeGlobString = string.Join(",", includeGlobs); var excludeGlobString = string.Join(",", excludeGlobs); Log.LogMessage($"{task.PackageName} Include=\"{includeGlobString}\" Exclude=\"{excludeGlobString}\""); } foreach (var globTask in globTasks) { HandlePackage(globTask); } } catch (Exception e) { Log.LogWarning($"Unexpected error while parsing patterns in ''. If the problem persists, please open an issue and include the build log."); Log.LogErrorFromException(e); throw; } finally { // Ensure we always write an output file. MSBuild may get confused otherwise. Write(); } return true; } private void HandlePackage(GlobTask globTask) { var assembliesInPackage = new List(); var packageDefPath = Path.Combine(PackageInstallDir, "Packages", globTask.PackageName, "package.xml"); if (!File.Exists(packageDefPath)) { globTask.LogWarningWithLineNumber( $"No package named '{globTask.PackageName}'."); return; } var dllsInPackage = dllRx.Matches(File.ReadAllText(packageDefPath)).Cast() .Where(match => match.Groups["name"].Success) .Select(match => match.Groups["name"].Value); var matchedDlls = dllsInPackage.Where(x => globTask.Includes(x, Log)); foreach (var dllPath in matchedDlls.Distinct()) { var absolutedllPath = Path.Combine(PackageInstallDir, dllPath); // Ensure we don't add references twice if they are matched by multiple patterns if (_added.Contains(absolutedllPath)) { Log.LogMessage($"{absolutedllPath} already added. Not adding again."); continue; } _added.Add(absolutedllPath); if (IsDotNetAssembly(absolutedllPath)) { assembliesInPackage.Add(dllPath); } else { globTask.LogWarningWithLineNumber($"{absolutedllPath} not recognized as a DotNet assembly. Reference not added."); } } Log.LogMessage(MessageImportance.Normal, "Found these assemblies in OpenTAP references: " + string.Join(", ", assembliesInPackage)); if (!assembliesInPackage.Any()) { globTask.LogWarningWithLineNumber($"No references added from package '{globTask.PackageName}'."); } else { WriteItemGroup(assembliesInPackage); Assemblies = Assemblies.Concat(assembliesInPackage).ToArray(); } } private bool IsDotNetAssembly(string fullPath) { try { AssemblyName testAssembly = AssemblyName.GetAssemblyName(fullPath); return true; } catch (Exception ex) { Log.LogMessage(MessageImportance.Low, $"Could not load assembly name from '{fullPath}'. {ex}\n{ex.GetType()}\n{ex.Message}\n{ex.StackTrace}"); return false; } } } }