diff --git a/src/Microsoft.DotNet.Build.Tasks.Packaging/src/Extensions.cs b/src/Microsoft.DotNet.Build.Tasks.Packaging/src/Extensions.cs index 2d61faa560..686e0e8b14 100644 --- a/src/Microsoft.DotNet.Build.Tasks.Packaging/src/Extensions.cs +++ b/src/Microsoft.DotNet.Build.Tasks.Packaging/src/Extensions.cs @@ -20,11 +20,20 @@ public static class Extensions { private static readonly NuGetFramework NullFramework = new NuGetFramework("Null,Version=v1.0"); + public static string GetString(this ITaskItem taskItem, string metadataName) + { + var metadataValue = taskItem.GetMetadata(metadataName)?.Trim(); + return String.IsNullOrEmpty(metadataValue) ? null : metadataValue; + } + public static bool GetBoolean(this ITaskItem taskItem, string metadataName, bool defaultValue = false) { bool result = false; var metadataValue = taskItem.GetMetadata(metadataName); - bool.TryParse(metadataValue, out result); + if (!bool.TryParse(metadataValue, out result)) + { + result = defaultValue; + } return result; } @@ -87,6 +96,17 @@ public static IReadOnlyList GetValueList(this ITaskItem taskItem, string return null; } + public static IEnumerable GetStrings(this ITaskItem taskItem, string metadataName) + { + var metadataValue = taskItem.GetMetadata(metadataName)?.Trim(); + if (!string.IsNullOrEmpty(metadataValue)) + { + return metadataValue.Split(';').Where(v => !String.IsNullOrEmpty(v.Trim())).ToArray(); + } + + return Enumerable.Empty(); + } + public static IEnumerable NullAsEmpty(this IEnumerable source) { if (source == null) diff --git a/src/Microsoft.DotNet.Build.Tasks.Packaging/src/GenerateRuntimeGraph.cs b/src/Microsoft.DotNet.Build.Tasks.Packaging/src/GenerateRuntimeGraph.cs new file mode 100644 index 0000000000..85d827c47e --- /dev/null +++ b/src/Microsoft.DotNet.Build.Tasks.Packaging/src/GenerateRuntimeGraph.cs @@ -0,0 +1,342 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using Microsoft.Build.Framework; +using Newtonsoft.Json; +using NuGet.RuntimeModel; +using NuGet.Versioning; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; + +namespace Microsoft.DotNet.Build.Tasks.Packaging +{ + public class GenerateRuntimeGraph : PackagingTask + { + + /// + /// A set of RuntimeGroups that can be used to generate a runtime graph + /// Identity: the base string for the RID, without version architecture, or qualifiers. + /// Parent: the base string for the parent of this RID. This RID will be imported by the baseRID, architecture-specific, + /// and qualifier-specific RIDs (with the latter two appending appropriate architecture and qualifiers). + /// Versions: A list of strings delimited by semi-colons that represent the versions for this RID. + /// TreatVersionsAsCompatible: Default is true. When true, version-specific RIDs will import the previous + /// version-specific RID in the Versions list, with the first version importing the version-less RID. + /// When false all version-specific RIDs will import the version-less RID (bypassing previous version-specific RIDs) + /// OmitVersionDelimiter: Default is false. When true no characters will separate the base RID and version (EG: win7). + /// When false a '.' will separate the base RID and version (EG: osx.10.12). + /// ApplyVersionsToParent: Default is false. When true, version-specific RIDs will import version-specific Parent RIDs + /// similar to is done for architecture and qualifier (see Parent above). + /// Architectures: A list of strings delimited by semi-colons that represent the architectures for this RID. + /// AdditionalQualifiers: A list of strings delimited by semi-colons that represent the additional qualifiers for this RID. + /// Additional qualifers do not stack, each only applies to the qualifier-less RIDs (so as not to cause combinatorial + /// exponential growth of RIDs). + /// OmitRIDs: A list of strings delimited by semi-colons that represent RIDs calculated from this RuntimeGroup that should + /// be omitted from the RuntimeGraph. This is useful in cases where overlapping RuntimeGroups are used and one is + /// designated to be "best" for some set of calculated RIDs, the others may omit the overlapping RIDs. + /// + [Required] + public ITaskItem[] RuntimeGroups + { + get; + set; + } + + /// + /// A set of QualifierPolicies that can be used to define the ordering of qualifer-specific RIDs in imports. + /// Identity: the qualifier name. This should match what is used in RuntimeGroups.AdditionalQualifiers + /// VersionPrecedence: default is false. When set to true, a qualifier-specific RID should import the + /// qualifier-less RID before any other RIDs that are not of the same version. When false, the qualifier- + /// less RID should be imported after all qualifier specific RIDs. + /// + /// Given the RID of foo.1.0-x64 : [ foo.1.0, foo.0.8-x64 ] + /// Applying the qualifier "bar" the following is the result for each value of VersionPrecedence: + /// True: foo.1.0-x64-bar : [ foo.1.0-bar, foo.1.0-x64, foo.0.8-x64-bar ] + /// False: foo.1.0-x64-bar : [ foo.1.0-bar, foo.0.8-x64-bar, foo.1.0-x64 ] + /// + public ITaskItem[] QualifierPolicies + { + get; + set; + } + + + /// + /// Optional source Runtime.json to use as a starting point when merging additional RuntimeGroups + /// + public ITaskItem SourceRuntimeJson + { + get; + set; + } + + /// + /// Where to write the final runtime.json + /// + [Required] + public ITaskItem RuntimeJson + { + get; + set; + } + + private Dictionary qualifierPolicies = new Dictionary(); + + public override bool Execute() + { + + if (RuntimeJson == null) + { + Log.LogError($"{nameof(RuntimeJson)} argument must be specified"); + return false; + } + + RuntimeGraph runtimeGraph; + if (SourceRuntimeJson != null) + { + var sourceRuntimeFilePath = SourceRuntimeJson.GetMetadata("FullPath"); + + if (!File.Exists(sourceRuntimeFilePath)) + { + Log.LogError($"{nameof(SourceRuntimeJson)} did not exist at {sourceRuntimeFilePath}."); + return false; + } + + runtimeGraph = JsonRuntimeFormat.ReadRuntimeGraph(sourceRuntimeFilePath); + } + else + { + runtimeGraph = new RuntimeGraph(); + } + + foreach(var qualifierPolicy in QualifierPolicies.NullAsEmpty().Select(i => new QualifierPolicy(i))) + { + qualifierPolicies[qualifierPolicy.Qualifier] = qualifierPolicy; + } + + foreach(var runtimeGroup in RuntimeGroups.NullAsEmpty().Select(i => new RuntimeGroup(i))) + { + runtimeGraph = SafeMerge(runtimeGraph, runtimeGroup); + } + + ValidateImports(runtimeGraph); + + JsonRuntimeFormat.WriteRuntimeGraph(RuntimeJson.ItemSpec, runtimeGraph); + + return !Log.HasLoggedErrors; + } + + + private RuntimeGraph SafeMerge(RuntimeGraph existingGraph, RuntimeGroup runtimeGroup) + { + var runtimeGraph = runtimeGroup.GetRuntimeGraph(qualifierPolicies); + + foreach (var existingRuntimeDescription in existingGraph.Runtimes.Values) + { + RuntimeDescription newRuntimeDescription; + + if (runtimeGraph.Runtimes.TryGetValue(existingRuntimeDescription.RuntimeIdentifier, out newRuntimeDescription)) + { + // overlapping RID, ensure that the imports match (same ordering and content) + if (!existingRuntimeDescription.InheritedRuntimes.SequenceEqual(newRuntimeDescription.InheritedRuntimes)) + { + Log.LogError($"RuntimeGroup {runtimeGroup.BaseRID} defines RID {newRuntimeDescription.RuntimeIdentifier} with imports {String.Join(";", newRuntimeDescription.InheritedRuntimes)} which differ from existing imports {String.Join(";", existingRuntimeDescription.InheritedRuntimes)}. You may avoid this by specifying OmitRIDs metadata with {newRuntimeDescription.RuntimeIdentifier}."); + } + } + } + + return RuntimeGraph.Merge(existingGraph, runtimeGraph); + } + + private void ValidateImports(RuntimeGraph runtimeGraph) + { + foreach(var runtimeDescription in runtimeGraph.Runtimes.Values) + { + foreach(var import in runtimeDescription.InheritedRuntimes) + { + if (!runtimeGraph.Runtimes.ContainsKey(import)) + { + Log.LogError($"Runtime {runtimeDescription.RuntimeIdentifier} imports {import} which is not defined."); + } + } + } + } + + class QualifierPolicy + { + public QualifierPolicy(ITaskItem item) + { + Qualifier = item.ItemSpec; + VersionPrecedence = item.GetBoolean(nameof(VersionPrecedence)); + } + + public string Qualifier { get; } + public bool VersionPrecedence { get; } + } + + class RuntimeGroup + { + private const string rootRID = "any"; + private const char VersionDelimiter = '.'; + private const char ArchitectureDelimiter = '-'; + private const char QualifierDelimiter = '-'; + + public RuntimeGroup(ITaskItem item) + { + BaseRID = item.ItemSpec; + Parent = item.GetString(nameof(Parent)); + Versions = item.GetStrings(nameof(Versions)); + TreatVersionsAsCompatible = item.GetBoolean(nameof(TreatVersionsAsCompatible), true); + OmitVersionDelimiter = item.GetBoolean(nameof(OmitVersionDelimiter)); + ApplyVersionsToParent = item.GetBoolean(nameof(ApplyVersionsToParent)); + Architectures = item.GetStrings(nameof(Architectures)); + AdditionalQualifiers = item.GetStrings(nameof(AdditionalQualifiers)); + OmitRIDs = new HashSet(item.GetStrings(nameof(OmitRIDs))); + } + + public string BaseRID { get; } + public string Parent { get; } + public IEnumerable Versions { get; } + public bool TreatVersionsAsCompatible { get; } + public bool OmitVersionDelimiter { get; } + public bool ApplyVersionsToParent { get; } + public IEnumerable Architectures { get; } + public IEnumerable AdditionalQualifiers { get; } + public ICollection OmitRIDs { get; } + + public IEnumerable GetRuntimeDescriptions() + { + // define the base as importing the parent + yield return new RuntimeDescription(BaseRID, Parent == null ? Enumerable.Empty() : new[] { Parent }); + + // define each arch as importing base and parent-arch + foreach(var architecture in Architectures) + { + var imports = new List(); + imports.Add(BaseRID); + + if (Parent != null && Parent != rootRID) + { + imports.Add($"{Parent}-{architecture}"); + } + + yield return new RuntimeDescription($"{BaseRID}-{architecture}", imports); + } + + var versionDelimiter = OmitVersionDelimiter ? String.Empty : VersionDelimiter.ToString(); + string lastVersion = null; + foreach (var version in Versions) + { + var imports = new List(); + + // define each version as importing the version-less base or previous version + if ((lastVersion == null) || !TreatVersionsAsCompatible) + { + imports.Add(BaseRID); + } + else + { + imports.Add($"{BaseRID}{versionDelimiter}{lastVersion}"); + } + + if (ApplyVersionsToParent) + { + imports.Add($"{Parent}{versionDelimiter}{version}"); + } + + yield return new RuntimeDescription($"{BaseRID}{versionDelimiter}{version}", imports); + + foreach (var architecture in Architectures) + { + // define each arch-specific version as importing the versioned base and either a previous version arch-specific RID or the base arch-specific RID + var archImports = new List(); + + archImports.Add($"{BaseRID}{versionDelimiter}{version}"); + + if ((lastVersion == null) || !TreatVersionsAsCompatible) + { + archImports.Add($"{BaseRID}{ArchitectureDelimiter}{architecture}"); + } + else + { + archImports.Add($"{BaseRID}{versionDelimiter}{lastVersion}{ArchitectureDelimiter}{architecture}"); + } + + if (ApplyVersionsToParent) + { + archImports.Add($"{Parent}{versionDelimiter}{version}{ArchitectureDelimiter}{architecture}"); + } + + + yield return new RuntimeDescription($"{BaseRID}{versionDelimiter}{version}{ArchitectureDelimiter}{architecture}", archImports); + } + + lastVersion = version; + } + } + + private IEnumerable AddQualifiers(IEnumerable unQualifiedruntimeDescriptions, Dictionary qualifierPolicies) + { + foreach(var runtimeDescription in unQualifiedruntimeDescriptions) + { + yield return runtimeDescription; + + foreach(var qualifier in AdditionalQualifiers) + { + var imports = new List(runtimeDescription.InheritedRuntimes + .NullAsEmpty() + .Select(rid => rid == null || rid == rootRID ? qualifier : $"{rid}-{qualifier}")); + + + QualifierPolicy qualifierPolicy = null; + + if (qualifierPolicies.TryGetValue(qualifier, out qualifierPolicy) && qualifierPolicy.VersionPrecedence) + { + var versionedRid = runtimeDescription.RuntimeIdentifier; + + var archIndex = versionedRid.IndexOf('-'); + + if (archIndex != -1) + { + versionedRid = versionedRid.Substring(0, archIndex); + } + + int insertAt = 0; + + + while (insertAt < imports.Count && imports[insertAt].StartsWith(versionedRid)) + { + insertAt++; + } + + imports.Insert(insertAt, runtimeDescription.RuntimeIdentifier); + } + else + { + imports.Add(runtimeDescription.RuntimeIdentifier); + } + + + yield return new RuntimeDescription($"{runtimeDescription.RuntimeIdentifier}{QualifierDelimiter}{qualifier}", imports); + + } + } + } + + public RuntimeGraph GetRuntimeGraph(Dictionary qualifierPolicies) + { + var rids = GetRuntimeDescriptions(); + + rids = rids.Where(rid => !OmitRIDs.Contains(rid.RuntimeIdentifier)); + + rids = AddQualifiers(rids, qualifierPolicies); + + rids = rids.Where(rid => !OmitRIDs.Contains(rid.RuntimeIdentifier)); + + return new RuntimeGraph(rids); + } + } + } +} diff --git a/src/Microsoft.DotNet.Build.Tasks.Packaging/src/Microsoft.DotNet.Build.Tasks.Packaging.csproj b/src/Microsoft.DotNet.Build.Tasks.Packaging/src/Microsoft.DotNet.Build.Tasks.Packaging.csproj index 30accabf6d..cf68a3f3cd 100644 --- a/src/Microsoft.DotNet.Build.Tasks.Packaging/src/Microsoft.DotNet.Build.Tasks.Packaging.csproj +++ b/src/Microsoft.DotNet.Build.Tasks.Packaging/src/Microsoft.DotNet.Build.Tasks.Packaging.csproj @@ -21,6 +21,7 @@ + diff --git a/src/Microsoft.DotNet.Build.Tasks.Packaging/src/PackageFiles/Packaging.common.targets b/src/Microsoft.DotNet.Build.Tasks.Packaging/src/PackageFiles/Packaging.common.targets index aade9e4f62..c6cf4483b9 100644 --- a/src/Microsoft.DotNet.Build.Tasks.Packaging/src/PackageFiles/Packaging.common.targets +++ b/src/Microsoft.DotNet.Build.Tasks.Packaging/src/PackageFiles/Packaging.common.targets @@ -25,6 +25,7 @@ +