From df3a5fba7a78e8990178f214d5031da1667a01a1 Mon Sep 17 00:00:00 2001 From: David Fowler Date: Thu, 12 Nov 2015 03:50:38 -0800 Subject: [PATCH] First pass at dotnet-pack - Ported nuget package building code over from dnu. Moved that code to use NuGet v3 primitives. - Simplified the package builder API - Left out resources and schema detection for now - This folder should remain self contained as the code will be copied into NuGet v3. - Missing features include symbols packages --- .../Compilation/LibraryExporter.cs | 7 +- .../Graph/LibraryDependencyType.cs | 2 +- .../Utilities/FrameworksExtensions.cs | 43 +- .../Utilities/PathUtility.cs | 5 - .../Utilities/VersionUtility.cs | 4 +- .../Program.cs | 9 +- .../NuGet/Constants.cs | 64 +++ .../NuGet/FrameworkAssemblyReference.cs | 29 ++ .../NuGet/IPackageFile.cs | 20 + .../NuGet/Manifest.cs | 87 ++++ .../NuGet/ManifestMetadata.cs | 84 ++++ .../NuGet/ManifestSchemaUtility.cs | 77 +++ .../NuGet/ManifestVersionAttribute.cs | 18 + .../NuGet/ManifestVersionUtility.cs | 111 ++++ .../NuGet/PackageBuilder.cs | 382 ++++++++++++++ .../NuGet/PackageDependencySet.cs | 39 ++ .../NuGet/PackageIdValidator.cs | 72 +++ .../NuGet/PackageMetadataXmlExtensions.cs | 182 +++++++ .../NuGet/PackageReferenceSet.cs | 34 ++ .../NuGet/PathUtility.cs | 33 ++ .../NuGet/PhysicalPackageFile.cs | 80 +++ src/Microsoft.DotNet.Tools.Pack/Program.cs | 475 ++++++++++++++++++ src/Microsoft.DotNet.Tools.Pack/project.json | 31 ++ 23 files changed, 1870 insertions(+), 18 deletions(-) create mode 100644 src/Microsoft.DotNet.Tools.Pack/NuGet/Constants.cs create mode 100644 src/Microsoft.DotNet.Tools.Pack/NuGet/FrameworkAssemblyReference.cs create mode 100644 src/Microsoft.DotNet.Tools.Pack/NuGet/IPackageFile.cs create mode 100644 src/Microsoft.DotNet.Tools.Pack/NuGet/Manifest.cs create mode 100644 src/Microsoft.DotNet.Tools.Pack/NuGet/ManifestMetadata.cs create mode 100644 src/Microsoft.DotNet.Tools.Pack/NuGet/ManifestSchemaUtility.cs create mode 100644 src/Microsoft.DotNet.Tools.Pack/NuGet/ManifestVersionAttribute.cs create mode 100644 src/Microsoft.DotNet.Tools.Pack/NuGet/ManifestVersionUtility.cs create mode 100644 src/Microsoft.DotNet.Tools.Pack/NuGet/PackageBuilder.cs create mode 100644 src/Microsoft.DotNet.Tools.Pack/NuGet/PackageDependencySet.cs create mode 100644 src/Microsoft.DotNet.Tools.Pack/NuGet/PackageIdValidator.cs create mode 100644 src/Microsoft.DotNet.Tools.Pack/NuGet/PackageMetadataXmlExtensions.cs create mode 100644 src/Microsoft.DotNet.Tools.Pack/NuGet/PackageReferenceSet.cs create mode 100644 src/Microsoft.DotNet.Tools.Pack/NuGet/PathUtility.cs create mode 100644 src/Microsoft.DotNet.Tools.Pack/NuGet/PhysicalPackageFile.cs create mode 100644 src/Microsoft.DotNet.Tools.Pack/Program.cs create mode 100644 src/Microsoft.DotNet.Tools.Pack/project.json diff --git a/src/Microsoft.DotNet.ProjectModel/Compilation/LibraryExporter.cs b/src/Microsoft.DotNet.ProjectModel/Compilation/LibraryExporter.cs index 77d416e9f..45169454b 100644 --- a/src/Microsoft.DotNet.ProjectModel/Compilation/LibraryExporter.cs +++ b/src/Microsoft.DotNet.ProjectModel/Compilation/LibraryExporter.cs @@ -220,7 +220,7 @@ namespace Microsoft.Extensions.ProjectModel.Compilation { foreach (var assemblyPath in section) { - if (PathUtility.IsPlaceholderFile(assemblyPath)) + if (IsPlaceholderFile(assemblyPath)) { continue; } @@ -232,6 +232,11 @@ namespace Microsoft.Extensions.ProjectModel.Compilation } } + private static bool IsPlaceholderFile(string path) + { + return string.Equals(Path.GetFileName(path), "_._", StringComparison.Ordinal); + } + private static bool LibraryIsOfType(LibraryType type, LibraryDescription library) { return type.Equals(LibraryType.Unspecified) || // No type filter was requested diff --git a/src/Microsoft.DotNet.ProjectModel/Graph/LibraryDependencyType.cs b/src/Microsoft.DotNet.ProjectModel/Graph/LibraryDependencyType.cs index a4ef3d7f8..40d2cddc6 100644 --- a/src/Microsoft.DotNet.ProjectModel/Graph/LibraryDependencyType.cs +++ b/src/Microsoft.DotNet.ProjectModel/Graph/LibraryDependencyType.cs @@ -9,7 +9,7 @@ namespace Microsoft.Extensions.ProjectModel.Graph { private readonly LibraryDependencyTypeFlag _flags; - public static LibraryDependencyType Default = new LibraryDependencyType(); + public static LibraryDependencyType Default = LibraryDependencyType.Parse("default"); private LibraryDependencyType(LibraryDependencyTypeFlag flags) { diff --git a/src/Microsoft.DotNet.ProjectModel/Utilities/FrameworksExtensions.cs b/src/Microsoft.DotNet.ProjectModel/Utilities/FrameworksExtensions.cs index 691463b10..af1ebc2c5 100644 --- a/src/Microsoft.DotNet.ProjectModel/Utilities/FrameworksExtensions.cs +++ b/src/Microsoft.DotNet.ProjectModel/Utilities/FrameworksExtensions.cs @@ -1,4 +1,5 @@ using System.Linq; +using System.Runtime.Versioning; namespace NuGet.Frameworks { @@ -8,13 +9,45 @@ namespace NuGet.Frameworks public static string GetTwoDigitShortFolderName(this NuGetFramework self) { var original = self.GetShortFolderName(); - - var digits = original.SkipWhile(c => !char.IsDigit(c)).ToArray(); - if(digits.Length == 1) + var index = 0; + for (; index < original.Length; index++) { - return original + "0"; + if (char.IsDigit(original[index])) + { + break; + } } - return original; + + var versionPart = original.Substring(index); + + // Assume if the version part was preserved then leave it alone + if (versionPart.IndexOf('.') != -1) + { + return original; + } + + var name = original.Substring(0, index); + var version = self.Version.ToString(2); + + if (self.Framework.Equals(FrameworkConstants.FrameworkIdentifiers.NetPlatform)) + { + return name + version; + } + + return name + version.Replace(".", string.Empty); + } + + // NuGet.Frameworks doesn't have the equivalent of the old VersionUtility.GetFrameworkString + // which is relevant for building packages + public static string GetFrameworkString(this NuGetFramework self) + { + var frameworkName = new FrameworkName(self.DotNetFrameworkName); + string name = frameworkName.Identifier + frameworkName.Version; + if (string.IsNullOrEmpty(frameworkName.Profile)) + { + return name; + } + return name + "-" + frameworkName.Profile; } } } diff --git a/src/Microsoft.DotNet.ProjectModel/Utilities/PathUtility.cs b/src/Microsoft.DotNet.ProjectModel/Utilities/PathUtility.cs index 8b3aceeb4..2a0e36692 100644 --- a/src/Microsoft.DotNet.ProjectModel/Utilities/PathUtility.cs +++ b/src/Microsoft.DotNet.ProjectModel/Utilities/PathUtility.cs @@ -9,11 +9,6 @@ namespace Microsoft.Extensions.ProjectModel.Utilities { internal static class PathUtility { - public static bool IsPlaceholderFile(string path) - { - return string.Equals(Path.GetFileName(path), "_._", StringComparison.Ordinal); - } - public static bool IsChildOfDirectory(string dir, string candidate) { if (dir == null) diff --git a/src/Microsoft.DotNet.ProjectModel/Utilities/VersionUtility.cs b/src/Microsoft.DotNet.ProjectModel/Utilities/VersionUtility.cs index ea427e329..30aa7b57b 100644 --- a/src/Microsoft.DotNet.ProjectModel/Utilities/VersionUtility.cs +++ b/src/Microsoft.DotNet.ProjectModel/Utilities/VersionUtility.cs @@ -5,9 +5,9 @@ using NuGet.Versioning; namespace Microsoft.Extensions.ProjectModel.Utilities { - internal static class VersionUtility + public static class VersionUtility { - public static NuGetVersion GetAssemblyVersion(string path) + internal static NuGetVersion GetAssemblyVersion(string path) { return new NuGetVersion(AssemblyLoadContext.GetAssemblyName(path).Version); } diff --git a/src/Microsoft.DotNet.Tools.Compiler/Program.cs b/src/Microsoft.DotNet.Tools.Compiler/Program.cs index 01d14d4bc..94bc2f4a1 100644 --- a/src/Microsoft.DotNet.Tools.Compiler/Program.cs +++ b/src/Microsoft.DotNet.Tools.Compiler/Program.cs @@ -268,9 +268,7 @@ namespace Microsoft.DotNet.Tools.Compiler runtimeContext.CreateExporter(configuration)); } - PrintSummary(diagnostics, sw, success); - - return success; + return PrintSummary(diagnostics, sw, success); } private static string GetProjectOutput(Project project, NuGetFramework framework, string configuration, string outputPath) @@ -433,7 +431,7 @@ namespace Microsoft.DotNet.Tools.Compiler return "\"" + input.Replace("\\", "\\\\").Replace("\"", "\\\"") + "\""; } - private static void PrintSummary(List diagnostics, Stopwatch sw, bool success = true) + private static bool PrintSummary(List diagnostics, Stopwatch sw, bool success = true) { PrintDiagnostics(diagnostics); @@ -445,6 +443,7 @@ namespace Microsoft.DotNet.Tools.Compiler if (errorCount > 0 || !success) { Reporter.Output.WriteLine("Compilation failed.".Red()); + success = false; } else { @@ -458,6 +457,8 @@ namespace Microsoft.DotNet.Tools.Compiler Reporter.Output.WriteLine($"Time elapsed {sw.Elapsed}"); Reporter.Output.WriteLine(); + + return success; } private static bool AddResources(Project project, List compilerArgs, string intermediateOutputPath) diff --git a/src/Microsoft.DotNet.Tools.Pack/NuGet/Constants.cs b/src/Microsoft.DotNet.Tools.Pack/NuGet/Constants.cs new file mode 100644 index 000000000..007bed2bd --- /dev/null +++ b/src/Microsoft.DotNet.Tools.Pack/NuGet/Constants.cs @@ -0,0 +1,64 @@ +// 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. + +using System; +using System.Collections.Generic; + +namespace NuGet +{ + public static class Constants + { + /// + /// Represents the ".nupkg" extension. + /// + public static readonly string PackageExtension = ".nupkg"; + + /// + /// Represents the ".nuspec" extension. + /// + public static readonly string ManifestExtension = ".nuspec"; + + /// + /// Represents the ".nupkg.sha512" extension. + /// + public static readonly string HashFileExtension = ".nupkg.sha512"; + + /// + /// Represents the content directory in the package. + /// + public static readonly string ContentDirectory = "content"; + + /// + /// Represents the lib directory in the package. + /// + public static readonly string LibDirectory = "lib"; + + /// + /// Represents the tools directory in the package. + /// + public static readonly string ToolsDirectory = "tools"; + + /// + /// Represents the build directory in the package. + /// + public static readonly string BuildDirectory = "build"; + + public static readonly string BinDirectory = "bin"; + public static readonly string PackageReferenceFile = "packages.config"; + + public static readonly string BeginIgnoreMarker = "NUGET: BEGIN LICENSE TEXT"; + public static readonly string EndIgnoreMarker = "NUGET: END LICENSE TEXT"; + + internal const string PackageRelationshipNamespace = "http://schemas.microsoft.com/packaging/2010/07/"; + + // Starting from nuget 2.0, we use a file with the special name '_._' to represent an empty folder. + public const string PackageEmptyFileName = "_._"; + + // This is temporary until we fix the gallery to have proper first class support for this. + // The magic unpublished date is 1900-01-01T00:00:00 + public static readonly DateTimeOffset Unpublished = new DateTimeOffset(1900, 1, 1, 0, 0, 0, TimeSpan.FromHours(-8)); + + public static readonly IReadOnlyList AssemblyReferencesExtensions + = new string[] { ".dll", ".exe", ".winmd" }; + } +} \ No newline at end of file diff --git a/src/Microsoft.DotNet.Tools.Pack/NuGet/FrameworkAssemblyReference.cs b/src/Microsoft.DotNet.Tools.Pack/NuGet/FrameworkAssemblyReference.cs new file mode 100644 index 000000000..ea614b1d6 --- /dev/null +++ b/src/Microsoft.DotNet.Tools.Pack/NuGet/FrameworkAssemblyReference.cs @@ -0,0 +1,29 @@ +using System; +using System.Collections.Generic; +using NuGet.Frameworks; + +namespace NuGet +{ + public class FrameworkAssemblyReference + { + public FrameworkAssemblyReference(string assemblyName, IEnumerable supportedFrameworks) + { + if (string.IsNullOrEmpty(assemblyName)) + { + throw new ArgumentException(nameof(assemblyName)); + } + + if (supportedFrameworks == null) + { + throw new ArgumentNullException(nameof(supportedFrameworks)); + } + + AssemblyName = assemblyName; + SupportedFrameworks = supportedFrameworks; + } + + public string AssemblyName { get; private set; } + + public IEnumerable SupportedFrameworks { get; private set; } + } +} \ No newline at end of file diff --git a/src/Microsoft.DotNet.Tools.Pack/NuGet/IPackageFile.cs b/src/Microsoft.DotNet.Tools.Pack/NuGet/IPackageFile.cs new file mode 100644 index 000000000..7282ef66f --- /dev/null +++ b/src/Microsoft.DotNet.Tools.Pack/NuGet/IPackageFile.cs @@ -0,0 +1,20 @@ +// 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. + +using System.IO; + +namespace NuGet +{ + public interface IPackageFile + { + /// + /// Gets the full path of the file inside the package. + /// + string Path + { + get; + } + + Stream GetStream(); + } +} \ No newline at end of file diff --git a/src/Microsoft.DotNet.Tools.Pack/NuGet/Manifest.cs b/src/Microsoft.DotNet.Tools.Pack/NuGet/Manifest.cs new file mode 100644 index 000000000..182804cb3 --- /dev/null +++ b/src/Microsoft.DotNet.Tools.Pack/NuGet/Manifest.cs @@ -0,0 +1,87 @@ +// 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. + +using System; +using System.IO; +using System.Linq; +using System.Xml.Linq; + +namespace NuGet +{ + public class Manifest + { + private const string SchemaVersionAttributeName = "schemaVersion"; + + public Manifest(ManifestMetadata metadata) + { + if (metadata == null) + { + throw new ArgumentNullException(nameof(metadata)); + } + + Metadata = metadata; + } + + public ManifestMetadata Metadata { get; } + + /// + /// Saves the current manifest to the specified stream. + /// + /// The target stream. + public void Save(Stream stream) + { + Save(stream, validate: true, minimumManifestVersion: 1); + } + + /// + /// Saves the current manifest to the specified stream. + /// + /// The target stream. + /// The minimum manifest version that this class must use when saving. + public void Save(Stream stream, int minimumManifestVersion) + { + Save(stream, validate: true, minimumManifestVersion: minimumManifestVersion); + } + + public void Save(Stream stream, bool validate) + { + Save(stream, validate, minimumManifestVersion: 1); + } + + public void Save(Stream stream, bool validate, int minimumManifestVersion) + { + int version = Math.Max(minimumManifestVersion, ManifestVersionUtility.GetManifestVersion(Metadata)); + var schemaNamespace = (XNamespace)ManifestSchemaUtility.GetSchemaNamespace(version); + + new XDocument( + new XElement(schemaNamespace + "package", + Metadata.ToXElement(schemaNamespace))).Save(stream); + } + + public static Manifest Create(PackageBuilder copy) + { + var metadata = new ManifestMetadata(); + metadata.Id = copy.Id?.Trim(); + metadata.Version = copy.Version; + metadata.Title = copy.Title?.Trim(); + metadata.Authors = copy.Authors.Distinct(); + metadata.Owners = copy.Owners.Distinct(); + metadata.Tags = string.Join(",", copy.Tags).Trim(); + metadata.LicenseUrl = copy.LicenseUrl; + metadata.ProjectUrl = copy.ProjectUrl; + metadata.IconUrl = copy.IconUrl; + metadata.RequireLicenseAcceptance = copy.RequireLicenseAcceptance; + metadata.Description = copy.Description?.Trim(); + metadata.Copyright = copy.Copyright?.Trim(); + metadata.Summary = copy.Summary?.Trim(); + metadata.ReleaseNotes = copy.ReleaseNotes?.Trim(); + metadata.Language = copy.Language?.Trim(); + metadata.DependencySets = copy.DependencySets; + metadata.FrameworkAssemblies = copy.FrameworkAssemblies; + metadata.PackageAssemblyReferences = copy.PackageAssemblyReferences; + metadata.MinClientVersionString = copy.MinClientVersion?.ToString(); + + return new Manifest(metadata); + } + } +} diff --git a/src/Microsoft.DotNet.Tools.Pack/NuGet/ManifestMetadata.cs b/src/Microsoft.DotNet.Tools.Pack/NuGet/ManifestMetadata.cs new file mode 100644 index 000000000..bf2c66577 --- /dev/null +++ b/src/Microsoft.DotNet.Tools.Pack/NuGet/ManifestMetadata.cs @@ -0,0 +1,84 @@ +// 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. + +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using NuGet.Versioning; + +namespace NuGet +{ + public class ManifestMetadata + { + private string _minClientVersionString; + private IEnumerable _authors = Enumerable.Empty(); + private IEnumerable _owners = Enumerable.Empty(); + + [ManifestVersion(5)] + public string MinClientVersionString + { + get { return _minClientVersionString; } + set + { + Version version = null; + if (!string.IsNullOrEmpty(value) && !System.Version.TryParse(value, out version)) + { + // TODO: Resources + throw new InvalidDataException("NuGetResources.Manifest_InvalidMinClientVersion"); + } + + _minClientVersionString = value; + MinClientVersion = version; + } + } + + public Version MinClientVersion { get; private set; } + + public string Id { get; set; } + + public NuGetVersion Version { get; set; } + + public string Title { get; set; } + + public IEnumerable Authors + { + get { return _authors; } + set { _authors = value ?? Enumerable.Empty(); } + } + + public IEnumerable Owners + { + get { return (_owners == null || !_owners.Any()) ? _authors : _owners; } + set { _owners = value ?? Enumerable.Empty(); } + } + + public Uri IconUrl { get; set; } + + public Uri LicenseUrl { get; set; } + + public Uri ProjectUrl { get; set; } + + public bool RequireLicenseAcceptance { get; set; } + + public string Description { get; set; } + + public string Summary { get; set; } + + [ManifestVersion(2)] + public string ReleaseNotes { get; set; } + + [ManifestVersion(2)] + public string Copyright { get; set; } + + public string Language { get; set; } + + public string Tags { get; set; } + + public IEnumerable DependencySets { get; set; } = new List(); + + public ICollection PackageAssemblyReferences { get; set; } = new List(); + + public IEnumerable FrameworkAssemblies { get; set; } = new List(); + } +} diff --git a/src/Microsoft.DotNet.Tools.Pack/NuGet/ManifestSchemaUtility.cs b/src/Microsoft.DotNet.Tools.Pack/NuGet/ManifestSchemaUtility.cs new file mode 100644 index 000000000..5663a93cc --- /dev/null +++ b/src/Microsoft.DotNet.Tools.Pack/NuGet/ManifestSchemaUtility.cs @@ -0,0 +1,77 @@ +// 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. + +using System; +using System.Globalization; +using System.Linq; + +namespace NuGet +{ + internal static class ManifestSchemaUtility + { + /// + /// Baseline schema + /// + internal const string SchemaVersionV1 = "http://schemas.microsoft.com/packaging/2010/07/nuspec.xsd"; + + /// + /// Added copyrights, references and release notes + /// + internal const string SchemaVersionV2 = "http://schemas.microsoft.com/packaging/2011/08/nuspec.xsd"; + + /// + /// Used if the version is a semantic version. + /// + internal const string SchemaVersionV3 = "http://schemas.microsoft.com/packaging/2011/10/nuspec.xsd"; + + /// + /// Added 'targetFramework' attribute for 'dependency' elements. + /// Allow framework folders under 'content' and 'tools' folders. + /// + internal const string SchemaVersionV4 = "http://schemas.microsoft.com/packaging/2012/06/nuspec.xsd"; + + /// + /// Added 'targetFramework' attribute for 'references' elements. + /// Added 'minClientVersion' attribute + /// + internal const string SchemaVersionV5 = "http://schemas.microsoft.com/packaging/2013/01/nuspec.xsd"; + + /// + /// Allows XDT transformation + /// + internal const string SchemaVersionV6 = "http://schemas.microsoft.com/packaging/2013/05/nuspec.xsd"; + + private static readonly string[] VersionToSchemaMappings = new[] { + SchemaVersionV1, + SchemaVersionV2, + SchemaVersionV3, + SchemaVersionV4, + SchemaVersionV5, + SchemaVersionV6 + }; + + public static int GetVersionFromNamespace(string @namespace) + { + int index = Math.Max(0, Array.IndexOf(VersionToSchemaMappings, @namespace)); + + // we count version from 1 instead of 0 + return index + 1; + } + + public static string GetSchemaNamespace(int version) + { + // Versions are internally 0-indexed but stored with a 1 index so decrement it by 1 + if (version <= 0 || version > VersionToSchemaMappings.Length) + { + // TODO: Resources + throw new InvalidOperationException(String.Format(CultureInfo.CurrentCulture, "NuGetResources.UnknownSchemaVersion", version)); + } + return VersionToSchemaMappings[version - 1]; + } + + public static bool IsKnownSchema(string schemaNamespace) + { + return VersionToSchemaMappings.Contains(schemaNamespace, StringComparer.OrdinalIgnoreCase); + } + } +} diff --git a/src/Microsoft.DotNet.Tools.Pack/NuGet/ManifestVersionAttribute.cs b/src/Microsoft.DotNet.Tools.Pack/NuGet/ManifestVersionAttribute.cs new file mode 100644 index 000000000..421033527 --- /dev/null +++ b/src/Microsoft.DotNet.Tools.Pack/NuGet/ManifestVersionAttribute.cs @@ -0,0 +1,18 @@ +// 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. + +using System; + +namespace NuGet +{ + [AttributeUsage(AttributeTargets.Property, AllowMultiple = false)] + internal sealed class ManifestVersionAttribute : Attribute + { + public ManifestVersionAttribute(int version) + { + Version = version; + } + + public int Version { get; private set; } + } +} diff --git a/src/Microsoft.DotNet.Tools.Pack/NuGet/ManifestVersionUtility.cs b/src/Microsoft.DotNet.Tools.Pack/NuGet/ManifestVersionUtility.cs new file mode 100644 index 000000000..eb37e78f7 --- /dev/null +++ b/src/Microsoft.DotNet.Tools.Pack/NuGet/ManifestVersionUtility.cs @@ -0,0 +1,111 @@ +// 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. + +using System; +using System.Collections; +using System.Linq; +using System.Reflection; + +namespace NuGet +{ + internal static class ManifestVersionUtility + { + public const int DefaultVersion = 1; + public const int SemverVersion = 3; + public const int TargetFrameworkSupportForDependencyContentsAndToolsVersion = 4; + public const int TargetFrameworkSupportForReferencesVersion = 5; + public const int XdtTransformationVersion = 6; + + public static int GetManifestVersion(ManifestMetadata metadata) + { + return Math.Max(GetVersionFromObject(metadata), GetMaxVersionFromMetadata(metadata)); + } + + private static int GetMaxVersionFromMetadata(ManifestMetadata metadata) + { + // Important: check for version 5 before version 4 + bool referencesHasTargetFramework = + metadata.PackageAssemblyReferences != null && + metadata.PackageAssemblyReferences.Any(r => r.TargetFramework != null); + + if (referencesHasTargetFramework) + { + return TargetFrameworkSupportForReferencesVersion; + } + + bool dependencyHasTargetFramework = + metadata.DependencySets != null && + metadata.DependencySets.Any(d => d.TargetFramework != null); + + if (dependencyHasTargetFramework) + { + return TargetFrameworkSupportForDependencyContentsAndToolsVersion; + } + + if (metadata.Version.IsPrerelease) + { + return SemverVersion; + } + + return DefaultVersion; + } + + private static int GetVersionFromObject(object obj) + { + // all public, gettable, non-static properties + return obj?.GetType() + .GetRuntimeProperties() + .Where(prop => prop.GetMethod != null && prop.GetMethod.IsPublic && !prop.GetMethod.IsStatic) + .Select(prop => GetVersionFromPropertyInfo(obj, prop)) + .Max() + ?? DefaultVersion; + } + + private static int GetVersionFromPropertyInfo(object obj, PropertyInfo property) + { + var value = property.GetValue(obj, index: null); + if (value == null) + { + return DefaultVersion; + } + + int? version = GetPropertyVersion(property); + if (!version.HasValue) + { + return DefaultVersion; + } + + var stringValue = value as string; + if (stringValue != null) + { + if (!string.IsNullOrEmpty(stringValue)) + { + return version.Value; + } + + return DefaultVersion; + } + + // For all other object types a null check would suffice. + return version.Value; + } + + private static int VisitList(IEnumerable list) + { + int version = DefaultVersion; + + foreach (var item in list) + { + version = Math.Max(version, GetVersionFromObject(item)); + } + + return version; + } + + private static int? GetPropertyVersion(PropertyInfo property) + { + var attribute = property.GetCustomAttribute(); + return attribute?.Version; + } + } +} diff --git a/src/Microsoft.DotNet.Tools.Pack/NuGet/PackageBuilder.cs b/src/Microsoft.DotNet.Tools.Pack/NuGet/PackageBuilder.cs new file mode 100644 index 000000000..22db7299a --- /dev/null +++ b/src/Microsoft.DotNet.Tools.Pack/NuGet/PackageBuilder.cs @@ -0,0 +1,382 @@ +// 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. + +using System; +using System.Collections.Generic; +using System.Globalization; +using System.IO; +using System.IO.Compression; +using System.Linq; +using System.Reflection; +using System.Runtime.InteropServices; +using NuGet.Packaging; +using NuGet.Packaging.Core; +using NuGet.Versioning; + +namespace NuGet +{ + public class PackageBuilder + { + private const string DefaultContentType = "application/octet"; + internal const string ManifestRelationType = "manifest"; + + public PackageBuilder() + { + Files = new List(); + DependencySets = new List(); + FrameworkAssemblies = new List(); + PackageAssemblyReferences = new List(); + Authors = new List(); + Owners = new List(); + Tags = new List(); + } + + public string Id + { + get; + set; + } + + public NuGetVersion Version + { + get; + set; + } + + public string Title + { + get; + set; + } + + public List Authors + { + get; + private set; + } + + public List Owners + { + get; + private set; + } + + public Uri IconUrl + { + get; + set; + } + + public Uri LicenseUrl + { + get; + set; + } + + public Uri ProjectUrl + { + get; + set; + } + + public bool RequireLicenseAcceptance + { + get; + set; + } + + public bool DevelopmentDependency + { + get; + set; + } + + public string Description + { + get; + set; + } + + public string Summary + { + get; + set; + } + + public string ReleaseNotes + { + get; + set; + } + + public string Language + { + get; + set; + } + + public List Tags + { + get; + private set; + } + + public string Copyright + { + get; + set; + } + + public List DependencySets + { + get; + private set; + } + + public List Files + { + get; + private set; + } + + public List FrameworkAssemblies + { + get; + private set; + } + + public List PackageAssemblyReferences + { + get; + private set; + } + + public Version MinClientVersion + { + get; + set; + } + + public void Save(Stream stream) + { + // Make sure we're saving a valid package id + PackageIdValidator.ValidatePackageId(Id); + + // Throw if the package doesn't contain any dependencies nor content + if (!Files.Any() && !DependencySets.SelectMany(d => d.Dependencies).Any() && !FrameworkAssemblies.Any()) + { + // TODO: Resources + throw new InvalidOperationException("NuGetResources.CannotCreateEmptyPackage"); + } + + if (!ValidateSpecialVersionLength(Version)) + { + // TODO: Resources + throw new InvalidOperationException("NuGetResources.SemVerSpecialVersionTooLong"); + } + + ValidateDependencySets(Version, DependencySets); + ValidateReferenceAssemblies(Files, PackageAssemblyReferences); + + using (var package = new ZipArchive(stream, ZipArchiveMode.Create)) + { + // Validate and write the manifest + WriteManifest(package, ManifestVersionUtility.DefaultVersion); + + // Write the files to the package + var extensions = WriteFiles(package); + + extensions.Add("nuspec"); + + WriteOpcContentTypes(package, extensions); + } + } + + private static string CreatorInfo() + { + var creatorInfo = new List(); + var assembly = typeof(PackageBuilder).GetTypeInfo().Assembly; + creatorInfo.Add(assembly.FullName); + + if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) + { + creatorInfo.Add("Linux"); + } + else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) + { + creatorInfo.Add("OSX"); + } + else if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + creatorInfo.Add("Windows"); + } + + var attribute = assembly.GetCustomAttributes().FirstOrDefault(); + if (attribute != null) + { + creatorInfo.Add(attribute.FrameworkDisplayName); + } + + return String.Join(";", creatorInfo); + } + + internal static void ValidateDependencySets(SemanticVersion version, IEnumerable dependencies) + { + if (version == null) + { + // We have independent validation for null-versions. + return; + } + + foreach (var dep in dependencies.SelectMany(s => s.Dependencies)) + { + PackageIdValidator.ValidatePackageId(dep.Id); + } + + // REVIEW: Do we want to keep enfocing this? + /*if (version.IsPrerelease) + { + // If we are creating a production package, do not allow any of the dependencies to be a prerelease version. + var prereleaseDependency = dependencies.SelectMany(set => set.Dependencies).FirstOrDefault(IsPrereleaseDependency); + if (prereleaseDependency != null) + { + throw new InvalidDataException(String.Format(CultureInfo.CurrentCulture, "NuGetResources.Manifest_InvalidPrereleaseDependency", prereleaseDependency.ToString())); + } + }*/ + } + + internal static void ValidateReferenceAssemblies(IEnumerable files, IEnumerable packageAssemblyReferences) + { + var libFiles = new HashSet(from file in files + where !string.IsNullOrEmpty(file.Path) && file.Path.StartsWith("lib", StringComparison.OrdinalIgnoreCase) + select Path.GetFileName(file.Path), StringComparer.OrdinalIgnoreCase); + + foreach (var reference in packageAssemblyReferences.SelectMany(p => p.References)) + { + if (!libFiles.Contains(reference) && + !libFiles.Contains(reference + ".dll") && + !libFiles.Contains(reference + ".exe") && + !libFiles.Contains(reference + ".winmd")) + { + // TODO: Resources + throw new InvalidDataException(String.Format(CultureInfo.CurrentCulture, "NuGetResources.Manifest_InvalidReference", reference)); + } + } + } + + private void WriteManifest(ZipArchive package, int minimumManifestVersion) + { + string path = Id + Constants.ManifestExtension; + + WriteOpcManifestRelationship(package, path); + + ZipArchiveEntry entry = package.CreateEntry(path, CompressionLevel.Optimal); + + using (Stream stream = entry.Open()) + { + Manifest manifest = Manifest.Create(this); + manifest.Save(stream, minimumManifestVersion); + } + } + + private HashSet WriteFiles(ZipArchive package) + { + var extensions = new HashSet(StringComparer.OrdinalIgnoreCase); + + // Add files that might not come from expanding files on disk + foreach (var file in Files.Distinct()) + { + using (Stream stream = file.GetStream()) + { + try + { + CreatePart(package, file.Path, stream); + + var fileExtension = Path.GetExtension(file.Path); + + // We have files without extension (e.g. the executables for Nix) + if (!string.IsNullOrEmpty(fileExtension)) + { + extensions.Add(fileExtension.Substring(1)); + } + } + catch + { + throw; + } + } + } + + return extensions; + } + + private static void CreatePart(ZipArchive package, string path, Stream sourceStream) + { + if (PackageHelper.IsManifest(path)) + { + return; + } + + var entry = package.CreateEntry(PathUtility.GetPathWithForwardSlashes(path), CompressionLevel.Optimal); + using (var stream = entry.Open()) + { + sourceStream.CopyTo(stream); + } + } + + private static bool IsPrereleaseDependency(PackageDependency dependency) + { + return dependency.VersionRange.MinVersion?.IsPrerelease == true || + dependency.VersionRange.MaxVersion?.IsPrerelease == true; + } + + private static bool ValidateSpecialVersionLength(SemanticVersion version) + { + if (!version.IsPrerelease) + { + return true; + } + + return version == null || version.Release.Length <= 20; + } + + private void WriteOpcManifestRelationship(ZipArchive package, string path) + { + ZipArchiveEntry relsEntry = package.CreateEntry("_rels/.rels", CompressionLevel.Optimal); + + using (var writer = new StreamWriter(relsEntry.Open())) + { + writer.Write(String.Format(@" + + +", path, GenerateRelationshipId())); + writer.Flush(); + } + } + + private static void WriteOpcContentTypes(ZipArchive package, HashSet extensions) + { + // OPC backwards compatibility + ZipArchiveEntry relsEntry = package.CreateEntry("[Content_Types].xml", CompressionLevel.Optimal); + + using (var writer = new StreamWriter(relsEntry.Open())) + { + writer.Write(@" + + "); + foreach (var extension in extensions) + { + writer.Write(@""); + } + writer.Write(""); + writer.Flush(); + } + } + + // Generate a relationship id for compatibility + private string GenerateRelationshipId() + { + return "R" + Guid.NewGuid().ToString("N").Substring(0, 16); + } + } +} diff --git a/src/Microsoft.DotNet.Tools.Pack/NuGet/PackageDependencySet.cs b/src/Microsoft.DotNet.Tools.Pack/NuGet/PackageDependencySet.cs new file mode 100644 index 000000000..1788b8645 --- /dev/null +++ b/src/Microsoft.DotNet.Tools.Pack/NuGet/PackageDependencySet.cs @@ -0,0 +1,39 @@ +// 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. + +using System; +using System.Collections.Generic; +using System.Linq; +using NuGet.Frameworks; +using NuGet.Packaging.Core; + +namespace NuGet +{ + public class PackageDependencySet + { + public PackageDependencySet(IEnumerable dependencies) + : this((NuGetFramework)null, dependencies) + { + } + + public PackageDependencySet(string targetFramework, IEnumerable dependencies) + : this(targetFramework != null ? NuGetFramework.Parse(targetFramework) : null, dependencies) + { + } + + public PackageDependencySet(NuGetFramework targetFramework, IEnumerable dependencies) + { + if (dependencies == null) + { + throw new ArgumentNullException(nameof(dependencies)); + } + + TargetFramework = targetFramework; + Dependencies = dependencies.ToArray(); + } + + public NuGetFramework TargetFramework { get; } + + public IReadOnlyList Dependencies { get; } + } +} \ No newline at end of file diff --git a/src/Microsoft.DotNet.Tools.Pack/NuGet/PackageIdValidator.cs b/src/Microsoft.DotNet.Tools.Pack/NuGet/PackageIdValidator.cs new file mode 100644 index 000000000..61ac42bad --- /dev/null +++ b/src/Microsoft.DotNet.Tools.Pack/NuGet/PackageIdValidator.cs @@ -0,0 +1,72 @@ +// 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. + +using System; +using System.Globalization; + +namespace NuGet +{ + public static class PackageIdValidator + { + internal const int MaxPackageIdLength = 100; + + public static bool IsValidPackageId(string packageId) + { + if (string.IsNullOrWhiteSpace(packageId)) + { + throw new ArgumentException(nameof(packageId)); + } + + // Rules: + // Should start with a character + // Can be followed by '.' or '-'. Cannot have 2 of these special characters consecutively. + // Cannot end with '-' or '.' + + var firstChar = packageId[0]; + if (!char.IsLetterOrDigit(firstChar) && firstChar != '_') + { + // Should start with a char/digit/_. + return false; + } + + var lastChar = packageId[packageId.Length - 1]; + if (lastChar == '-' || lastChar == '.') + { + // Should not end with a '-' or '.'. + return false; + } + + for (int index = 1; index < packageId.Length - 1; index++) + { + var ch = packageId[index]; + if (!char.IsLetterOrDigit(ch) && ch != '-' && ch != '.') + { + return false; + } + + if ((ch == '-' || ch == '.') && ch == packageId[index - 1]) + { + // Cannot have two successive '-' or '.' in the name. + return false; + } + } + + return true; + } + + public static void ValidatePackageId(string packageId) + { + if (packageId.Length > MaxPackageIdLength) + { + // TODO: Resources + throw new ArgumentException("NuGetResources.Manifest_IdMaxLengthExceeded"); + } + + if (!IsValidPackageId(packageId)) + { + // TODO: Resources + throw new ArgumentException(string.Format(CultureInfo.CurrentCulture, "NuGetResources.InvalidPackageId", packageId)); + } + } + } +} \ No newline at end of file diff --git a/src/Microsoft.DotNet.Tools.Pack/NuGet/PackageMetadataXmlExtensions.cs b/src/Microsoft.DotNet.Tools.Pack/NuGet/PackageMetadataXmlExtensions.cs new file mode 100644 index 000000000..5b1425ea7 --- /dev/null +++ b/src/Microsoft.DotNet.Tools.Pack/NuGet/PackageMetadataXmlExtensions.cs @@ -0,0 +1,182 @@ +// 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. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Xml.Linq; +using NuGet.Frameworks; +using NuGet.Packaging.Core; + +namespace NuGet +{ + internal static class PackageMetadataXmlExtensions + { + private const string References = "references"; + private const string Reference = "reference"; + private const string Group = "group"; + private const string File = "file"; + private const string TargetFramework = "targetFramework"; + private const string FrameworkAssemblies = "frameworkAssemblies"; + private const string FrameworkAssembly = "frameworkAssembly"; + private const string AssemblyName = "assemblyName"; + private const string Dependencies = "dependencies"; + + public static XElement ToXElement(this ManifestMetadata metadata, XNamespace ns) + { + var elem = new XElement(ns + "metadata"); + if (metadata.MinClientVersionString != null) + { + elem.SetAttributeValue("minClientVersion", metadata.MinClientVersionString); + } + + elem.Add(new XElement(ns + "id", metadata.Id)); + elem.Add(new XElement(ns + "version", metadata.Version.ToString())); + AddElementIfNotNull(elem, ns, "title", metadata.Title); + elem.Add(new XElement(ns + "requireLicenseAcceptance", metadata.RequireLicenseAcceptance)); + AddElementIfNotNull(elem, ns, "authors", metadata.Authors, authors => string.Join(",", authors)); + AddElementIfNotNull(elem, ns, "owners", metadata.Owners, owners => string.Join(",", owners)); + AddElementIfNotNull(elem, ns, "licenseUrl", metadata.LicenseUrl); + AddElementIfNotNull(elem, ns, "projectUrl", metadata.ProjectUrl); + AddElementIfNotNull(elem, ns, "iconUrl", metadata.IconUrl); + AddElementIfNotNull(elem, ns, "description", metadata.Description); + AddElementIfNotNull(elem, ns, "summary", metadata.Summary); + AddElementIfNotNull(elem, ns, "releaseNotes", metadata.ReleaseNotes); + AddElementIfNotNull(elem, ns, "copyright", metadata.Copyright); + AddElementIfNotNull(elem, ns, "language", metadata.Language); + AddElementIfNotNull(elem, ns, "tags", metadata.Tags); + + elem.Add(GetXElementFromGroupableItemSets( + ns, + metadata.DependencySets, + set => set.TargetFramework != null, + set => set.TargetFramework.GetFrameworkString(), + set => set.Dependencies, + GetXElementFromPackageDependency, + Dependencies, + TargetFramework)); + + elem.Add(GetXElementFromGroupableItemSets( + ns, + metadata.PackageAssemblyReferences, + set => set.TargetFramework != null, + set => set.TargetFramework.GetFrameworkString(), + set => set.References, + GetXElementFromPackageReference, + References, + TargetFramework)); + + elem.Add(GetXElementFromFrameworkAssemblies(ns, metadata.FrameworkAssemblies)); + + return elem; + } + + private static XElement GetXElementFromGroupableItemSets( + XNamespace ns, + IEnumerable objectSets, + Func isGroupable, + Func getGroupIdentifer, + Func> getItems, + Func getXElementFromItem, + string parentName, + string identiferAttributeName) + { + if (objectSets == null || !objectSets.Any()) + { + return null; + } + + var groupableSets = new List(); + var ungroupableSets = new List(); + + foreach (var set in objectSets) + { + if (isGroupable(set)) + { + groupableSets.Add(set); + } + else + { + ungroupableSets.Add(set); + } + } + + var childElements = new List(); + if (!groupableSets.Any()) + { + // none of the item sets are groupable, then flatten the items + childElements.AddRange(objectSets.SelectMany(getItems).Select(item => getXElementFromItem(ns, item))); + } + else + { + // move the group with null target framework (if any) to the front just for nicer display in UI + foreach (var set in ungroupableSets.Concat(groupableSets)) + { + var groupElem = new XElement( + ns + Group, + getItems(set).Select(item => getXElementFromItem(ns, item)).ToArray()); + + if (isGroupable(set)) + { + groupElem.SetAttributeValue(identiferAttributeName, getGroupIdentifer(set)); + } + + childElements.Add(groupElem); + } + } + + return new XElement(ns + parentName, childElements.ToArray()); + } + + private static XElement GetXElementFromPackageReference(XNamespace ns, string reference) + { + return new XElement(ns + Reference, new XAttribute(File, reference)); + } + + private static XElement GetXElementFromPackageDependency(XNamespace ns, PackageDependency dependency) + { + return new XElement(ns + "dependency", + new XAttribute("id", dependency.Id), + dependency.VersionRange != null ? new XAttribute("version", dependency.VersionRange.ToString()) : null); + } + + private static XElement GetXElementFromFrameworkAssemblies(XNamespace ns, IEnumerable references) + { + if (references == null || !references.Any()) + { + return null; + } + + return new XElement( + ns + FrameworkAssemblies, + references.Select(reference => + new XElement(ns + FrameworkAssembly, + new XAttribute(AssemblyName, reference.AssemblyName), + reference.SupportedFrameworks != null && reference.SupportedFrameworks.Any() ? + new XAttribute("targetFramework", string.Join(", ", reference.SupportedFrameworks.Select(f => f.GetFrameworkString()))) : + null))); + } + + private static void AddElementIfNotNull(XElement parent, XNamespace ns, string name, T value) + where T : class + { + if (value != null) + { + parent.Add(new XElement(ns + name, value)); + } + } + + private static void AddElementIfNotNull(XElement parent, XNamespace ns, string name, T value, Func process) + where T : class + { + if (value != null) + { + var processed = process(value); + if (processed != null) + { + parent.Add(new XElement(ns + name, processed)); + } + } + } + } +} diff --git a/src/Microsoft.DotNet.Tools.Pack/NuGet/PackageReferenceSet.cs b/src/Microsoft.DotNet.Tools.Pack/NuGet/PackageReferenceSet.cs new file mode 100644 index 000000000..e2bf15467 --- /dev/null +++ b/src/Microsoft.DotNet.Tools.Pack/NuGet/PackageReferenceSet.cs @@ -0,0 +1,34 @@ +// 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. + +using System; +using System.Collections.Generic; +using System.Linq; +using NuGet.Frameworks; + +namespace NuGet +{ + public class PackageReferenceSet + { + public PackageReferenceSet(IEnumerable references) + : this(null, references) + { + } + + public PackageReferenceSet(NuGetFramework targetFramework, IEnumerable references) + { + if (references == null) + { + throw new ArgumentNullException(nameof(references)); + } + + TargetFramework = targetFramework; + References = references.ToArray(); + } + + public IReadOnlyCollection References { get; } + + public NuGetFramework TargetFramework { get; } + + } +} diff --git a/src/Microsoft.DotNet.Tools.Pack/NuGet/PathUtility.cs b/src/Microsoft.DotNet.Tools.Pack/NuGet/PathUtility.cs new file mode 100644 index 000000000..4b55360a2 --- /dev/null +++ b/src/Microsoft.DotNet.Tools.Pack/NuGet/PathUtility.cs @@ -0,0 +1,33 @@ +// 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. + +using System; +using System.IO; + +namespace NuGet +{ + internal static class PathUtility + { + public static string GetPathWithForwardSlashes(string path) + { + return path.Replace('\\', '/'); + } + + public static string GetPathWithBackSlashes(string path) + { + return path.Replace('/', '\\'); + } + + public static string GetPathWithDirectorySeparator(string path) + { + if (Path.DirectorySeparatorChar == '/') + { + return GetPathWithForwardSlashes(path); + } + else + { + return GetPathWithBackSlashes(path); + } + } + } +} \ No newline at end of file diff --git a/src/Microsoft.DotNet.Tools.Pack/NuGet/PhysicalPackageFile.cs b/src/Microsoft.DotNet.Tools.Pack/NuGet/PhysicalPackageFile.cs new file mode 100644 index 000000000..55ccd6368 --- /dev/null +++ b/src/Microsoft.DotNet.Tools.Pack/NuGet/PhysicalPackageFile.cs @@ -0,0 +1,80 @@ +// 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. + +using System; +using System.IO; + +namespace NuGet +{ + public class PhysicalPackageFile : IPackageFile + { + private readonly Func _streamFactory; + + public PhysicalPackageFile() + { + } + + public PhysicalPackageFile(PhysicalPackageFile file) + { + SourcePath = file.SourcePath; + TargetPath = file.TargetPath; + } + + internal PhysicalPackageFile(Func streamFactory) + { + _streamFactory = streamFactory; + } + + /// + /// Path on disk + /// + public string SourcePath { get; set; } + + /// + /// Path in package + /// + public string TargetPath { get; set; } + + public string Path + { + get + { + return TargetPath; + } + } + + public Stream GetStream() + { + return _streamFactory != null ? _streamFactory() : File.OpenRead(SourcePath); + } + + public override string ToString() + { + return TargetPath; + } + + public override bool Equals(object obj) + { + var file = obj as PhysicalPackageFile; + + return file != null && string.Equals(SourcePath, file.SourcePath, StringComparison.OrdinalIgnoreCase) && + string.Equals(TargetPath, file.TargetPath, StringComparison.OrdinalIgnoreCase); + } + + public override int GetHashCode() + { + int hash = 0; + if (SourcePath != null) + { + hash = SourcePath.GetHashCode(); + } + + if (TargetPath != null) + { + hash = hash * 4567 + TargetPath.GetHashCode(); + } + + return hash; + } + } +} diff --git a/src/Microsoft.DotNet.Tools.Pack/Program.cs b/src/Microsoft.DotNet.Tools.Pack/Program.cs new file mode 100644 index 000000000..d0aaf225e --- /dev/null +++ b/src/Microsoft.DotNet.Tools.Pack/Program.cs @@ -0,0 +1,475 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; + +using System.Text; +using Microsoft.Extensions.ProjectModel; +using NuGet; +using Microsoft.Dnx.Runtime.Common.CommandLine; +using Microsoft.DotNet.Cli.Utils; +using NuGet.Packaging.Core; +using Microsoft.Extensions.ProjectModel.Graph; +using NuGet.Versioning; +using NuGet.Frameworks; +using Microsoft.Extensions.ProjectModel.Files; +using Microsoft.Extensions.FileSystemGlobbing; +using Microsoft.Extensions.FileSystemGlobbing.Abstractions; +using Microsoft.Extensions.ProjectModel.Utilities; + +namespace Microsoft.DotNet.Tools.Compiler +{ + public class Program + { + public static int Main(string[] args) + { + DebugHelper.HandleDebugSwitch(ref args); + + var app = new CommandLineApplication(); + app.Name = "dotnet compile"; + app.FullName = ".NET Compiler"; + app.Description = "Compiler for the .NET Platform"; + app.HelpOption("-h|--help"); + + var output = app.Option("-o|--output ", "Directory in which to place outputs", CommandOptionType.SingleValue); + var intermediateOutput = app.Option("-t|--temp-output ", "Directory in which to place temporary outputs", CommandOptionType.SingleValue); + var configuration = app.Option("-c|--configuration ", "Configuration under which to build", CommandOptionType.SingleValue); + var project = app.Argument("", "The project to compile, defaults to the current directory. Can be a path to a project.json or a project directory"); + + app.OnExecute(() => + { + // Locate the project and get the name and full path + var path = project.Value; + if (string.IsNullOrEmpty(path)) + { + path = Directory.GetCurrentDirectory(); + } + + var configValue = configuration.Value() ?? Cli.Utils.Constants.DefaultConfiguration; + var outputValue = output.Value(); + + return BuildPackage(path, configValue, outputValue, intermediateOutput.Value()) ? 1 : 0; + }); + + try + { + return app.Execute(args); + } + catch (Exception ex) + { +#if DEBUG + Console.Error.WriteLine(ex); +#else + Console.Error.WriteLine(ex.Message); +#endif + return 1; + } + } + + private static bool BuildPackage(string path, string configuration, string outputValue, string intermediateOutputValue) + { + var contexts = ProjectContext.CreateContextForEachFramework(path); + var project = contexts.First().ProjectFile; + + if (project.Files.SourceFiles.Any()) + { + var argsBuilder = new StringBuilder(); + argsBuilder.Append($"--configuration {configuration}"); + + if (!string.IsNullOrEmpty(outputValue)) + { + argsBuilder.Append($" --output \"{outputValue}\""); + } + + if (!string.IsNullOrEmpty(intermediateOutputValue)) + { + argsBuilder.Append($" --temp-output \"{intermediateOutputValue}\""); + } + + argsBuilder.Append($" \"{path}\""); + + var result = Command.Create("dotnet-compile", argsBuilder.ToString()) + .ForwardStdOut() + .ForwardStdErr() + .Execute(); + + if (result.ExitCode != 0) + { + return false; + } + } + + Reporter.Output.WriteLine($"Producing nuget package for {project.Name}"); + + var packDiagnostics = new List(); + + // Things compiled now build the package + var 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()}"); + PopulateDependencies(context, packageBuilder); + + var outputPath = GetOutputPath(context, configuration, outputValue); + var outputName = GetProjectOutputName(context.ProjectFile, context.TargetFramework, configuration); + + TryAddOutputFile(packageBuilder, context, outputPath, outputName); + + // REVIEW: Do we keep making symbols packages? + TryAddOutputFile(packageBuilder, context, outputPath, $"{project.Name}.pdb"); + TryAddOutputFile(packageBuilder, context, outputPath, $"{project.Name}.mdb"); + + TryAddOutputFile(packageBuilder, context, outputPath, $"{project.Name}.xml"); + + Reporter.Verbose.WriteLine(""); + } + + var rootOutputPath = GetOutputPath(project, configuration, outputValue); + var packageOutputPath = GetPackagePath(project, rootOutputPath); + + if (GeneratePackage(project, packageBuilder, packageOutputPath, packDiagnostics)) + { + return true; + } + + return false; + } + + private static bool GeneratePackage(Project project, PackageBuilder packageBuilder, string nupkg, List 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); + } + + var root = project.ProjectDirectory; + + if (project.Files.PackInclude != null && project.Files.PackInclude.Any()) + { + AddPackageFiles(project, project.Files.PackInclude, packageBuilder, 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()) + { + 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 static void AddPackageFiles(Project project, IEnumerable packageFiles, PackageBuilder packageBuilder, IList 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 CollectAdditionalFiles(DirectoryInfoBase rootDirectory, IEnumerable projectFileGlobs, string projectFilePath, IList 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) + }; + } + } + } + } + + private static void TryAddOutputFile(PackageBuilder packageBuilder, + ProjectContext context, + string outputPath, + string filePath) + { + var targetPath = Path.Combine("lib", context.TargetFramework.GetTwoDigitShortFolderName(), Path.GetFileName(filePath)); + var sourcePath = Path.Combine(outputPath, filePath); + + if (!File.Exists(sourcePath)) + { + return; + } + + packageBuilder.Files.Add(new PhysicalPackageFile + { + SourcePath = sourcePath, + TargetPath = targetPath + }); + } + + public static void PopulateDependencies(ProjectContext context, PackageBuilder packageBuilder) + { + var dependencies = new List(); + var project = context.RootProject; + + foreach (var dependency in project.Dependencies) + { + if (!dependency.HasFlag(LibraryDependencyTypeFlag.BecomesNupkgDependency)) + { + 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)); + } + + private static string GetPackagePath(Project project, string outputPath, bool symbols = false) + { + string fileName = $"{project.Name}.{project.Version}{(symbols ? ".symbols" : string.Empty)}{NuGet.Constants.PackageExtension}"; + return Path.Combine(outputPath, fileName); + } + + 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; + } + + // REVIEW: This code copying kinda sucks + private static string GetProjectOutputName(Project project, NuGetFramework framework, string configuration) + { + var compilationOptions = project.GetCompilerOptions(framework, configuration); + var outputExtension = ".dll"; + + if (framework.IsDesktop() && compilationOptions.EmitEntryPoint.GetValueOrDefault()) + { + outputExtension = ".exe"; + } + + return project.Name + outputExtension; + } + + private static string GetOutputPath(Project project, string configuration, string outputOptionValue) + { + var outputPath = string.Empty; + + if (string.IsNullOrEmpty(outputOptionValue)) + { + outputPath = Path.Combine( + GetDefaultRootOutputPath(project, outputOptionValue), + Cli.Utils.Constants.BinDirectoryName, + configuration); + } + else + { + outputPath = outputOptionValue; + } + + return outputPath; + } + + private static string GetOutputPath(ProjectContext context, string configuration, string outputOptionValue) + { + var outputPath = string.Empty; + + if (string.IsNullOrEmpty(outputOptionValue)) + { + outputPath = Path.Combine( + GetDefaultRootOutputPath(context.ProjectFile, outputOptionValue), + Cli.Utils.Constants.BinDirectoryName, + configuration, + context.TargetFramework.GetTwoDigitShortFolderName()); + } + else + { + outputPath = outputOptionValue; + } + + return outputPath; + } + + private static string GetDefaultRootOutputPath(Project project, string outputOptionValue) + { + string rootOutputPath = string.Empty; + + if (string.IsNullOrEmpty(outputOptionValue)) + { + rootOutputPath = project.ProjectDirectory; + } + + return rootOutputPath; + } + } +} \ No newline at end of file diff --git a/src/Microsoft.DotNet.Tools.Pack/project.json b/src/Microsoft.DotNet.Tools.Pack/project.json new file mode 100644 index 000000000..377c01c99 --- /dev/null +++ b/src/Microsoft.DotNet.Tools.Pack/project.json @@ -0,0 +1,31 @@ +{ + "name": "dotnet-pack", + "version": "1.0.0-*", + "compilationOptions": { + "emitEntryPoint": true + }, + "dependencies": { + "Microsoft.NETCore.Runtime": "1.0.1-beta-23504", + "System.IO.Compression.ZipFile": "4.0.1-beta-23504", + "Microsoft.NETCore.Targets.DNXCore": "5.0.0-beta-23511", + + "System.Console": "4.0.0-beta-23504", + "System.Collections": "4.0.11-beta-23504", + "System.Linq": "4.0.1-beta-23504", + "System.Diagnostics.Process": "4.1.0-beta-23504", + "System.IO.FileSystem": "4.0.1-beta-23504", + + "Microsoft.DotNet.ProjectModel": "1.0.0-*", + "Microsoft.DotNet.Cli.Utils": { + "type": "build", + "version": "1.0.0-*" + }, + "Microsoft.Extensions.CommandLineUtils.Sources": { + "type": "build", + "version": "1.0.0-*" + } + }, + "frameworks": { + "dnxcore50": { } + } +}