|
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using Microsoft.Build.Framework;
using Microsoft.Build.Utilities;
using Newtonsoft.Json;
using NuGet.ContentModel;
using NuGet.Frameworks;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
namespace Microsoft.DotNet.Build.Tasks.Packaging
{
public class HarvestPackage : BuildTask
{
/// <summary>
/// Package ID to harvest
/// </summary>
[Required]
public string PackageId { get; set; }
/// <summary>
/// Current package version.
/// </summary>
[Required]
public string PackageVersion { get; set; }
/// <summary>
/// Folder where packages have been restored
/// </summary>
[Required]
public string[] PackagesFolders { get; set; }
/// <summary>
/// Path to runtime.json that contains the runtime graph.
/// </summary>
[Required]
public string RuntimeFile { get; set; }
/// <summary>
/// Additional packages to consider for evaluating support but not harvesting assets.
/// Identity: Package ID
/// Version: Package version.
/// </summary>
public ITaskItem[] RuntimePackages { get; set; }
/// <summary>
/// Set to false to suppress harvesting of files and only harvest supported framework information.
/// </summary>
public bool HarvestAssets { get; set; }
/// <summary>
/// Set to true to harvest all files by default.
/// </summary>
public bool IncludeAllPaths { get; set; }
/// <summary>
/// Set to partial paths to exclude from file harvesting.
/// </summary>
public string[] PathsToExclude { get; set; }
/// <summary>
/// Set to partial paths to include from file harvesting.
/// </summary>
public ITaskItem[] PathsToInclude { get; set; }
/// <summary>
/// Set to partial paths to suppress from both file and support harvesting.
/// </summary>
public string[] PathsToSuppress { get; set; }
/// <summary>
/// Frameworks to consider for support evaluation.
/// Identity: Framework
/// RuntimeIDs: Semi-colon separated list of runtime IDs
/// </summary>
public ITaskItem[] Frameworks { get; set; }
/// <summary>
/// Files already in the package.
/// Identity: path to file
/// AssemblyVersion: version of assembly
/// TargetFramework: target framework moniker to use for harvesting file's dependencies
/// TargetPath: path of file in package
/// IsReferenceAsset: true for files in Ref.
/// </summary>
public ITaskItem[] Files { get; set; }
/// <summary>
/// Frameworks that were supported by previous package version.
/// Identity: Framework
/// Version: Assembly version if supported
/// </summary>
[Output]
public ITaskItem[] SupportedFrameworks { get; set; }
/// <summary>
/// Files harvested from previous package version.
/// Identity: path to file
/// AssemblyVersion: version of assembly
/// TargetFramework: target framework moniker to use for harvesting file's dependencies
/// TargetPath: path of file in package
/// IsReferenceAsset: true for files in Ref.
/// </summary>
[Output]
public ITaskItem[] HarvestedFiles { get; set; }
/// <summary>
/// When Files are specified, contains the updated set of files, with removals.
/// </summary>
[Output]
public ITaskItem[] UpdatedFiles { get; set; }
private Dictionary<string, string> _packageFolders = new Dictionary<string, string>();
/// <summary>
/// Generates a table in markdown that lists the API version supported by
/// various packages at all levels of NETStandard.
/// </summary>
/// <returns></returns>
public override bool Execute()
{
if (LocatePackages())
{
if (HarvestAssets)
{
HarvestFilesFromPackage();
}
if (Frameworks != null && Frameworks.Length > 0)
{
HarvestSupportedFrameworks();
}
}
return !Log.HasLoggedErrors;
}
private bool LocatePackages()
{
_packageFolders.Add(PackageId, LocatePackageFolder(PackageId, PackageVersion));
if (RuntimePackages != null)
{
foreach (var runtimePackage in RuntimePackages)
{
_packageFolders.Add(runtimePackage.ItemSpec, LocatePackageFolder(runtimePackage.ItemSpec, runtimePackage.GetMetadata("Version")));
}
}
return _packageFolders.Values.All(f => f != null);
}
private void HarvestSupportedFrameworks()
{
List<ITaskItem> supportedFrameworks = new List<ITaskItem>();
AggregateNuGetAssetResolver resolver = new AggregateNuGetAssetResolver(RuntimeFile);
string packagePath = _packageFolders[PackageId];
foreach (var packageFolder in _packageFolders)
{
resolver.AddPackageItems(packageFolder.Key, GetPackageItems(packageFolder.Value));
}
// create a resolver that can be used to determine the API version for inbox assemblies
// since inbox assemblies are represented with placeholders we can remove the placeholders
// and use the netstandard reference assembly to determine the API version
var filesWithoutPlaceholders = GetPackageItems(packagePath)
.Where(f => !NuGetAssetResolver.IsPlaceholder(f));
NuGetAssetResolver resolverWithoutPlaceholders = new NuGetAssetResolver(RuntimeFile, filesWithoutPlaceholders);
string package = $"{PackageId}/{PackageVersion}";
foreach (var framework in Frameworks)
{
var runtimeIds = framework.GetMetadata("RuntimeIDs")?.Split(';');
NuGetFramework fx;
try
{
fx = FrameworkUtilities.ParseNormalized(framework.ItemSpec);
}
catch (Exception ex)
{
Log.LogError($"Could not parse Framework {framework.ItemSpec}. {ex}");
continue;
}
if (fx.Equals(NuGetFramework.UnsupportedFramework))
{
Log.LogError($"Did not recognize {framework.ItemSpec} as valid Framework.");
continue;
}
var compileAssets = resolver.ResolveCompileAssets(fx, PackageId);
bool hasCompileAsset, hasCompilePlaceHolder;
NuGetAssetResolver.ExamineAssets(Log, "Compile", package, fx.ToString(), compileAssets, out hasCompileAsset, out hasCompilePlaceHolder);
// start by making sure it has some asset available for compile
var isSupported = hasCompileAsset || hasCompilePlaceHolder;
if (!isSupported)
{
Log.LogMessage(LogImportance.Low, $"Skipping {fx} because it is not supported.");
continue;
}
foreach (var runtimeId in runtimeIds)
{
string target = String.IsNullOrEmpty(runtimeId) ? fx.ToString() : $"{fx}/{runtimeId}";
var runtimeAssets = resolver.ResolveRuntimeAssets(fx, runtimeId);
bool hasRuntimeAsset, hasRuntimePlaceHolder;
NuGetAssetResolver.ExamineAssets(Log, "Runtime", package, target, runtimeAssets, out hasRuntimeAsset, out hasRuntimePlaceHolder);
isSupported &= hasCompileAsset == hasRuntimeAsset;
isSupported &= hasCompilePlaceHolder == hasRuntimePlaceHolder;
if (!isSupported)
{
Log.LogMessage(LogImportance.Low, $"Skipping {fx} because it is not supported on {target}.");
break;
}
}
if (isSupported)
{
var supportedFramework = new TaskItem(framework.ItemSpec);
supportedFramework.SetMetadata("HarvestedFromPackage", package);
// set version
// first try the resolved compile asset for this package
var refAssm = compileAssets.FirstOrDefault(r => !NuGetAssetResolver.IsPlaceholder(r))?.Substring(PackageId.Length + 1);
if (refAssm == null)
{
// if we didn't have a compile asset it means this framework is supported inbox with a placeholder
// resolve the assets without placeholders to pick up the netstandard reference assembly.
compileAssets = resolverWithoutPlaceholders.ResolveCompileAssets(fx);
refAssm = compileAssets.FirstOrDefault(r => !NuGetAssetResolver.IsPlaceholder(r));
}
string version = "unknown";
if (refAssm != null)
{
version = VersionUtility.GetAssemblyVersion(Path.Combine(packagePath, refAssm))?.ToString() ?? version;
}
supportedFramework.SetMetadata("Version", version);
Log.LogMessage(LogImportance.Low, $"Validating version {version} for {supportedFramework.ItemSpec} because it was supported by {PackageId}/{PackageVersion}.");
supportedFrameworks.Add(supportedFramework);
}
}
SupportedFrameworks = supportedFrameworks.ToArray();
}
public void HarvestFilesFromPackage()
{
string pathToPackage = _packageFolders[PackageId];
var livePackageItems = Files.NullAsEmpty()
.Where(f => IsIncludedExtension(f.GetMetadata("Extension")))
.Select(f => new PackageItem(f));
var livePackageFiles = new Dictionary<string, PackageItem>(StringComparer.OrdinalIgnoreCase);
foreach (var livePackageItem in livePackageItems)
{
PackageItem existingitem;
if (livePackageFiles.TryGetValue(livePackageItem.TargetPath, out existingitem))
{
Log.LogError($"Package contains two files with same targetpath: {livePackageItem.TargetPath}, items:{livePackageItem.SourcePath}, {existingitem.SourcePath}.");
}
else
{
livePackageFiles.Add(livePackageItem.TargetPath, livePackageItem);
}
}
var harvestedFiles = new List<ITaskItem>();
var removeFiles = new List<ITaskItem>();
// make sure we preserve refs that match desktop assemblies
var liveDesktopDlls = livePackageFiles.Values.Where(pi => pi.IsDll && pi.TargetFramework?.Framework == FrameworkConstants.FrameworkIdentifiers.Net);
var desktopRefVersions = liveDesktopDlls.Where(d => d.IsRef && d.Version != null).Select(d => d.Version);
var desktopLibVersions = liveDesktopDlls.Where(d => !d.IsRef && d.Version != null).Select(d => d.Version);
// find desktop assemblies with no matching lib.
var preserveRefVersion = new HashSet<Version>(desktopLibVersions);
preserveRefVersion.ExceptWith(desktopRefVersions);
foreach (var extension in s_includedExtensions)
{
foreach (var packageFile in Directory.EnumerateFiles(pathToPackage, $"*{extension}", SearchOption.AllDirectories))
{
string harvestPackagePath = packageFile.Substring(pathToPackage.Length + 1).Replace('\\', '/');
// determine if we should include this file from the harvested package
// exclude if its specifically set for exclusion
if (ShouldExclude(harvestPackagePath))
{
Log.LogMessage(LogImportance.Low, $"Excluding package path {harvestPackagePath} because it is specifically excluded.");
continue;
}
ITaskItem includeItem = null;
if (!IncludeAllPaths && !ShouldInclude(harvestPackagePath, out includeItem))
{
Log.LogMessage(LogImportance.Low, $"Excluding package path {harvestPackagePath} because it is not included in {nameof(PathsToInclude)}.");
continue;
}
// allow for the harvested item to be moved
var remappedTargetPath = includeItem?.GetMetadata("TargetPath");
if (!String.IsNullOrEmpty(remappedTargetPath))
{
harvestPackagePath = remappedTargetPath + '/' + Path.GetFileName(packageFile);
}
List<string> targetPaths = new List<string>() { harvestPackagePath };
var additionalTargetPaths = includeItem?.GetMetadata("AdditionalTargetPath");
if (!String.IsNullOrEmpty(additionalTargetPaths))
{
foreach (var additionalTargetPath in additionalTargetPaths.Split(';'))
{
if (!String.IsNullOrEmpty(additionalTargetPath))
{
targetPaths.Add(additionalTargetPath + '/' + Path.GetFileName(packageFile));
}
}
}
var assemblyVersion = extension == s_dll ? VersionUtility.GetAssemblyVersion(packageFile) : null;
PackageItem liveFile = null;
foreach (var livePackagePath in targetPaths)
{
// determine if the harvested file clashes with a live built file
// we'll prefer the harvested reference assembly so long as it's the same API
// version and not required to match implementation 1:1 as is the case for desktop
if (livePackageFiles.TryGetValue(livePackagePath, out liveFile))
{
// Not a dll, or not a versioned assembly: prefer live built file.
if (extension != s_dll || assemblyVersion == null || liveFile.Version == null)
{
// we don't consider this an error even for explicitly included files
Log.LogMessage(LogImportance.Low, $"Preferring live build of package path {livePackagePath} over the asset from last stable package because the file is not versioned.");
continue;
}
// not a ref
if (!liveFile.IsRef)
{
LogSkipIncludedFile(livePackagePath, " because it is a newer implementation.");
continue;
}
// preserve desktop references to ensure bindingRedirects will work.
if (liveFile.TargetFramework.Framework == FrameworkConstants.FrameworkIdentifiers.Net)
{
LogSkipIncludedFile(livePackagePath, " because it is desktop reference.");
continue;
}
// as above but handle the case where a netstandard ref may be used for a desktop impl.
if (preserveRefVersion.Contains(liveFile.Version))
{
LogSkipIncludedFile(livePackagePath, " because it will be applicable for desktop projects.");
continue;
}
// preserve references with a different major.minor version
if (assemblyVersion.Major != liveFile.Version.Major ||
assemblyVersion.Minor != liveFile.Version.Minor)
{
LogSkipIncludedFile(livePackagePath, $" because it is a different API version ( {liveFile.Version.Major}.{liveFile.Version.Minor} vs {assemblyVersion.Major}.{assemblyVersion.Minor}.");
continue;
}
// preserve references that specifically set the preserve metadata.
bool preserve = false;
bool.TryParse(liveFile.OriginalItem.GetMetadata("Preserve"), out preserve);
if (preserve)
{
LogSkipIncludedFile(livePackagePath, " because it set metadata Preserve=true.");
continue;
}
// replace the live file with the harvested one, removing both the live file and PDB from the
// file list.
Log.LogMessage($"Using reference {livePackagePath} from last stable package {PackageId}/{PackageVersion} rather than the built reference {liveFile.SourcePath} since it is the same API version. Set <Preserve>true</Preserve> on {liveFile.SourceProject} if you'd like to avoid this..");
removeFiles.Add(liveFile.OriginalItem);
PackageItem livePdbFile;
if (livePackageFiles.TryGetValue(Path.ChangeExtension(livePackagePath, ".pdb"), out livePdbFile))
{
removeFiles.Add(livePdbFile.OriginalItem);
}
}
else
{
Log.LogMessage(LogImportance.Low, $"Including {livePackagePath} from last stable package {PackageId}/{PackageVersion}.");
}
var item = new TaskItem(packageFile);
if (liveFile?.OriginalItem != null)
{
// preserve all the meta-data from the live file that was replaced.
liveFile.OriginalItem.CopyMetadataTo(item);
}
else
{
if (includeItem != null)
{
includeItem.CopyMetadataTo(item);
}
var targetPath = Path.GetDirectoryName(livePackagePath).Replace('\\', '/');
item.SetMetadata("TargetPath", targetPath);
string targetFramework = GetTargetFrameworkFromPackagePath(targetPath);
item.SetMetadata("TargetFramework", targetFramework);
// only harvest for non-portable frameworks, matches logic in packaging.targets.
bool harvestDependencies = !targetFramework.StartsWith("portable-");
item.SetMetadata("HarvestDependencies", harvestDependencies.ToString());
item.SetMetadata("IsReferenceAsset", IsReferencePackagePath(targetPath).ToString());
}
if (assemblyVersion != null)
{
// overwrite whatever metadata may have been copied from the live file.
item.SetMetadata("AssemblyVersion", assemblyVersion.ToString());
}
item.SetMetadata("HarvestedFrom", $"{PackageId}/{PackageVersion}/{harvestPackagePath}");
harvestedFiles.Add(item);
}
}
}
HarvestedFiles = harvestedFiles.ToArray();
if (_pathsNotIncluded != null)
{
foreach (var pathNotIncluded in _pathsNotIncluded)
{
Log.LogError($"Path '{pathNotIncluded}' was specified in {nameof(PathsToInclude)} but was not found in the package {PackageId}/{PackageVersion}.");
}
}
if (Files != null)
{
UpdatedFiles = Files.Except(removeFiles).ToArray();
}
}
private string LocatePackageFolder(string packageId, string packageVersion)
{
foreach (var packageFolder in PackagesFolders)
{
var candidateFolder = Path.Combine(packageFolder, packageId, packageVersion);
if (Directory.Exists(candidateFolder))
{
return candidateFolder;
}
// handle lower-case restore path
candidateFolder = Path.Combine(packageFolder, packageId.ToLowerInvariant(), packageVersion.ToLowerInvariant());
if (Directory.Exists(candidateFolder))
{
return candidateFolder;
}
}
Log.LogError($"Cannot locate package '{PackageId}' version '{PackageVersion}' under '{string.Join(", ", PackagesFolders)}'. Harvesting is needed to redistribute assets and ensure compatibility with the previous release. You can disable this by setting HarvestStablePackage=false.");
return null;
}
private void LogSkipIncludedFile(string packagePath, string reason)
{
if (IncludeAllPaths)
{
Log.LogMessage(LogImportance.Low, $"Preferring live build of package path {packagePath} over the asset from last stable package{reason}.");
}
else
{
Log.LogError($"Package path {packagePath} was specified to be harvested but it conflicts with live build{reason}.");
}
}
private HashSet<string> _pathsToExclude = null;
private bool ShouldExclude(string packagePath)
{
if (_pathsToExclude == null)
{
_pathsToExclude = new HashSet<string>(PathsToExclude.NullAsEmpty().Select(NormalizePath), StringComparer.OrdinalIgnoreCase);
}
return ShouldSuppress(packagePath) || ProbePath(packagePath, _pathsToExclude);
}
private Dictionary<string, ITaskItem> _pathsToInclude = null;
private HashSet<string> _pathsNotIncluded = null;
private bool ShouldInclude(string packagePath, out ITaskItem includeItem)
{
if (_pathsToInclude == null)
{
_pathsToInclude = PathsToInclude.NullAsEmpty().ToDictionary(i => NormalizePath(i.ItemSpec), i=> i, StringComparer.OrdinalIgnoreCase);
_pathsNotIncluded = new HashSet<string>(_pathsToInclude.Keys);
}
return ProbePath(packagePath, _pathsToInclude, _pathsNotIncluded, out includeItem);
}
private HashSet<string> _pathsToSuppress = null;
private bool ShouldSuppress(string packagePath)
{
if (_pathsToSuppress == null)
{
_pathsToSuppress = new HashSet<string>(PathsToSuppress.NullAsEmpty().Select(NormalizePath));
}
return ProbePath(packagePath, _pathsToSuppress);
}
private static bool ProbePath(string path, ICollection<string> pathsIncluded)
{
for (var probePath = NormalizePath(path);
!String.IsNullOrEmpty(probePath);
probePath = NormalizePath(Path.GetDirectoryName(probePath)))
{
if (pathsIncluded.Contains(probePath))
{
return true;
}
}
return false;
}
private static bool ProbePath<T>(string path, IDictionary<string, T> pathsIncluded, ICollection<string> pathsNotIncluded, out T result)
{
result = default(T);
for (var probePath = NormalizePath(path);
!String.IsNullOrEmpty(probePath);
probePath = NormalizePath(Path.GetDirectoryName(probePath)))
{
if (pathsIncluded.TryGetValue(probePath, out result))
{
pathsNotIncluded.Remove(probePath);
return true;
}
}
return false;
}
private static string NormalizePath(string path)
{
return path?.Replace('\\', '/')?.Trim();
}
private static string GetTargetFrameworkFromPackagePath(string path)
{
var parts = path.Split(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar);
if (parts.Length >= 2)
{
if (parts[0].Equals("lib", StringComparison.OrdinalIgnoreCase) ||
parts[0].Equals("ref", StringComparison.OrdinalIgnoreCase))
{
return parts[1];
}
if (parts.Length >= 4 &&
parts[0].Equals("runtimes", StringComparison.OrdinalIgnoreCase) &&
parts[2].Equals("lib", StringComparison.OrdinalIgnoreCase))
{
return parts[3];
}
}
return null;
}
private static string s_dll = ".dll";
private static string[] s_includedExtensions = new[] { s_dll, ".pdb", ".xml", "._" };
private static bool IsIncludedExtension(string extension)
{
return extension != null && extension.Length > 0 && s_includedExtensions.Contains(extension, StringComparer.OrdinalIgnoreCase);
}
private static bool IsReferencePackagePath(string path)
{
return path.StartsWith("ref", StringComparison.OrdinalIgnoreCase);
}
private IEnumerable<string> GetPackageItems(string packageFolder)
{
return Directory.EnumerateFiles(packageFolder, "*", SearchOption.AllDirectories)
.Select(f => f.Substring(packageFolder.Length + 1).Replace('\\', '/'))
.Where(f => !ShouldSuppress(f));
}
}
}
|