|
// 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 NuGet.Frameworks;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
namespace Microsoft.DotNet.Build.Tasks.Packaging
{
public class GeneratePackageReport : BuildTask
{
private Dictionary<string, PackageItem> _targetPathToPackageItem;
private AggregateNuGetAssetResolver _resolver;
private Dictionary<NuGetFramework, string[]> _frameworks;
private NuGetAssetResolver _resolverWithoutPlaceholders;
private HashSet<string> _unusedTargetPaths;
[Required]
public string PackageId
{
get;
set;
}
[Required]
public string PackageVersion
{
get;
set;
}
[Required]
public ITaskItem[] Files
{
get;
set;
}
/// <summary>
/// Frameworks to evaluate.
/// Identity: Framework
/// RuntimeIDs: Semi-colon seperated list of runtime IDs
/// </summary>
[Required]
public ITaskItem[] Frameworks
{
get;
set;
}
/// <summary>
/// Path to runtime.json that contains the runtime graph.
/// </summary>
[Required]
public string RuntimeFile { get; set; }
[Required]
public ITaskItem[] PackageIndexes
{
get;
set;
}
/// <summary>
/// JSON file describing results of validation
/// </summary>
[Required]
public string ReportFile
{
get;
set;
}
public override bool Execute()
{
LoadFiles();
LoadFrameworks();
var report = new PackageReport()
{
Id = PackageId,
Version = PackageVersion,
SupportedFrameworks = new Dictionary<string, string>()
};
string package = $"{PackageId}/{PackageVersion}";
foreach (var framework in _frameworks.OrderBy(f => f.Key.ToString()))
{
var fx = framework.Key;
var runtimeIds = framework.Value;
var compileAssets = _resolver.ResolveCompileAssets(fx, PackageId);
bool hasCompileAsset, hasCompilePlaceHolder;
NuGetAssetResolver.ExamineAssets(Log, "Compile", package, fx.ToString(), compileAssets, out hasCompileAsset, out hasCompilePlaceHolder);
MarkUsed(compileAssets);
// start by making sure it has some asset available for compile
var isSupported = hasCompileAsset || hasCompilePlaceHolder;
if (runtimeIds.All(rid => !String.IsNullOrEmpty(rid)))
{
// Add Framework only (compile) target if all RIDs are non-empty.
// This acts as a compile target for a framework that requires a RID for runtime.
var reportTarget = new Target()
{
Framework = fx.ToString(),
RuntimeID = null,
CompileAssets = compileAssets.Select(c => GetPackageAssetFromTargetPath(c)).ToArray()
};
report.Targets.Add(fx.ToString(), reportTarget);
}
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);
MarkUsed(runtimeAssets);
if (!FrameworkUtilities.IsGenerationMoniker(fx) && !fx.IsPCL)
{
// only look at runtime assets for runnable frameworks.
isSupported &= (hasCompileAsset && hasRuntimeAsset) || // matching assets
(hasCompilePlaceHolder && hasRuntimeAsset) || // private runtime
(hasCompilePlaceHolder && hasRuntimePlaceHolder); // placeholders
}
var nativeAssets = _resolver.ResolveNativeAssets(fx, runtimeId);
MarkUsed(nativeAssets);
var reportTarget = new Target()
{
Framework = fx.ToString(),
RuntimeID = runtimeId,
CompileAssets = compileAssets.Select(c => GetPackageAssetFromTargetPath(c)).ToArray(),
RuntimeAssets = runtimeAssets.Select(r => GetPackageAssetFromTargetPath(r)).ToArray(),
NativeAssets = nativeAssets.Select(n => GetPackageAssetFromTargetPath(n)).ToArray()
};
report.Targets[target] = reportTarget;
}
if (isSupported)
{
// Find 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));
}
var version = "unknown";
if (refAssm != null)
{
version = _targetPathToPackageItem[AggregateNuGetAssetResolver.AsPackageSpecificTargetPath(PackageId, refAssm)].Version?.ToString() ?? version;
}
report.SupportedFrameworks.Add(fx.ToString(), version);
}
}
report.UnusedAssets = _unusedTargetPaths.Select(tp => GetPackageAssetFromTargetPath(tp)).ToArray();
report.Save(ReportFile);
return !Log.HasLoggedErrors;
}
private static string[] s_noRids = new[] { string.Empty };
private static HashSet<string> s_ignoredFrameworks = new HashSet<string>()
{
FrameworkConstants.FrameworkIdentifiers.AspNet,
FrameworkConstants.FrameworkIdentifiers.AspNetCore,
FrameworkConstants.FrameworkIdentifiers.Dnx,
FrameworkConstants.FrameworkIdentifiers.DnxCore,
FrameworkConstants.FrameworkIdentifiers.DotNet,
FrameworkConstants.FrameworkIdentifiers.NetPlatform,
FrameworkConstants.FrameworkIdentifiers.NetStandardApp,
FrameworkConstants.FrameworkIdentifiers.Silverlight,
FrameworkConstants.FrameworkIdentifiers.Windows,
FrameworkConstants.FrameworkIdentifiers.WinRT
};
private void LoadFrameworks()
{
_frameworks = new Dictionary<NuGetFramework, string[]>(NuGetFramework.Comparer);
// load the specified frameworks
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;
}
_frameworks.Add(fx, runtimeIds);
}
// inspect any TFMs explicitly targeted
var fileFrameworks = _targetPathToPackageItem.Values.Select(f => f.TargetFramework).Distinct(NuGetFramework.Comparer).Where(f => f != null);
foreach(var fileFramework in fileFrameworks)
{
if (!_frameworks.ContainsKey(fileFramework))
{
_frameworks.Add(fileFramework, s_noRids);
}
}
// inspect any TFMs inbox
var index = PackageIndex.Load(PackageIndexes.Select(pi => pi.GetMetadata("FullPath")));
var inboxFrameworks = index.GetInboxFrameworks(PackageId).NullAsEmpty();
foreach (var inboxFramework in inboxFrameworks)
{
if (!_frameworks.ContainsKey(inboxFramework))
{
_frameworks.Add(inboxFramework, s_noRids);
}
}
// inspect for derived TFMs
var expander = new FrameworkExpander();
foreach(var framework in _frameworks.Keys.ToArray())
{
var derivedFxs = expander.Expand(framework);
foreach (var derivedFx in derivedFxs)
{
if (derivedFx.IsDesktop() && derivedFx.HasProfile)
{
// skip desktop profiles
continue;
}
if (derivedFx.Version.Major == 0 && derivedFx.Version.Minor == 0)
{
// skip unversioned frameworks
continue;
}
if (s_ignoredFrameworks.Contains(derivedFx.Framework))
{
continue;
}
if (!_frameworks.ContainsKey(derivedFx))
{
_frameworks.Add(derivedFx, s_noRids);
}
}
}
}
private void LoadFiles()
{
var packageItems = new Dictionary<string, List<PackageItem>>();
foreach (var file in Files)
{
try
{
var packageItem = new PackageItem(file);
if (!packageItem.TargetPath.StartsWith("runtimes") && !packageItem.IsDll && !packageItem.IsPlaceholder)
{
continue;
}
if (String.IsNullOrWhiteSpace(packageItem.TargetPath))
{
Log.LogError($"{packageItem.TargetPath} is missing TargetPath metadata");
}
string packageId = packageItem.Package ?? PackageId;
if (!packageItems.ContainsKey(packageId))
{
packageItems[packageId] = new List<PackageItem>();
}
packageItems[packageId].Add(packageItem);
}
catch (Exception ex)
{
Log.LogError($"Could not parse File {file.ItemSpec}. {ex}");
// skip it.
}
}
// build a map to translate back to source file from resolved asset
// we use package-specific paths since we're resolving a set of packages.
_targetPathToPackageItem = new Dictionary<string, PackageItem>();
_unusedTargetPaths = new HashSet<string>();
foreach (var packageSpecificItems in packageItems)
{
foreach (PackageItem packageItem in packageSpecificItems.Value)
{
string packageSpecificTargetPath = AggregateNuGetAssetResolver.AsPackageSpecificTargetPath(packageSpecificItems.Key, packageItem.TargetPath);
if (_targetPathToPackageItem.ContainsKey(packageSpecificTargetPath))
{
Log.LogError($"Files {_targetPathToPackageItem[packageSpecificTargetPath].SourcePath} and {packageItem.SourcePath} have the same TargetPath {packageSpecificTargetPath}.");
}
_targetPathToPackageItem[packageSpecificTargetPath] = packageItem;
_unusedTargetPaths.Add(packageSpecificTargetPath);
}
}
_resolver = new AggregateNuGetAssetResolver(RuntimeFile);
foreach (string packageId in packageItems.Keys)
{
_resolver.AddPackageItems(packageId, packageItems[packageId].Select(f => f.TargetPath));
}
// 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
if (packageItems.Any() && packageItems.ContainsKey(PackageId))
{
var filesWithoutPlaceholders = packageItems[PackageId]
.Select(pf => pf.TargetPath)
.Where(f => !NuGetAssetResolver.IsPlaceholder(f));
_resolverWithoutPlaceholders = new NuGetAssetResolver(RuntimeFile, filesWithoutPlaceholders);
}
}
private PackageAsset GetPackageAssetFromTargetPath(string targetPath)
{
PackageItem packageItem = null;
if (!_targetPathToPackageItem.TryGetValue(targetPath, out packageItem))
{
throw new ArgumentException($"Could not find source item for {targetPath}", nameof(targetPath));
}
var packageAsset = new PackageAsset()
{
HarvestedFrom = packageItem.HarvestedFrom,
LocalPath = packageItem.SourcePath,
PackagePath = packageItem.TargetPath,
TargetFramework = packageItem.TargetFramework,
Version = packageItem.Version
};
if (packageItem.SourceProject != null)
{
packageAsset.SourceProject = new BuildProject()
{
Project = packageItem.SourceProject,
AdditionalProperties = packageItem.AdditionalProperties,
UndefineProperties = packageItem.UndefineProperties
};
}
return packageAsset;
}
private void MarkUsed(IEnumerable<string> targetPaths)
{
foreach(var targetPath in targetPaths)
{
_unusedTargetPaths.Remove(targetPath);
}
}
}
}
|