|
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
#nullable disable
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices;
using NuGet.Commands;
using NuGet.Common;
using NuGet.Frameworks;
using NuGet.LibraryModel;
using NuGet.Packaging;
using NuGet.Packaging.Core;
using NuGet.Packaging.Licenses;
using NuGet.ProjectModel;
using NuGet.Versioning;
using PackageSpecificWarningProperties = NuGet.Commands.PackCommand.PackageSpecificWarningProperties;
namespace NuGet.Build.Tasks.Pack
{
public class PackTaskLogic : IPackTaskLogic
{
private const string IdentityProperty = "Identity";
private PackageSpecificWarningProperties _packageSpecificWarningProperties;
public PackArgs GetPackArgs(IPackTaskRequest<IMSBuildItem> request)
{
var packArgs = new PackArgs
{
InstallPackageToOutputPath = request.InstallPackageToOutputPath,
OutputFileNamesWithoutVersion = request.OutputFileNamesWithoutVersion,
OutputDirectory = request.PackageOutputPath,
Serviceable = request.Serviceable,
Tool = request.IsTool,
Symbols = request.IncludeSymbols,
SymbolPackageFormat = PackArgs.GetSymbolPackageFormat(request.SymbolPackageFormat),
BasePath = request.NuspecBasePath,
NoPackageAnalysis = request.NoPackageAnalysis,
NoDefaultExcludes = request.NoDefaultExcludes,
Deterministic = request.Deterministic,
DeterministicTimestamp = request.DeterministicTimestamp,
WarningProperties = WarningProperties.GetWarningProperties(request.TreatWarningsAsErrors, request.WarningsAsErrors, request.NoWarn, request.WarningsNotAsErrors),
PackTargetArgs = new MSBuildPackTargetArgs()
};
packArgs.Logger = new PackCollectorLogger(request.Logger, packArgs.WarningProperties, _packageSpecificWarningProperties);
if (request.MinClientVersion != null)
{
Version version;
if (!Version.TryParse(request.MinClientVersion, out version))
{
throw new PackagingException(NuGetLogCode.NU5022, string.Format(
CultureInfo.CurrentCulture,
Strings.InvalidMinClientVersion,
request.MinClientVersion));
}
packArgs.MinClientVersion = version;
}
LockFile assetsFile = GetAssetsFile(request);
(Dictionary<string, string> targetAliasToNuGetFramework, Dictionary<string, List<string>> nuGetFrameworkToDuplicateAliases) = BuildAliasFrameworkMappings(assetsFile);
InitCurrentDirectoryAndFileName(request, packArgs);
InitNuspecOutputPath(request, packArgs);
PackCommandRunner.SetupCurrentDirectory(packArgs);
if (!string.IsNullOrEmpty(request.NuspecFile))
{
if (request.NuspecProperties != null && request.NuspecProperties.Any())
{
packArgs.Properties.AddRange(ParsePropertiesAsDictionary(request.NuspecProperties));
if (packArgs.Properties.TryGetValue("version", out var version))
{
packArgs.Version = version;
}
}
}
else
{
// This only needs to happen when packing via csproj, not nuspec.
packArgs.PackTargetArgs.AllowedOutputExtensionsInPackageBuildOutputFolder = InitOutputExtensions(request.AllowedOutputExtensionsInPackageBuildOutputFolder);
packArgs.PackTargetArgs.AllowedOutputExtensionsInSymbolsPackageBuildOutputFolder = InitOutputExtensions(request.AllowedOutputExtensionsInSymbolsPackageBuildOutputFolder);
packArgs.PackTargetArgs.TargetPathsToAssemblies = InitLibFiles(request.BuildOutputInPackage, targetAliasToNuGetFramework, nuGetFrameworkToDuplicateAliases);
packArgs.PackTargetArgs.TargetPathsToSymbols = InitLibFiles(request.TargetPathsToSymbols, targetAliasToNuGetFramework, nuGetFrameworkToDuplicateAliases);
packArgs.PackTargetArgs.AssemblyName = request.AssemblyName;
packArgs.PackTargetArgs.IncludeBuildOutput = request.IncludeBuildOutput;
packArgs.PackTargetArgs.BuildOutputFolder = request.BuildOutputFolders;
packArgs.PackTargetArgs.TargetFrameworks = ParseFrameworks(request, targetAliasToNuGetFramework);
if (request.IncludeSource)
{
packArgs.PackTargetArgs.SourceFiles = GetSourceFiles(request, packArgs.CurrentDirectory);
packArgs.Symbols = request.IncludeSource;
}
var contentFiles = ProcessContentToIncludeInPackage(request, packArgs);
packArgs.PackTargetArgs.ContentFiles = contentFiles;
}
return packArgs;
}
public PackageBuilder GetPackageBuilder(IPackTaskRequest<IMSBuildItem> request)
{
// Load the assets JSON file produced by restore.
var assetsFilePath = Path.Combine(request.RestoreOutputPath, LockFileFormat.AssetsFileName);
if (!File.Exists(assetsFilePath))
{
throw new PackagingException(NuGetLogCode.NU5023, string.Format(
CultureInfo.CurrentCulture,
Strings.AssetsFileNotFound,
assetsFilePath));
}
var builder = new PackageBuilder(request.Deterministic, request.Logger)
{
Id = request.PackageId,
Description = request.Description,
Title = request.Title,
Copyright = request.Copyright,
ReleaseNotes = request.ReleaseNotes,
RequireLicenseAcceptance = request.RequireLicenseAcceptance,
EmitRequireLicenseAcceptance = request.RequireLicenseAcceptance,
PackageTypes = ParsePackageTypes(request),
DeterministicTimestamp = request.DeterministicTimestamp,
};
if (request.DevelopmentDependency)
{
builder.DevelopmentDependency = true;
}
if (request.PackageVersion != null)
{
NuGetVersion version;
if (!NuGetVersion.TryParse(request.PackageVersion, out version))
{
throw new PackagingException(NuGetLogCode.NU5024, string.Format(
CultureInfo.CurrentCulture,
Strings.InvalidPackageVersion,
request.PackageVersion));
}
builder.Version = version;
}
else
{
builder.Version = new NuGetVersion("1.0.0");
}
if (request.Authors != null)
{
builder.Authors.AddRange(request.Authors);
}
if (request.Tags != null)
{
builder.Tags.AddRange(request.Tags);
}
Uri tempUri;
if (Uri.TryCreate(request.LicenseUrl, UriKind.Absolute, out tempUri))
{
builder.LicenseUrl = tempUri;
}
if (Uri.TryCreate(request.ProjectUrl, UriKind.Absolute, out tempUri))
{
builder.ProjectUrl = tempUri;
}
if (Uri.TryCreate(request.IconUrl, UriKind.Absolute, out tempUri))
{
builder.IconUrl = tempUri;
}
if (!string.IsNullOrEmpty(request.RepositoryUrl) || !string.IsNullOrEmpty(request.RepositoryType))
{
builder.Repository = new RepositoryMetadata(
request.RepositoryType,
request.RepositoryUrl,
request.RepositoryBranch,
request.RepositoryCommit);
}
builder.LicenseMetadata = BuildLicenseMetadata(request);
builder.Icon = request.PackageIcon;
builder.Readme = request.Readme;
if (request.MinClientVersion != null)
{
Version version;
if (!Version.TryParse(request.MinClientVersion, out version))
{
throw new PackagingException(NuGetLogCode.NU5022, string.Format(
CultureInfo.CurrentCulture,
Strings.InvalidMinClientVersion,
request.MinClientVersion));
}
builder.MinClientVersion = version;
}
// The assets file is necessary for project and package references. Pack should not do any traversal,
// so we leave that work up to restore (which produces the assets file).
var lockFileFormat = new LockFileFormat();
var assetsFile = lockFileFormat.Read(assetsFilePath);
if (assetsFile.PackageSpec == null)
{
throw new PackagingException(NuGetLogCode.NU5025, string.Format(
CultureInfo.CurrentCulture,
Strings.AssetsFileDoesNotHaveValidPackageSpec,
assetsFilePath));
}
var projectRefToVersionMap = new Dictionary<string, string>(PathUtility.GetStringComparerBasedOnOS());
if (request.ProjectReferencesWithVersions != null && request.ProjectReferencesWithVersions.Any())
{
projectRefToVersionMap = request
.ProjectReferencesWithVersions
.ToDictionary(msbuildItem => msbuildItem.Identity,
msbuildItem => msbuildItem.GetProperty("ProjectVersion"), PathUtility.GetStringComparerBasedOnOS());
}
(Dictionary<string, string> targetAliasToNuGetFramework, Dictionary<string, List<string>> nuGetFrameworkToDuplicateAliases) = BuildAliasFrameworkMappings(assetsFile);
var frameworksWithSuppressedDependencies = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
if (request.FrameworksWithSuppressedDependencies != null && request.FrameworksWithSuppressedDependencies.Any())
{
frameworksWithSuppressedDependencies = [.. request.FrameworksWithSuppressedDependencies.Select(t => t.Identity)];
}
PopulateProjectAndPackageReferences(builder,
assetsFile,
projectRefToVersionMap,
frameworksWithSuppressedDependencies,
nuGetFrameworkToDuplicateAliases);
PopulateFrameworkAssemblyReferences(builder, request);
PopulateFrameworkReferences(builder, assetsFile, targetAliasToNuGetFramework, nuGetFrameworkToDuplicateAliases);
return builder;
}
private LicenseMetadata BuildLicenseMetadata(IPackTaskRequest<IMSBuildItem> request)
{
var hasLicenseExpression = !string.IsNullOrEmpty(request.PackageLicenseExpression);
var hasLicenseFile = !string.IsNullOrEmpty(request.PackageLicenseFile);
if (hasLicenseExpression || hasLicenseFile)
{
if (!string.IsNullOrEmpty(request.LicenseUrl))
{
throw new PackagingException(NuGetLogCode.NU5035, string.Format(
CultureInfo.CurrentCulture,
Strings.NuGetLicenses_LicenseUrlCannotBeUsedInConjuctionWithLicense));
}
if (hasLicenseExpression && hasLicenseFile)
{
throw new PackagingException(NuGetLogCode.NU5033, string.Format(
CultureInfo.CurrentCulture,
Strings.InvalidLicenseCombination,
request.PackageLicenseExpression));
}
var version = GetLicenseExpressionVersion(request);
if (hasLicenseExpression)
{
if (version.CompareTo(LicenseMetadata.CurrentVersion) <= 0)
{
try
{
var expression = NuGetLicenseExpression.Parse(request.PackageLicenseExpression);
return new LicenseMetadata(
type: LicenseType.Expression,
license: request.PackageLicenseExpression,
expression: expression,
warningsAndErrors: null,
version: version);
}
catch (NuGetLicenseExpressionParsingException e)
{
throw new PackagingException(NuGetLogCode.NU5032, string.Format(
CultureInfo.CurrentCulture,
Strings.InvalidLicenseExpression,
request.PackageLicenseExpression, e.Message),
e);
}
}
else
{
throw new PackagingException(NuGetLogCode.NU5034, string.Format(
CultureInfo.CurrentCulture,
Strings.InvalidLicenseExppressionVersion_VersionTooHigh,
request.PackageLicenseExpressionVersion,
LicenseMetadata.CurrentVersion));
}
}
if (hasLicenseFile)
{
return new LicenseMetadata(
type: LicenseType.File,
license: request.PackageLicenseFile,
expression: null,
warningsAndErrors: null,
version: version);
}
}
return null;
}
private static Version GetLicenseExpressionVersion(IPackTaskRequest<IMSBuildItem> request)
{
Version version;
if (string.IsNullOrEmpty(request.PackageLicenseExpressionVersion))
{
version = LicenseMetadata.EmptyVersion;
}
else
{
if (!Version.TryParse(request.PackageLicenseExpressionVersion, out version))
{
throw new PackagingException(NuGetLogCode.NU5034, string.Format(
CultureInfo.CurrentCulture,
Strings.InvalidLicenseExpressionVersion,
request.PackageLicenseExpressionVersion));
}
}
return version;
}
private LockFile GetAssetsFile(IPackTaskRequest<IMSBuildItem> request)
{
if (request.PackItem == null)
{
throw new PackagingException(NuGetLogCode.NU5028, Strings.NoPackItemProvided);
}
string assetsFilePath = Path.Combine(request.RestoreOutputPath, LockFileFormat.AssetsFileName);
if (!File.Exists(assetsFilePath))
{
throw new InvalidOperationException(string.Format(
CultureInfo.CurrentCulture,
Strings.AssetsFileNotFound,
assetsFilePath));
}
// The assets file is necessary for project and package references. Pack should not do any traversal,
// so we leave that work up to restore (which produces the assets file).
var lockFileFormat = new LockFileFormat();
return lockFileFormat.Read(assetsFilePath);
}
/// <summary>
/// Builds mappings from target alias to NuGet framework short name, and identifies
/// frameworks that have multiple aliases resolving to the same NuGet framework.
/// </summary>
/// <returns>
/// A tuple of (targetAliasToNuGetFramework, duplicateAliases).
/// duplicateAliases is null when there are no duplicate frameworks; otherwise it contains
/// only the entries where multiple aliases map to the same framework.
/// </returns>
private static (Dictionary<string, string> targetAliasToNuGetFramework, Dictionary<string, List<string>> duplicateAliases)
BuildAliasFrameworkMappings(LockFile assetsFile)
{
var targetAliasToNuGetFramework = new Dictionary<string, string>();
var frameworkToAliases = new Dictionary<string, List<string>>();
bool hasDuplicates = false;
foreach (var tfm in assetsFile.PackageSpec.TargetFrameworks)
{
var tfmShortName = tfm.FrameworkName.GetShortFolderName();
targetAliasToNuGetFramework[tfm.TargetAlias] = tfmShortName;
if (!frameworkToAliases.TryGetValue(tfmShortName, out List<string> list))
{
frameworkToAliases[tfmShortName] = [tfm.TargetAlias];
}
else
{
list.Add(tfm.TargetAlias);
hasDuplicates = true;
}
}
if (!hasDuplicates)
{
return (targetAliasToNuGetFramework, null);
}
// Keep only entries where more than one alias maps to the same framework.
foreach (var key in frameworkToAliases.Where(e => e.Value.Count <= 1).Select(e => e.Key).ToList())
{
frameworkToAliases.Remove(key);
}
return (targetAliasToNuGetFramework, frameworkToAliases);
}
private static void PopulateFrameworkAssemblyReferences(PackageBuilder builder, IPackTaskRequest<IMSBuildItem> request)
{
// First add all the assembly references which are not specific to a certain TFM.
var tfmSpecificRefs = new Dictionary<string, IList<string>>(StringComparer.OrdinalIgnoreCase);
// Then add the TFM specific framework assembly references, and ignore any which have already been added above.
foreach (var tfmRef in request.FrameworkAssemblyReferences)
{
var targetFramework = tfmRef.GetProperty("TargetFramework");
if (tfmSpecificRefs.ContainsKey(tfmRef.Identity))
{
tfmSpecificRefs[tfmRef.Identity].Add(targetFramework);
}
else
{
tfmSpecificRefs.Add(tfmRef.Identity, new List<string>() { targetFramework });
}
}
builder.FrameworkReferences.AddRange(
tfmSpecificRefs.Select(
t => new FrameworkAssemblyReference(
t.Key, t.Value.Select(
k => NuGetFramework.Parse(k))
)));
}
private static void PopulateFrameworkReferences(
PackageBuilder builder,
LockFile assetsFile,
Dictionary<string, string> targetAliasToNuGetFramework,
Dictionary<string, List<string>> nuGetFrameworkToDuplicateAliases)
{
var tfmSpecificRefs = new Dictionary<string, ISet<string>>();
bool hasAnyRefs = false;
foreach (var framework in assetsFile.PackageSpec.TargetFrameworks)
{
var frameworkShortFolderName = targetAliasToNuGetFramework[framework.TargetAlias];
var refNames = new HashSet<string>(ComparisonUtility.FrameworkReferenceNameComparer);
foreach (var frameworkRef in framework.FrameworkReferences.Where(e => e.PrivateAssets != FrameworkDependencyFlags.All))
{
refNames.Add(frameworkRef.Name);
hasAnyRefs = true;
}
if (!tfmSpecificRefs.ContainsKey(frameworkShortFolderName))
{
tfmSpecificRefs.Add(frameworkShortFolderName, refNames);
}
else if (!tfmSpecificRefs[frameworkShortFolderName].SetEquals(refNames))
{
List<string> duplicates = nuGetFrameworkToDuplicateAliases[frameworkShortFolderName];
throw new PackagingException(NuGetLogCode.NU5051,
string.Format(CultureInfo.CurrentCulture,
Strings.DuplicateFrameworks,
string.Join(", ", duplicates) + " => " + frameworkShortFolderName,
string.Format(
CultureInfo.CurrentCulture,
Strings.AmbigiousFrameworkReferences,
string.Join(", ", duplicates))));
}
// else: same framework refs as already stored — skip.
}
if (hasAnyRefs)
{
builder.FrameworkReferenceGroups.AddRange(
tfmSpecificRefs.Select(e =>
new FrameworkReferenceGroup(
NuGetFramework.Parse(e.Key),
e.Value.Select(fr => new FrameworkReference(fr)))));
}
}
public PackCommandRunner GetPackCommandRunner(
IPackTaskRequest<IMSBuildItem> request,
PackArgs packArgs,
PackageBuilder packageBuilder)
{
var runner = new PackCommandRunner(
packArgs,
MSBuildProjectFactory.ProjectCreator,
packageBuilder);
runner.GenerateNugetPackage = request.ContinuePackingAfterGeneratingNuspec;
return runner;
}
public bool BuildPackage(PackCommandRunner runner)
{
return runner.RunPackageBuild();
}
private static IEnumerable<OutputLibFile> InitLibFiles(IMSBuildItem[] libFiles, IDictionary<string, string> targetAliasToNuGetFramework, Dictionary<string, List<string>> nuGetFrameworkToDuplicateAliases)
{
var assemblies = new List<OutputLibFile>();
if (libFiles == null)
{
return assemblies;
}
Dictionary<string, string> duplicateTargetPath = null;
if (nuGetFrameworkToDuplicateAliases != null)
{
duplicateTargetPath = new Dictionary<string, string>();
}
foreach (var assembly in libFiles)
{
var finalOutputPath = assembly.GetProperty("FinalOutputPath");
// Fallback to using Identity if FinalOutputPath is not set.
// See bug https://github.com/NuGet/Home/issues/5408
if (string.IsNullOrEmpty(finalOutputPath))
{
finalOutputPath = assembly.GetProperty(IdentityProperty);
}
var targetPath = assembly.GetProperty("TargetPath");
var targetFrameworkProperty = assembly.GetProperty("TargetFramework");
if (!File.Exists(finalOutputPath))
{
throw new PackagingException(NuGetLogCode.NU5026, string.Format(CultureInfo.CurrentCulture, Strings.Error_FileNotFound, finalOutputPath));
}
string translated = null;
var succeeded = targetAliasToNuGetFramework.TryGetValue(targetFrameworkProperty, out translated);
string targetFramework = succeeded ? translated : targetFrameworkProperty;
// If target path is not set, default it to the file name. Only satellite DLLs have a special target path
// where culture is part of the target path. This condition holds true for files like runtimeconfig.json file
// in netcore projects.
if (targetPath == null)
{
targetPath = Path.GetFileName(finalOutputPath);
}
if (duplicateTargetPath?.TryGetValue(targetPath, out string existingTfm) == true)
{
throw new PackagingException(NuGetLogCode.NU5051,
string.Format(CultureInfo.CurrentCulture,
Strings.DuplicateFrameworks,
existingTfm + ", " + targetFrameworkProperty + " => " + targetFramework,
string.Format(CultureInfo.CurrentCulture, Strings.AmbigiousPackageAssemblies, targetPath)));
}
duplicateTargetPath?.Add(targetPath, targetFrameworkProperty);
if (string.IsNullOrEmpty(targetFramework) || NuGetFramework.Parse(targetFramework).IsSpecificFramework == false)
{
throw new PackagingException(NuGetLogCode.NU5027, string.Format(CultureInfo.CurrentCulture, Strings.InvalidTargetFramework, finalOutputPath));
}
assemblies.Add(new OutputLibFile()
{
FinalOutputPath = finalOutputPath,
TargetPath = targetPath,
TargetFramework = targetFramework
});
}
return assemblies;
}
private static ISet<NuGetFramework> ParseFrameworks(IPackTaskRequest<IMSBuildItem> request, IDictionary<string, string> targetAliasToNuGetFramework)
{
var nugetFrameworks = new HashSet<NuGetFramework>();
if (request.TargetFrameworks != null)
{
nugetFrameworks = new HashSet<NuGetFramework>(request.TargetFrameworks.Select(targetFramework =>
{
string translated = null;
var succeeded = targetAliasToNuGetFramework.TryGetValue(targetFramework, out translated);
if (succeeded)
{
targetFramework = translated;
}
return NuGetFramework.Parse(targetFramework);
}));
}
return nugetFrameworks;
}
private ICollection<PackageType> ParsePackageTypes(IPackTaskRequest<IMSBuildItem> request)
{
var listOfPackageTypes = new List<PackageType>();
if (request.PackageTypes != null)
{
foreach (var packageType in request.PackageTypes)
{
var packageTypeSplitInPart = packageType.Split(new char[] { ',' });
var packageTypeName = packageTypeSplitInPart[0].Trim();
var version = PackageType.EmptyVersion;
if (packageTypeSplitInPart.Length > 1)
{
var versionString = packageTypeSplitInPart[1];
_ = Version.TryParse(versionString, out version);
}
listOfPackageTypes.Add(new PackageType(packageTypeName, version));
}
}
return listOfPackageTypes;
}
private void InitCurrentDirectoryAndFileName(IPackTaskRequest<IMSBuildItem> request, PackArgs packArgs)
{
if (request.PackItem == null)
{
throw new PackagingException(NuGetLogCode.NU5028, Strings.NoPackItemProvided);
}
packArgs.CurrentDirectory = Path.Combine(
request.PackItem.GetProperty("RootDir"),
request.PackItem.GetProperty("Directory")).TrimEnd(Path.DirectorySeparatorChar);
packArgs.Arguments = new string[]
{
!string.IsNullOrEmpty(request.NuspecFile)
? request.NuspecFile
: string.Concat(request.PackItem.GetProperty("FileName"), request.PackItem.GetProperty("Extension"))
};
packArgs.Path = !string.IsNullOrEmpty(request.NuspecFile)
? request.NuspecFile
: request.PackItem.GetProperty("FullPath");
packArgs.Exclude = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
}
private void InitNuspecOutputPath(IPackTaskRequest<IMSBuildItem> request, PackArgs packArgs)
{
if (Path.IsPathRooted(request.NuspecOutputPath))
{
packArgs.PackTargetArgs.NuspecOutputPath = request.NuspecOutputPath;
}
else
{
packArgs.PackTargetArgs.NuspecOutputPath = Path.Combine(
packArgs.CurrentDirectory,
request.NuspecOutputPath);
}
}
private Dictionary<string, IEnumerable<ContentMetadata>> ProcessContentToIncludeInPackage(
IPackTaskRequest<IMSBuildItem> request,
PackArgs packArgs)
{
// This maps from source path on disk to target path inside the nupkg.
var fileModel = new Dictionary<string, IEnumerable<ContentMetadata>>();
if (request.PackageFiles != null)
{
var excludeFiles = CalculateFilesToExcludeInPack(request);
foreach (var packageFile in request.PackageFiles)
{
var sourcePath = GetSourcePath(packageFile);
if (excludeFiles.Contains(sourcePath))
{
continue;
}
var totalContentMetadata = GetContentMetadata(packageFile, sourcePath, packArgs, request.ContentTargetFolders);
if (fileModel.ContainsKey(sourcePath))
{
var existingContentMetadata = fileModel[sourcePath];
fileModel[sourcePath] = existingContentMetadata.Concat(totalContentMetadata);
}
else
{
var existingContentMetadata = new List<ContentMetadata>();
existingContentMetadata.AddRange(totalContentMetadata);
fileModel.Add(sourcePath, existingContentMetadata);
}
}
}
return fileModel;
}
// The targetpaths returned from this function contain the directory in the nuget package where the file would go to. The filename is added later on to the target path.
// whether or not the filename is added later on is dependent upon the fact that does the targetpath resolved here ends with a directory separator char or not.
private IEnumerable<ContentMetadata> GetContentMetadata(IMSBuildItem packageFile, string sourcePath,
PackArgs packArgs, string[] contentTargetFolders)
{
var targetPaths = contentTargetFolders
.Select(PathUtility.EnsureTrailingSlash)
.ToList();
var isPackagePathSpecified = packageFile.Properties.Contains("PackagePath");
// if user specified a PackagePath, then use that. Look for any ** which are indicated by the RecrusiveDir metadata in msbuild.
if (isPackagePathSpecified)
{
// The rule here is that if the PackagePath is an empty string, then we add the file to the root of the package.
// Instead if it is a ';' delimited string, then the user needs to specify a '\' to indicate that the file should go to the root of the package.
var packagePathString = packageFile.GetProperty("PackagePath");
targetPaths = packagePathString == null
? new string[] { string.Empty }.ToList()
: MSBuildStringUtility.Split(packagePathString)
.Distinct()
.ToList();
var recursiveDir = packageFile.GetProperty("RecursiveDir");
// The below NuGetRecursiveDir workaround needs to be done due to msbuild bug https://github.com/Microsoft/msbuild/issues/3121
recursiveDir = string.IsNullOrEmpty(recursiveDir) ? packageFile.GetProperty("NuGetRecursiveDir") : recursiveDir;
if (!string.IsNullOrEmpty(recursiveDir))
{
var newTargetPaths = new List<string>();
var fileName = Path.GetFileName(sourcePath);
foreach (var targetPath in targetPaths)
{
newTargetPaths.Add(PathUtility.GetStringComparerBasedOnOS().
Compare(Path.GetExtension(fileName),
Path.GetExtension(targetPath)) == 0
&& !string.IsNullOrEmpty(Path.GetExtension(fileName))
? targetPath
: Path.Combine(targetPath, recursiveDir));
}
targetPaths = newTargetPaths;
}
}
var buildActionString = packageFile.GetProperty("BuildAction");
var buildAction = BuildAction.Parse(string.IsNullOrEmpty(buildActionString) ? "None" : buildActionString);
// TODO: Do the work to get the right language of the project, tracked via https://github.com/NuGet/Home/issues/4100
var language = buildAction.Equals(BuildAction.Compile) ? "cs" : "any";
var setOfTargetPaths = new HashSet<string>(targetPaths, PathUtility.GetStringComparerBasedOnOS());
// If package path wasn't specified, then we expand the "contentFiles" value we
// got from ContentTargetFolders and expand it to contentFiles/any/<TFM>/
if (!isPackagePathSpecified)
{
if (setOfTargetPaths.Remove("contentFiles" + Path.DirectorySeparatorChar)
|| setOfTargetPaths.Remove("contentFiles"))
{
foreach (var framework in packArgs.PackTargetArgs.TargetFrameworks)
{
setOfTargetPaths.Add(PathUtility.EnsureTrailingSlash(
Path.Combine("contentFiles", language, framework.GetShortFolderName()
)));
}
}
}
// this if condition means there is no package path provided, file is within the project directory
// and the target path should preserve this relative directory structure.
// This case would be something like :
// <Content Include= "folderA\folderB\abc.txt">
// Since the package path wasn't specified, we will add this to the package paths obtained via ContentTargetFolders and preserve
// relative directory structure
if (!isPackagePathSpecified &&
sourcePath.StartsWith(packArgs.CurrentDirectory, StringComparison.CurrentCultureIgnoreCase) &&
!Path.GetFileName(sourcePath)
.Equals(packageFile.GetProperty(IdentityProperty), StringComparison.CurrentCultureIgnoreCase))
{
var newTargetPaths = new List<string>();
var identity = packageFile.GetProperty(IdentityProperty);
// Identity can be a rooted absolute path too, in which case find the path relative to the current directory
if (Path.IsPathRooted(identity))
{
identity = PathUtility.GetRelativePath(PathUtility.EnsureTrailingSlash(packArgs.CurrentDirectory), identity);
identity = Path.GetDirectoryName(identity);
}
// If identity is not a rooted path, then it is a relative path to the project directory
else if (identity.EndsWith(Path.GetFileName(sourcePath), StringComparison.CurrentCultureIgnoreCase))
{
identity = Path.GetDirectoryName(identity);
}
foreach (var targetPath in setOfTargetPaths)
{
var newTargetPath = Path.Combine(targetPath, identity);
// We need to do this because evaluated identity in the above line of code can be an empty string
// in the case when the original identity string was the absolute path to a file in project directory, and is in
// the same directory as the csproj file.
newTargetPath = PathUtility.EnsureTrailingSlash(newTargetPath);
newTargetPaths.Add(newTargetPath);
}
setOfTargetPaths = new HashSet<string>(newTargetPaths, PathUtility.GetStringComparerBasedOnOS());
}
// we take the final set of evaluated target paths and append the file name to it if not
// already done. we check whether the extension of the target path is the same as the extension
// of the source path and add the filename accordingly.
var totalSetOfTargetPaths = new List<string>();
foreach (var targetPath in setOfTargetPaths)
{
var currentPath = targetPath;
var fileName = Path.GetFileName(sourcePath);
if (string.IsNullOrEmpty(Path.GetExtension(fileName)) ||
!Path.GetExtension(fileName)
.Equals(Path.GetExtension(targetPath), StringComparison.OrdinalIgnoreCase))
{
currentPath = Path.Combine(targetPath, fileName);
}
totalSetOfTargetPaths.Add(currentPath);
}
return totalSetOfTargetPaths.Select(target => new ContentMetadata()
{
BuildAction = buildAction.Value,
Source = sourcePath,
Target = target,
CopyToOutput = packageFile.GetProperty("PackageCopyToOutput"),
Flatten = packageFile.GetProperty("PackageFlatten")
});
}
private string GetSourcePath(IMSBuildItem packageFile)
{
var sourcePath = packageFile.GetProperty("FullPath");
if (packageFile.Properties.Contains("MSBuildSourceProjectFile"))
{
var sourceProjectFile = packageFile.GetProperty("MSBuildSourceProjectFile");
var identity = packageFile.GetProperty(IdentityProperty);
sourcePath = Path.Combine(sourceProjectFile.Replace(Path.GetFileName(sourceProjectFile), string.Empty), identity);
}
return Path.GetFullPath(sourcePath);
}
private ISet<string> CalculateFilesToExcludeInPack(IPackTaskRequest<IMSBuildItem> request)
{
var excludeFiles = new HashSet<string>();
if (request.PackageFilesToExclude != null)
{
foreach (var file in request.PackageFilesToExclude)
{
var sourcePath = GetSourcePath(file);
excludeFiles.Add(sourcePath);
}
}
return excludeFiles;
}
private IDictionary<string, string> GetSourceFiles(IPackTaskRequest<IMSBuildItem> request, string currentProjectDirectory)
{
var sourceFiles = new Dictionary<string, string>();
if (request.SourceFiles != null)
{
foreach (var src in request.SourceFiles)
{
var sourcePath = GetSourcePath(src);
var sourceProjectFile = currentProjectDirectory;
if (src.Properties.Contains("MSBuildSourceProjectFile"))
{
sourceProjectFile = src.GetProperty("MSBuildSourceProjectFile");
sourceProjectFile = Path.GetDirectoryName(sourceProjectFile);
}
sourceFiles[sourcePath] = sourceProjectFile;
}
}
return sourceFiles;
}
private void PopulateProjectAndPackageReferences(PackageBuilder packageBuilder, LockFile assetsFile,
IDictionary<string, string> projectRefToVersionMap,
ISet<string> frameworksWithSuppressedDependencies,
Dictionary<string, List<string>> nuGetFrameworkToDuplicateAliases)
{
if (nuGetFrameworkToDuplicateAliases != null)
{
foreach (var duplicates in nuGetFrameworkToDuplicateAliases)
{
var unsuppressedAliases = duplicates.Value
.Where(alias => !frameworksWithSuppressedDependencies.Contains(alias))
.ToList();
if (unsuppressedAliases.Count > 1)
{
throw new PackagingException(NuGetLogCode.NU5051,
string.Format(CultureInfo.CurrentCulture,
Strings.DuplicateFrameworks,
string.Join(", ", duplicates.Value) + " => " + duplicates.Key,
string.Format(CultureInfo.CurrentCulture, Strings.AmbigiousPackageDependencies, string.Join(", ", unsuppressedAliases))));
}
}
}
var dependenciesByFramework = new Dictionary<NuGetFramework, HashSet<LibraryDependency>>(NuGetFrameworkFullComparer.Instance);
InitializeProjectDependencies(assetsFile, dependenciesByFramework, projectRefToVersionMap, frameworksWithSuppressedDependencies);
InitializePackageDependencies(assetsFile, dependenciesByFramework, frameworksWithSuppressedDependencies);
foreach (var pair in dependenciesByFramework)
{
PackCommandRunner.AddDependencyGroups(pair.Value, pair.Key, packageBuilder);
}
}
private static void InitializeProjectDependencies(
LockFile assetsFile,
IDictionary<NuGetFramework, HashSet<LibraryDependency>> dependenciesByFramework,
IDictionary<string, string> projectRefToVersionMap,
ISet<string> frameworkWithSuppressedDependencies)
{
// From the package spec, all we know is each absolute path to the project reference the the target
// framework that project reference applies to.
if (assetsFile.PackageSpec.RestoreMetadata == null)
{
return;
}
// Using the libraries section of the assets file, the library name and version for the project path.
var projectPathToLibraryIdentities = assetsFile
.Libraries
.Where(library => library.MSBuildProject != null)
.ToLookup(
library => Path.GetFullPath(Path.Combine(
Path.GetDirectoryName(assetsFile.PackageSpec.RestoreMetadata.ProjectPath),
PathUtility.GetPathWithDirectorySeparator(library.MSBuildProject))),
library => new PackageIdentity(library.Name, library.Version),
PathUtility.GetStringComparerBasedOnOS());
// Consider all of the project references, grouped by target framework.
foreach (var framework in assetsFile.PackageSpec.RestoreMetadata.TargetFrameworks)
{
var target = assetsFile.GetTarget(framework.TargetAlias, runtimeIdentifier: null);
if (target == null || frameworkWithSuppressedDependencies.Contains(framework.TargetAlias))
{
continue;
}
HashSet<LibraryDependency> dependencies;
if (!dependenciesByFramework.TryGetValue(framework.FrameworkName, out dependencies))
{
dependencies = new HashSet<LibraryDependency>();
dependenciesByFramework[framework.FrameworkName] = dependencies;
}
// For the current target framework, create a map from library identity to library model. This allows
// us to be sure we have picked the correct library (name and version) for this target framework.
var libraryIdentityToTargetLibrary = target
.Libraries
.ToLookup(library => new PackageIdentity(library.Name, library.Version));
foreach (var projectReference in framework.ProjectReferences)
{
var libraryIdentities = projectPathToLibraryIdentities[projectReference.ProjectPath];
var targetLibrary = libraryIdentities
.Select(identity => libraryIdentityToTargetLibrary[identity].FirstOrDefault())
.FirstOrDefault(library => library != null);
if (targetLibrary == null)
{
continue;
}
var versionToUse = new VersionRange(targetLibrary.Version);
// Use the project reference version obtained at build time if it exists, otherwise fallback to the one in assets file.
if (projectRefToVersionMap.TryGetValue(projectReference.ProjectPath, out var projectRefVersion))
{
versionToUse = VersionRange.Parse(projectRefVersion, allowFloating: false);
}
// TODO: Implement <TreatAsPackageReference>false</TreatAsPackageReference>
// https://github.com/NuGet/Home/issues/3891
//
// For now, assume the project reference is a package dependency.
var projectDependency = new LibraryDependency()
{
LibraryRange = new LibraryRange(
targetLibrary.Name,
versionToUse,
LibraryDependencyTarget.All),
IncludeType = projectReference.IncludeAssets & ~projectReference.ExcludeAssets,
SuppressParent = projectReference.PrivateAssets
};
PackCommandRunner.AddLibraryDependency(projectDependency, dependencies);
}
}
}
private void InitializePackageDependencies(
LockFile assetsFile,
Dictionary<NuGetFramework, HashSet<LibraryDependency>> dependenciesByFramework,
ISet<string> frameworkWithSuppressedDependencies)
{
var packageSpecificNoWarnProperties = new Dictionary<string, HashSet<(NuGetLogCode, NuGetFramework)>>(StringComparer.OrdinalIgnoreCase);
var frameworks = assetsFile.PackageSpec.TargetFrameworks;
// From the package spec, we know the direct package dependencies of this project.
for (var i = 0; i < frameworks.Count; i++)
{
var framework = frameworks[i];
if (frameworkWithSuppressedDependencies.Contains(framework.TargetAlias))
{
continue;
}
// First, the framework-specific dependencies
var newFrameworkDependencies = AddDependencies(framework.Dependencies, dependenciesByFramework, framework, assetsFile, packageSpecificNoWarnProperties);
framework = new TargetFrameworkInformation(framework) { Dependencies = newFrameworkDependencies };
// Next, the central transitive dependencies
foreach (var centralTDG in assetsFile.CentralTransitiveDependencyGroups)
{
if (centralTDG.FrameworkName.Equals(framework.FrameworkName.ToString(), StringComparison.OrdinalIgnoreCase))
{
AddDependencies(centralTDG.TransitiveDependencies.ToList(), dependenciesByFramework, framework, assetsFile, packageSpecificNoWarnProperties);
}
}
var dependencies = dependenciesByFramework[framework.FrameworkName];
dependencies.RemoveWhere(dependency => IsDependencyPruned(dependency, framework.PackagesToPrune));
frameworks[i] = framework;
}
if (packageSpecificNoWarnProperties.Keys.Count > 0)
{
_packageSpecificWarningProperties = PackageSpecificWarningProperties.CreatePackageSpecificWarningProperties(packageSpecificNoWarnProperties);
}
static bool IsDependencyPruned(LibraryDependency dependency, IReadOnlyDictionary<string, PrunePackageReference> packagesToPrune)
{
if (packagesToPrune?.TryGetValue(dependency.Name, out PrunePackageReference packageToPrune) == true
&& dependency.LibraryRange.VersionRange.Satisfies(packageToPrune.VersionRange.MaxVersion))
{
return true;
}
return false;
}
}
private static void AddDependencies(
IList<LibraryDependency> packageDependencies,
Dictionary<NuGetFramework, HashSet<LibraryDependency>> dependenciesByFramework,
TargetFrameworkInformation framework,
LockFile assetsFile,
Dictionary<string, HashSet<(NuGetLogCode, NuGetFramework)>> packageSpecificNoWarnProperties)
{
HashSet<LibraryDependency> dependencies;
if (!dependenciesByFramework.TryGetValue(framework.FrameworkName, out dependencies))
{
dependencies = new HashSet<LibraryDependency>();
dependenciesByFramework[framework.FrameworkName] = dependencies;
}
// Add each package dependency.
for (int i = 0; i < packageDependencies.Count; i++)
{
var updatedPackageDependency = GetUpdatedPackageDependency(packageDependencies[i], assetsFile, framework, packageSpecificNoWarnProperties, dependencies);
packageDependencies[i] = updatedPackageDependency;
}
}
private static ImmutableArray<LibraryDependency> AddDependencies(
ImmutableArray<LibraryDependency> packageDependencies,
Dictionary<NuGetFramework, HashSet<LibraryDependency>> dependenciesByFramework,
TargetFrameworkInformation framework,
LockFile assetsFile,
Dictionary<string, HashSet<(NuGetLogCode, NuGetFramework)>> packageSpecificNoWarnProperties)
{
HashSet<LibraryDependency> dependencies;
if (!dependenciesByFramework.TryGetValue(framework.FrameworkName, out dependencies))
{
dependencies = new HashSet<LibraryDependency>();
dependenciesByFramework[framework.FrameworkName] = dependencies;
}
LibraryDependency[] updatedDependencies = new LibraryDependency[packageDependencies.Length];
// Add each package dependency.
for (var i = 0; i < packageDependencies.Length; i++)
{
var updatedPackageDependency = GetUpdatedPackageDependency(packageDependencies[i], assetsFile, framework, packageSpecificNoWarnProperties, dependencies);
updatedDependencies[i] = updatedPackageDependency;
}
return ImmutableCollectionsMarshal.AsImmutableArray(updatedDependencies);
}
private static LibraryDependency GetUpdatedPackageDependency(
LibraryDependency packageDependency,
LockFile assetsFile,
TargetFrameworkInformation framework,
Dictionary<string, HashSet<(NuGetLogCode, NuGetFramework)>> packageSpecificNoWarnProperties,
HashSet<LibraryDependency> dependencies)
{
// If we have a floating package dependency like 1.2.3-xyz-*, we
// use the version of the package that restore resolved it to.
if (packageDependency.LibraryRange.VersionRange.IsFloating)
{
var lockFileTarget = assetsFile.GetTarget(framework.FrameworkName, runtimeIdentifier: null);
var package = lockFileTarget.Libraries.First(
library =>
string.Equals(library.Name, packageDependency.Name, StringComparison.OrdinalIgnoreCase));
VersionRange versionRange;
if (packageDependency.LibraryRange.VersionRange.HasUpperBound)
{
versionRange = new VersionRange(
minVersion: package.Version,
includeMinVersion: packageDependency.LibraryRange.VersionRange.IsMinInclusive,
maxVersion: packageDependency.LibraryRange.VersionRange.MaxVersion,
includeMaxVersion: packageDependency.LibraryRange.VersionRange.IsMaxInclusive);
}
else
{
versionRange = new VersionRange(
minVersion: package.Version,
includeMinVersion: packageDependency.LibraryRange.VersionRange.IsMinInclusive);
}
var libraryRange = new LibraryRange(packageDependency.LibraryRange) { VersionRange = versionRange };
packageDependency = new LibraryDependency(packageDependency) { LibraryRange = libraryRange };
}
if (packageDependency.NoWarn.Length > 0)
{
HashSet<(NuGetLogCode, NuGetFramework)> nowarnProperties = null;
if (!packageSpecificNoWarnProperties.TryGetValue(packageDependency.Name, out nowarnProperties))
{
nowarnProperties = new HashSet<(NuGetLogCode, NuGetFramework)>();
}
nowarnProperties.AddRange(packageDependency.NoWarn.Select(n => (n, framework.FrameworkName)));
packageSpecificNoWarnProperties[packageDependency.Name] = nowarnProperties;
}
PackCommandRunner.AddLibraryDependency(packageDependency, dependencies);
return packageDependency;
}
private static IDictionary<string, string> ParsePropertiesAsDictionary(string[] properties)
{
var dictionary = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
foreach (var item in properties)
{
var index = item.IndexOf("=", StringComparison.Ordinal);
// Make sure '=' is not the first or the last character of the string
if (index > 0 && index < item.Length - 1)
{
var key = item.Substring(0, index);
var value = item.Substring(index + 1);
dictionary[key] = value;
}
// if value is empty string, set it to string.Empty instead of erroring out
else if (index == item.Length - 1)
{
var key = item.Substring(0, index);
var value = string.Empty;
dictionary[key] = value;
}
else
{
throw new PackagingException(NuGetLogCode.NU5029, Strings.InvalidNuspecProperties);
}
}
return dictionary;
}
private HashSet<string> InitOutputExtensions(IEnumerable<string> outputExtensions)
{
return new HashSet<string>(outputExtensions.Distinct(StringComparer.OrdinalIgnoreCase));
}
}
}
|