dotnet-installer/src/dotnet/commands/dotnet-pack/PackageGenerator.cs

381 lines
15 KiB
C#

// Copyright (c) .NET Foundation and contributors. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using Microsoft.DotNet.Cli.Utils;
using Microsoft.DotNet.ProjectModel;
using Microsoft.DotNet.ProjectModel.Files;
using Microsoft.DotNet.ProjectModel.Graph;
using Microsoft.DotNet.ProjectModel.Utilities;
using Microsoft.Extensions.FileSystemGlobbing;
using Microsoft.Extensions.FileSystemGlobbing.Abstractions;
using NuGet;
using NuGet.Frameworks;
using NuGet.Packaging.Core;
using NuGet.Versioning;
using Microsoft.DotNet.Cli.Compiler.Common;
using Microsoft.DotNet.ProjectModel.Resources;
using Microsoft.DotNet.Tools.Pack;
using PackageBuilder = NuGet.PackageBuilder;
namespace Microsoft.DotNet.Tools.Compiler
{
public class PackageGenerator
{
protected ArtifactPathsCalculator ArtifactPathsCalculator { get; }
protected Project Project { get; }
protected string Configuration { get; }
protected PackageBuilder PackageBuilder { get; private set; }
public PackageGenerator(Project project, string configuration, ArtifactPathsCalculator artifactPathsCalculator)
{
ArtifactPathsCalculator = artifactPathsCalculator;
Project = project;
Configuration = configuration;
}
public bool BuildPackage(IEnumerable<ProjectContext> contexts, List<DiagnosticMessage> packDiagnostics)
{
Reporter.Output.WriteLine($"Producing nuget package \"{GetPackageName()}\" for {Project.Name}");
PackageBuilder = CreatePackageBuilder(Project);
// TODO: Report errors for required fields
// id
// author
// description
foreach (var context in contexts)
{
Reporter.Verbose.WriteLine($"Processing {context.TargetFramework.ToString().Yellow()}");
ProcessContext(context);
Reporter.Verbose.WriteLine("");
}
var packageOutputPath = Path.Combine(
ArtifactPathsCalculator.PackageOutputPath,
GetPackageName() + NuGet.Constants.PackageExtension);
if (GeneratePackage(packageOutputPath, packDiagnostics))
{
return true;
}
return false;
}
protected virtual void ProcessContext(ProjectContext context)
{
PopulateDependencies(context);
var inputFolder = ArtifactPathsCalculator.InputPathForContext(context);
var compilationOptions = Project.GetCompilerOptions(context.TargetFramework, Configuration);
var outputName = compilationOptions.OutputName;
var outputExtension =
context.TargetFramework.IsDesktop() && compilationOptions.EmitEntryPoint.GetValueOrDefault()
? ".exe" : ".dll";
var resourceCultures = context.ProjectFile.Files.ResourceFiles
.Select(resourceFile => ResourceUtility.GetResourceCultureName(resourceFile.Key))
.Distinct();
foreach (var culture in resourceCultures)
{
if (string.IsNullOrEmpty(culture))
{
continue;
}
var resourceFilePath = Path.Combine(culture, $"{outputName}.resources.dll");
TryAddOutputFile(context, inputFolder, resourceFilePath);
}
TryAddOutputFile(context, inputFolder, outputName + outputExtension);
TryAddOutputFile(context, inputFolder, $"{outputName}.xml");
TryAddOutputFile(context, inputFolder, $"{outputName}.runtimeconfig.json");
}
protected virtual bool GeneratePackage(string nupkg, List<DiagnosticMessage> packDiagnostics)
{
foreach (var sharedFile in Project.Files.SharedFiles)
{
var file = new PhysicalPackageFile();
file.SourcePath = sharedFile;
file.TargetPath = Path.Combine("shared", Path.GetFileName(sharedFile));
PackageBuilder.Files.Add(file);
}
if (Project.Files.PackInclude != null && Project.Files.PackInclude.Any())
{
AddPackageFiles(Project.Files.PackInclude, packDiagnostics);
}
// Write the packages as long as we're still in a success state.
if (!packDiagnostics.Any(d => d.Severity == DiagnosticMessageSeverity.Error))
{
Reporter.Verbose.WriteLine($"Adding package files");
foreach (var file in PackageBuilder.Files.OfType<PhysicalPackageFile>())
{
if (file.SourcePath != null && File.Exists(file.SourcePath))
{
Reporter.Verbose.WriteLine($"Adding {file.Path.Yellow()}");
}
}
Directory.CreateDirectory(Path.GetDirectoryName(nupkg));
using (var fs = File.Create(nupkg))
{
PackageBuilder.Save(fs);
Reporter.Output.WriteLine($"{Project.Name} -> {Path.GetFullPath(nupkg)}");
}
return true;
}
return false;
}
private void AddPackageFiles(IEnumerable<PackIncludeEntry> packageFiles, IList<DiagnosticMessage> diagnostics)
{
var rootDirectory = new DirectoryInfoWrapper(new DirectoryInfo(Project.ProjectDirectory));
foreach (var match in CollectAdditionalFiles(rootDirectory, packageFiles, Project.ProjectFilePath, diagnostics))
{
PackageBuilder.Files.Add(match);
}
}
internal static IEnumerable<PhysicalPackageFile> CollectAdditionalFiles(DirectoryInfoBase rootDirectory, IEnumerable<PackIncludeEntry> projectFileGlobs, string projectFilePath, IList<DiagnosticMessage> diagnostics)
{
foreach (var entry in projectFileGlobs)
{
// Evaluate the globs on the right
var matcher = new Matcher();
matcher.AddIncludePatterns(entry.SourceGlobs);
var results = matcher.Execute(rootDirectory);
var files = results.Files.ToList();
// Check for illegal characters
if (string.IsNullOrEmpty(entry.Target))
{
diagnostics.Add(new DiagnosticMessage(
ErrorCodes.NU1003,
$"Invalid '{ProjectFilesCollection.PackIncludePropertyName}' section. The target '{entry.Target}' is invalid, " +
"targets must either be a file name or a directory suffixed with '/'. " +
"The root directory of the package can be specified by using a single '/' character.",
projectFilePath,
DiagnosticMessageSeverity.Error,
entry.Line,
entry.Column));
continue;
}
if (entry.Target.Split('/').Any(s => s.Equals(".") || s.Equals("..")))
{
diagnostics.Add(new DiagnosticMessage(
ErrorCodes.NU1004,
$"Invalid '{ProjectFilesCollection.PackIncludePropertyName}' section. " +
$"The target '{entry.Target}' contains path-traversal characters ('.' or '..'). " +
"These characters are not permitted in target paths.",
projectFilePath,
DiagnosticMessageSeverity.Error,
entry.Line,
entry.Column));
continue;
}
// Check the arity of the left
if (entry.Target.EndsWith("/"))
{
var dir = entry.Target.Substring(0, entry.Target.Length - 1).Replace('/', Path.DirectorySeparatorChar);
foreach (var file in files)
{
yield return new PhysicalPackageFile()
{
SourcePath = Path.Combine(rootDirectory.FullName, PathUtility.GetPathWithDirectorySeparator(file.Path)),
TargetPath = Path.Combine(dir, PathUtility.GetPathWithDirectorySeparator(file.Stem))
};
}
}
else
{
// It's a file. If the glob matched multiple things, we're sad :(
if (files.Count > 1)
{
// Arity mismatch!
string sourceValue = entry.SourceGlobs.Length == 1 ?
$"\"{entry.SourceGlobs[0]}\"" :
("[" + string.Join(",", entry.SourceGlobs.Select(v => $"\"{v}\"")) + "]");
diagnostics.Add(new DiagnosticMessage(
ErrorCodes.NU1005,
$"Invalid '{ProjectFilesCollection.PackIncludePropertyName}' section. " +
$"The target '{entry.Target}' refers to a single file, but the pattern {sourceValue} " +
"produces multiple files. To mark the target as a directory, suffix it with '/'.",
projectFilePath,
DiagnosticMessageSeverity.Error,
entry.Line,
entry.Column));
}
else
{
yield return new PhysicalPackageFile()
{
SourcePath = Path.Combine(rootDirectory.FullName, files[0].Path),
TargetPath = PathUtility.GetPathWithDirectorySeparator(entry.Target)
};
}
}
}
}
protected void TryAddOutputFile(ProjectContext context,
string outputPath,
string filePath)
{
var targetPath = Path.Combine("lib", context.TargetFramework.GetTwoDigitShortFolderName(), filePath);
var sourcePath = Path.Combine(outputPath, filePath);
if (!File.Exists(sourcePath))
{
return;
}
PackageBuilder.Files.Add(new PhysicalPackageFile
{
SourcePath = sourcePath,
TargetPath = targetPath
});
}
private void PopulateDependencies(ProjectContext context)
{
var dependencies = new List<PackageDependency>();
var project = context.RootProject;
foreach (var dependency in project.Dependencies)
{
if (dependency.Type.Equals(LibraryDependencyType.Build))
{
continue;
}
// TODO: Efficiency
var dependencyDescription = context.LibraryManager.GetLibraries().First(l => l.RequestedRanges.Contains(dependency));
// REVIEW: Can we get this far with unresolved dependencies
if (dependencyDescription == null || !dependencyDescription.Resolved)
{
continue;
}
if (dependencyDescription.Identity.Type == LibraryType.Project &&
((ProjectDescription)dependencyDescription).Project.EmbedInteropTypes)
{
continue;
}
if (dependency.Target == LibraryType.ReferenceAssembly)
{
PackageBuilder.FrameworkAssemblies.Add(new FrameworkAssemblyReference(dependency.Name, new[] { context.TargetFramework }));
Reporter.Verbose.WriteLine($"Adding framework assembly {dependency.Name.Yellow()}");
}
else
{
VersionRange dependencyVersion = null;
if (dependency.VersionRange == null ||
dependency.VersionRange.IsFloating)
{
dependencyVersion = new VersionRange(dependencyDescription.Identity.Version);
}
else
{
dependencyVersion = dependency.VersionRange;
}
Reporter.Verbose.WriteLine($"Adding dependency {dependency.Name.Yellow()} {VersionUtility.RenderVersion(dependencyVersion).Yellow()}");
dependencies.Add(new PackageDependency(dependency.Name, dependencyVersion));
}
}
PackageBuilder.DependencySets.Add(new PackageDependencySet(context.TargetFramework, dependencies));
}
protected virtual string GetPackageName()
{
return $"{Project.Name}.{Project.Version}";
}
private static string GetDefaultRootOutputPath(Project project, string outputOptionValue)
{
string rootOutputPath = string.Empty;
if (string.IsNullOrEmpty(outputOptionValue))
{
rootOutputPath = project.ProjectDirectory;
}
return rootOutputPath;
}
private static PackageBuilder CreatePackageBuilder(Project project)
{
var builder = new PackageBuilder();
builder.Authors.AddRange(project.Authors);
builder.Owners.AddRange(project.Owners);
if (builder.Authors.Count == 0)
{
var defaultAuthor = Environment.GetEnvironmentVariable("NUGET_AUTHOR");
if (string.IsNullOrEmpty(defaultAuthor))
{
builder.Authors.Add(project.Name);
}
else
{
builder.Authors.Add(defaultAuthor);
}
}
builder.Description = project.Description ?? project.Name;
builder.Id = project.Name;
builder.Version = project.Version;
builder.Title = project.Title;
builder.Summary = project.Summary;
builder.Copyright = project.Copyright;
builder.RequireLicenseAcceptance = project.RequireLicenseAcceptance;
builder.ReleaseNotes = project.ReleaseNotes;
builder.Language = project.Language;
builder.Tags.AddRange(project.Tags);
if (!string.IsNullOrEmpty(project.IconUrl))
{
builder.IconUrl = new Uri(project.IconUrl);
}
if (!string.IsNullOrEmpty(project.ProjectUrl))
{
builder.ProjectUrl = new Uri(project.ProjectUrl);
}
if (!string.IsNullOrEmpty(project.LicenseUrl))
{
builder.LicenseUrl = new Uri(project.LicenseUrl);
}
return builder;
}
}
}