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;
}
}
}
}