using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Threading; using System.Xml; using System.Xml.Linq; using Microsoft.Build.Framework; using OpenTap.Authentication; using Task = Microsoft.Build.Utilities.Task; namespace Keysight.OpenTap.Sdk.MSBuild { /// /// MSBuild Task to help install packages. This task is invoked when using 'OpenTapPackageReference' and 'AdditionalOpenTapPackages' in .csproj files /// [Serializable] public class InstallOpenTapPackages : Task, ICancelableTask { internal IImageDeployer ImageDeployer { get; set; } /// /// Full qualified path to the .csproj file for which packages are being installed /// public string SourceFile { get; set; } /// /// Array of packages to install, including version and repository /// public ITaskItem[] PackagesToInstall { get; set; } /// /// Array of package repositories to resolve packages from /// public ITaskItem[] Repositories { get; set; } /// /// The build directory containing 'tap.exe' and 'OpenTAP.dll' /// public string TapDir { get; set; } /// /// The runtime directory in the OpenTAP nuget package /// public string OpenTapRuntimeDir { get; set; } /// /// The target platform defined in msbuild /// public string PlatformTarget { get; set; } private XElement _document; internal XElement Document { get { if (string.IsNullOrWhiteSpace(SourceFile)) return null; 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; } } private int[] GetLineNum(ITaskItem item) { var lines = new List(); try { var packageName = item.ItemSpec; var version = item.GetMetadata("Version"); var repository = item.GetMetadata("Repository"); var doc = Document; if (doc == null) return new[] { 0 }; foreach (var elem in doc.GetPackageElements(packageName)) { if (elem is IXmlLineInfo lineInfo && lineInfo.HasLineInfo()) { // If there is no exact match, return every possible match lines.Add(lineInfo.LineNumber); var elemVersion = elem.ElemOrAttributeValue("Version", ""); var elemRepo = elem.ElemOrAttributeValue("Repository", ""); if (elemVersion != version || elemRepo != repository) continue; return new[] {lineInfo.LineNumber}; } } } catch { // Ignore exception } if (lines.Count > 0) return lines.ToArray(); return new[] {0}; } /// /// Install the packages. This will be invoked by MSBuild /// /// public override bool Execute() { if (PackagesToInstall == null || PackagesToInstall.Length == 0) return true; using (OpenTapContext.Create(TapDir, OpenTapRuntimeDir)) return InstallPackages(); } private CancellationTokenSource cts = new CancellationTokenSource(); public void Cancel() { cts.Cancel(); } /// Load Authentication tokens from Items into settings. /// True if successful. bool ProcessAuthTokens() { var repo2token = new Dictionary(); { var repoTokens = new List<(string repo, string token)>(); foreach (var item in Repositories ?? Array.Empty()) { // Read token from var repo = item.GetMetadata("Repository")?.Trim(); var token = item.GetMetadata("Token").Trim(); if (string.IsNullOrWhiteSpace(token)) continue; if (string.IsNullOrWhiteSpace(repo)) { Log.LogError("When Token is used Repository must be specified."); return false; } repoTokens.Add((repo, token)); } foreach (var package in PackagesToInstall) { // Read token from // also applies top AdditionalOpenTapPackage. var repo = package.GetMetadata("Repository")?.Trim(); var token = package.GetMetadata("Token").Trim(); if (string.IsNullOrWhiteSpace(token)) continue; if (string.IsNullOrWhiteSpace(repo)) { Log.LogError("When Token is used Repository must be specified."); return false; } repoTokens.Add((repo, token)); } foreach (var (repo, token) in repoTokens) { if (repo2token.TryGetValue(repo, out var token2)) { if (token == token2) continue; Log.LogError($"Repository ({repo}) already specified with a different token."); return false; } repo2token.Add(repo, token); } } foreach (var tokenPair in repo2token) { try { var uri = new Uri(tokenPair.Key, UriKind.RelativeOrAbsolute); string host; if (uri.IsAbsoluteUri) { host = uri.Host; } else { host = tokenPair.Key; } AuthenticationSettings.Current.Tokens.Insert(0, new TokenInfo(tokenPair.Value, "", host)); } catch (Exception e) { Log.LogError($"Could not parse URI for token: {e.Message}"); return false; } } return true; } private void UpdateMarkerFiles() { // This is to avoid the following scenario: // 1. Install foo version 1.0.0, creating a marker file // 2. Install foo version 1.0.1, creating a marker file // 3. Downgrade foo to version 1.0.0. If marker file exists, this will be skipped foreach (var pkg in PackagesToInstall) { var name = pkg.ItemSpec; var path = Path.Combine(TapDir, "Packages", name); if (!Directory.Exists(path)) Directory.CreateDirectory(path); var markerFiles = Directory.GetFiles(path, ".v*.marker"); foreach (var f in markerFiles) { try { File.Delete(f); } catch { // ignore. Not a big deal } } var version = pkg.GetMetadata("Version") ?? ""; var fs = File.Create(Path.Combine(path, $".v{version}.marker")); fs.Close(); } } /// /// We *must* be in an OpenTapContext before calling this method because /// it depends on OpenTAP DLLs. /// /// private bool InstallPackages() { if (!ProcessAuthTokens()) return false; var repos = Repositories?.SelectMany(r => r.ItemSpec.Split(new[] { ";" }, StringSplitOptions.RemoveEmptyEntries)) .ToList() ?? new List(); repos.AddRange(PackagesToInstall.Select(p => p.GetMetadata("Repository")) .Where(m => string.IsNullOrWhiteSpace(m) == false)); repos = repos.Distinct(StringComparer.OrdinalIgnoreCase).ToList(); using (var imageInstaller = new OpenTapImageInstaller(TapDir, OpenTapRuntimeDir, cts.Token)) { imageInstaller.LogMessage += OnInstallerLogMessage; imageInstaller.ImageDeployer = ImageDeployer; try { var success = imageInstaller.InstallImage(PackagesToInstall, repos); if (success) UpdateMarkerFiles(); return success; } catch (Exception ex) { Log.LogError(ex.Message); return false; } } } private void OnInstallerLogMessage(string message, int logEventType, ITaskItem item) { // This mirrors the LogEventType enum from OpenTAP, but this class must not depend on OpenTAP being already // resolved. Special care is given to resolving the correct OpenTAP dll in the Execute method. var logLevelMap = new Dictionary() { [10] = "Error", [20] = "Warning", [30] = "Information", [40] = "Debug", }; var logLevel = logLevelMap[logEventType]; var numbers = item == null ? Array.Empty() : GetLineNum(item); var lineNumber = numbers.Any() ? numbers.First() : 0; // Log the messages with line number and source file information // A line number of '0' causes the line number to be emitted by the MSBuild logger var source = "OpenTAP Install"; switch (logLevel) { case "Error": Log.LogError(null, source, null, SourceFile, lineNumber, 0, 0, 0, message); break; case "Warning": Log.LogWarning(null, source, null, SourceFile, lineNumber, 0, 0, 0, message); break; case "Information": Log.LogMessage(null, source, null, SourceFile, lineNumber, 0, 0, 0, MessageImportance.Normal, message); break; case "Debug": Log.LogMessage(null, source, null, SourceFile, lineNumber, 0, 0, 0, MessageImportance.Low, message); break; } } } }