diff --git a/src/Microsoft.DotNet.Build.Tasks.Packaging/src/GenerateRuntimeGraph.cs b/src/Microsoft.DotNet.Build.Tasks.Packaging/src/GenerateRuntimeGraph.cs index 7948abe8db1..25946410251 100644 --- a/src/Microsoft.DotNet.Build.Tasks.Packaging/src/GenerateRuntimeGraph.cs +++ b/src/Microsoft.DotNet.Build.Tasks.Packaging/src/GenerateRuntimeGraph.cs @@ -4,12 +4,10 @@ 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; -using System.Text; using System.Xml.Linq; namespace Microsoft.DotNet.Build.Tasks.Packaging @@ -54,6 +52,15 @@ public ITaskItem[] RuntimeGroups set; } + /// + /// Additional runtime identifiers to add to the graph. + /// + public string[] AdditionalRuntimeIdentifiers + { + get; + set; + } + /// /// Optional source Runtime.json to use as a starting point when merging additional RuntimeGroups /// @@ -135,7 +142,11 @@ public override bool Execute() runtimeGraph = new RuntimeGraph(); } - foreach (var runtimeGroup in RuntimeGroups.NullAsEmpty().Select(i => new RuntimeGroup(i))) + List runtimeGroups = RuntimeGroups.NullAsEmpty().Select(i => new RuntimeGroup(i)).ToList(); + + AddRuntimeIdentifiers(runtimeGroups, AdditionalRuntimeIdentifiers.NullAsEmpty(), defaultParent: "linux"); + + foreach (var runtimeGroup in runtimeGroups) { runtimeGraph = SafeMerge(runtimeGraph, runtimeGroup); } @@ -273,307 +284,67 @@ private void ValidateImports(RuntimeGraph runtimeGraph, IDictionary runtimeGroups, IEnumerable runtimeIdentifiers, string defaultParent) { - private const string rootRID = "any"; - private const char VersionDelimiter = '.'; - private const char ArchitectureDelimiter = '-'; - private const char QualifierDelimiter = '-'; + var runtimeGroupsByBaseRID = runtimeGroups.GroupBy(rg => rg.BaseRID).ToDictionary(g => g.Key, g => new List(g.AsEnumerable())); - public RuntimeGroup(ITaskItem item) + foreach(var runtimeIdentifer in runtimeIdentifiers) { - 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))); - OmitRIDDefinitions = new HashSet(item.GetStrings(nameof(OmitRIDDefinitions))); - OmitRIDReferences = new HashSet(item.GetStrings(nameof(OmitRIDReferences))); - } - - 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 ICollection OmitRIDDefinitions { get; } - public ICollection OmitRIDReferences { get; } - - private class RIDMapping - { - public RIDMapping(RID runtimeIdentifier) - { - RuntimeIdentifier = runtimeIdentifier; - Imports = Enumerable.Empty(); - } - - public RIDMapping(RID runtimeIdentifier, IEnumerable imports) - { - RuntimeIdentifier = runtimeIdentifier; - Imports = imports; - } - - public RID RuntimeIdentifier { get; } - - public IEnumerable Imports { get; } - } - - private class RID - { - public string BaseRID { get; set; } - public string VersionDelimiter { get; set; } - public string Version { get; set; } - public string ArchitectureDelimiter { get; set; } - public string Architecture { get; set; } - public string QualifierDelimiter { get; set; } - public string Qualifier { get; set; } - - public override string ToString() - { - StringBuilder builder = new StringBuilder(BaseRID); - - if (HasVersion()) - { - builder.Append(VersionDelimiter); - builder.Append(Version); - } - - if (HasArchitecture()) - { - builder.Append(ArchitectureDelimiter); - builder.Append(Architecture); - } - - if (HasQualifier()) - { - builder.Append(QualifierDelimiter); - builder.Append(Qualifier); - } - - return builder.ToString(); - } - - public bool HasVersion() - { - return Version != null; - } - - public bool HasArchitecture() - { - return Architecture != null; - } - - public bool HasQualifier() - { - return Qualifier != null; - } - } - - private RID CreateRuntime(string baseRid, string version = null, string architecture = null, string qualifier = null) - { - return new RID() - { - BaseRID = baseRid, - VersionDelimiter = OmitVersionDelimiter ? String.Empty : VersionDelimiter.ToString(), - Version = version, - ArchitectureDelimiter = ArchitectureDelimiter.ToString(), - Architecture = architecture, - QualifierDelimiter = QualifierDelimiter.ToString(), - Qualifier = qualifier - }; - } - - private IEnumerable GetRIDMappings() - { - // base => - // Parent - yield return Parent == null ? - new RIDMapping(CreateRuntime(BaseRID)) : - new RIDMapping(CreateRuntime(BaseRID), new[] { CreateRuntime(Parent) }); - - foreach (var architecture in Architectures) - { - // base + arch => - // base, - // parent + arch - var imports = new List() - { - CreateRuntime(BaseRID) - }; + RuntimeGroup runtimeGroup = null; + RID rid = RID.Parse(runtimeIdentifer); - if (!IsNullOrRoot(Parent)) - { - imports.Add(CreateRuntime(Parent, architecture: architecture)); - } - - yield return new RIDMapping(CreateRuntime(BaseRID, architecture: architecture), imports); - } - string lastVersion = null; - foreach (var version in Versions) + if (runtimeGroupsByBaseRID.TryGetValue(rid.BaseRID, out var candidateRuntimeGroups)) { - // base + version => - // base + lastVersion, - // parent + version (optionally) - var imports = new List() - { - CreateRuntime(BaseRID, version: lastVersion) - }; - - if (ApplyVersionsToParent) - { - imports.Add(CreateRuntime(Parent, version: version)); - } - - yield return new RIDMapping(CreateRuntime(BaseRID, version: version), imports); + RuntimeVersion closestVersion = null; - foreach (var architecture in Architectures) + foreach(var candidate in candidateRuntimeGroups) { - // base + version + architecture => - // base + version, - // base + lastVersion + architecture, - // parent + version + architecture (optionally) - var archImports = new List() - { - CreateRuntime(BaseRID, version: version), - CreateRuntime(BaseRID, version: lastVersion, architecture: architecture) - }; - - if (ApplyVersionsToParent) + if (rid.HasArchitecture && !candidate.Architectures.Contains(rid.Architecture)) { - archImports.Add(CreateRuntime(Parent, version: version, architecture: architecture)); + continue; } - yield return new RIDMapping(CreateRuntime(BaseRID, version: version, architecture: architecture), archImports); - } - - if (TreatVersionsAsCompatible) - { - lastVersion = version; - } - } - - foreach (var qualifier in AdditionalQualifiers) - { - // base + qual => - // base, - // parent + qual - yield return new RIDMapping(CreateRuntime(BaseRID, qualifier: qualifier), - new[] - { - CreateRuntime(BaseRID), - IsNullOrRoot(Parent) ? CreateRuntime(qualifier) : CreateRuntime(Parent, qualifier:qualifier) - }); - - foreach (var architecture in Architectures) - { - // base + arch + qualifier => - // base + qualifier, - // base + arch - // parent + arch + qualifier - var imports = new List() + if (!rid.HasVersion) { - CreateRuntime(BaseRID, qualifier: qualifier), - CreateRuntime(BaseRID, architecture: architecture) - }; - - if (!IsNullOrRoot(Parent)) - { - imports.Add(CreateRuntime(Parent, architecture: architecture, qualifier: qualifier)); + runtimeGroup = candidate; + continue; } - yield return new RIDMapping(CreateRuntime(BaseRID, architecture: architecture, qualifier: qualifier), imports); - } - - lastVersion = null; - foreach (var version in Versions) - { - // base + version + qualifier => - // base + version, - // base + lastVersion + qualifier - // parent + version + qualifier (optionally) - var imports = new List() + foreach(var version in candidate.Versions) { - CreateRuntime(BaseRID, version: version), - CreateRuntime(BaseRID, version: lastVersion, qualifier: qualifier) - }; - - if (ApplyVersionsToParent) - { - imports.Add(CreateRuntime(Parent, version: version, qualifier: qualifier)); - } - - yield return new RIDMapping(CreateRuntime(BaseRID, version: version, qualifier: qualifier), imports); - - foreach (var architecture in Architectures) - { - // base + version + architecture + qualifier => - // base + version + qualifier, - // base + version + architecture, - // base + version, - // base + lastVersion + architecture + qualifier, - // parent + version + architecture + qualifier (optionally) - var archImports = new List() - { - CreateRuntime(BaseRID, version: version, qualifier: qualifier), - CreateRuntime(BaseRID, version: version, architecture: architecture), - CreateRuntime(BaseRID, version: version), - CreateRuntime(BaseRID, version: lastVersion, architecture: architecture, qualifier: qualifier) - }; - - if (ApplyVersionsToParent) + if (closestVersion == null || + ((version <= rid.Version) && + (version > closestVersion))) { - imports.Add(CreateRuntime(Parent, version: version, architecture: architecture, qualifier: qualifier)); + closestVersion = version; + runtimeGroup = candidate; } - - yield return new RIDMapping(CreateRuntime(BaseRID, version: version, architecture: architecture, qualifier: qualifier), archImports); - } - - if (TreatVersionsAsCompatible) - { - lastVersion = version; } } - } - } - private bool IsNullOrRoot(string rid) - { - return rid == null || rid == rootRID; - } + if (runtimeGroup == null) + { + // couldn't find a close group, create a new one for just this arch/version + RuntimeGroup templateGroup = candidateRuntimeGroups.First(); + runtimeGroup = RuntimeGroup.CreateFromTemplate(templateGroup); + // add to overall list + runtimeGroups.Add(runtimeGroup); - public IEnumerable GetRuntimeDescriptions() - { - foreach (var mapping in GetRIDMappings()) - { - var rid = mapping.RuntimeIdentifier.ToString(); - - if (OmitRIDs.Contains(rid) || OmitRIDDefinitions.Contains(rid)) - { - continue; + // add to our base-RID specific list from the dictionary so that further iterations see it. + candidateRuntimeGroups.Add(runtimeGroup); } + } + else + { + runtimeGroup = new RuntimeGroup(rid.BaseRID, defaultParent); + runtimeGroups.Add(runtimeGroup); - var imports = mapping.Imports - .Select(i => i.ToString()) - .Where(i => !OmitRIDs.Contains(i) && !OmitRIDReferences.Contains(i)) - .ToArray(); - - yield return new RuntimeDescription(rid, imports); + runtimeGroupsByBaseRID.Add(rid.BaseRID, new List() { runtimeGroup }); } - } - public RuntimeGraph GetRuntimeGraph() - { - return new RuntimeGraph(GetRuntimeDescriptions()); + runtimeGroup.ApplyRid(rid); } } 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 1148b8a5ac7..42d2e34cec2 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 @@ -48,6 +48,10 @@ $(BeforePack);AddRuntimeJson + + + + - + <_candidatePackageFolder>%(_candidatPackageFolders.Identity) @@ -50,17 +51,14 @@ $(_candidatePackageFolder)\%(Identity)\%(Version) - $(_candidatePackageFolder)\$([System.String]::new("%(Identity)\%(Version)").ToLower()) + $(_candidatePackageFolder)\$([System.String]::new("%(Identity)\%(Version)").ToLower()) - - + + diff --git a/src/Microsoft.DotNet.Build.Tasks.Packaging/tests/RidTests.cs b/src/Microsoft.DotNet.Build.Tasks.Packaging/tests/RidTests.cs new file mode 100644 index 00000000000..762309bd79a --- /dev/null +++ b/src/Microsoft.DotNet.Build.Tasks.Packaging/tests/RidTests.cs @@ -0,0 +1,43 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Generic; +using Xunit; + +namespace Microsoft.DotNet.Build.Tasks.Packaging.Tests +{ + public class RidTests + { + public static IEnumerable ValidRIDData() + { + yield return new object[] { "win10-x64", new RID() { BaseRID = "win", OmitVersionDelimiter = true, Version = new RuntimeVersion("10"), Architecture = "x64" } }; + yield return new object[] { "win10", new RID() { BaseRID = "win", OmitVersionDelimiter = true, Version = new RuntimeVersion("10")} }; + yield return new object[] { "linux", new RID() { BaseRID = "linux" } }; + yield return new object[] { "linux-x64", new RID() { BaseRID = "linux", Architecture = "x64" } }; + yield return new object[] { "linux-x64", new RID() { BaseRID = "linux", Architecture = "x64" } }; + yield return new object[] { "debian.10-x64", new RID() { BaseRID = "debian", Version = new RuntimeVersion("10"), Architecture = "x64" } }; + yield return new object[] { "linuxmint.19.2-x64", new RID() { BaseRID = "linuxmint", Version = new RuntimeVersion("19.2"), Architecture = "x64" } }; + yield return new object[] { "ubuntu.14.04-x64", new RID() { BaseRID = "ubuntu", Version = new RuntimeVersion("14.04"), Architecture = "x64" } }; + yield return new object[] { "foo-bar.42-arm", new RID() { BaseRID = "foo-bar", Version = new RuntimeVersion("42"), Architecture = "arm" } }; + yield return new object[] { "foo-bar-arm", new RID() { BaseRID = "foo", Architecture = "bar", Qualifier = "arm" } }; // demonstrates ambiguity, avoid using `-` in base + } + + [Theory] + [MemberData(nameof(ValidRIDData))] + internal void ParseCorrectly(string input, RID expected) + { + RID actual = RID.Parse(input); + + Assert.Equal(expected, actual); + } + + [Theory] + [MemberData(nameof(ValidRIDData))] + internal void ToStringAsExpected(string expected, RID rid) + { + string actual = rid.ToString(); + + Assert.Equal(expected, actual); + } + } +} diff --git a/src/Microsoft.DotNet.Build.Tasks.Packaging/tests/RuntimeVersionTests.cs b/src/Microsoft.DotNet.Build.Tasks.Packaging/tests/RuntimeVersionTests.cs new file mode 100644 index 00000000000..579f14cf0f7 --- /dev/null +++ b/src/Microsoft.DotNet.Build.Tasks.Packaging/tests/RuntimeVersionTests.cs @@ -0,0 +1,233 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Generic; +using Xunit; + +namespace Microsoft.DotNet.Build.Tasks.Packaging.Tests +{ + public class RuntimeVersionTests + { + public enum Comparison + { + LessThan, + Equal, + GreaterThan + } + + public static IEnumerable ComparisonData() + { + yield return new object[] { "0.0", "00.0", Comparison.LessThan }; + yield return new object[] { "2.0", "1.0", Comparison.GreaterThan }; + yield return new object[] { "2", "1.0", Comparison.GreaterThan }; + yield return new object[] { "2", "1", Comparison.GreaterThan }; + yield return new object[] { "10", "10.0", Comparison.LessThan }; + yield return new object[] { "10", "10.00", Comparison.LessThan }; + yield return new object[] { "10.0", "10.0", Comparison.Equal }; + yield return new object[] { "10.0", null, Comparison.GreaterThan }; + yield return new object[] { "8", "8", Comparison.Equal }; + } + + [MemberData(nameof(ComparisonData))] + [Theory] + public static void CompareTo(string vs1, string vs2, Comparison expected) + { + RuntimeVersion v1 = new RuntimeVersion(vs1); + RuntimeVersion v2 = vs2 == null ? null : new RuntimeVersion(vs2); + int actual = v1.CompareTo(v2); + int invActual = v2?.CompareTo(v1) ?? -1; + + switch (expected) + { + case Comparison.LessThan: + Assert.True(actual < 0); + Assert.True(invActual > 0); + break; + case Comparison.Equal: + Assert.Equal(0, actual); + Assert.Equal(0, invActual); + break; + case Comparison.GreaterThan: + Assert.True(actual > 0); + Assert.True(invActual < 0); + break; + } + } + + [MemberData(nameof(ComparisonData))] + [Theory] + public static void GreaterThan(string vs1, string vs2, Comparison expected) + { + RuntimeVersion v1 = new RuntimeVersion(vs1); + RuntimeVersion v2 = vs2 == null ? null : new RuntimeVersion(vs2); + bool actual = v1 > v2; + bool invActual = v2 > v1; + + switch (expected) + { + case Comparison.LessThan: + Assert.False(actual); + Assert.True(invActual); + break; + case Comparison.Equal: + Assert.False(actual); + Assert.False(invActual); + break; + case Comparison.GreaterThan: + Assert.True(actual); + Assert.False(invActual); + break; + } + } + + [MemberData(nameof(ComparisonData))] + [Theory] + public static void GreaterThanOrEqual(string vs1, string vs2, Comparison expected) + { + RuntimeVersion v1 = new RuntimeVersion(vs1); + RuntimeVersion v2 = vs2 == null ? null : new RuntimeVersion(vs2); + bool actual = v1 >= v2; + bool invActual = v2 >= v1; + + switch (expected) + { + case Comparison.LessThan: + Assert.False(actual); + Assert.True(invActual); + break; + case Comparison.Equal: + Assert.True(actual); + Assert.True(invActual); + break; + case Comparison.GreaterThan: + Assert.True(actual); + Assert.False(invActual); + break; + } + } + + [MemberData(nameof(ComparisonData))] + [Theory] + public static void LessThan(string vs1, string vs2, Comparison expected) + { + RuntimeVersion v1 = new RuntimeVersion(vs1); + RuntimeVersion v2 = vs2 == null ? null : new RuntimeVersion(vs2); + bool actual = v1 < v2; + bool invActual = v2 < v1; + + switch (expected) + { + case Comparison.LessThan: + Assert.True(actual); + Assert.False(invActual); + break; + case Comparison.Equal: + Assert.False(actual); + Assert.False(invActual); + break; + case Comparison.GreaterThan: + Assert.False(actual); + Assert.True(invActual); + break; + } + } + + [MemberData(nameof(ComparisonData))] + [Theory] + public static void LessThanOrEqual(string vs1, string vs2, Comparison expected) + { + RuntimeVersion v1 = new RuntimeVersion(vs1); + RuntimeVersion v2 = vs2 == null ? null : new RuntimeVersion(vs2); + bool actual = v1 <= v2; + bool invActual = v2 <= v1; + + switch (expected) + { + case Comparison.LessThan: + Assert.True(actual); + Assert.False(invActual); + break; + case Comparison.Equal: + Assert.True(actual); + Assert.True(invActual); + break; + case Comparison.GreaterThan: + Assert.False(actual); + Assert.True(invActual); + break; + } + } + + [MemberData(nameof(ComparisonData))] + [Theory] + public static void Equal(string vs1, string vs2, Comparison expected) + { + RuntimeVersion v1 = new RuntimeVersion(vs1); + RuntimeVersion v2 = vs2 == null ? null : new RuntimeVersion(vs2); + bool actual = v1 == v2; + bool invActual = v2 == v1; + + switch (expected) + { + case Comparison.LessThan: + Assert.False(actual); + Assert.False(invActual); + break; + case Comparison.Equal: + Assert.True(actual); + Assert.True(invActual); + break; + case Comparison.GreaterThan: + Assert.False(actual); + Assert.False(invActual); + break; + } + } + + [MemberData(nameof(ComparisonData))] + [Theory] + public static void GetHashCodeUnique(string vs1, string vs2, Comparison expected) + { + RuntimeVersion v1 = new RuntimeVersion(vs1); + RuntimeVersion v2 = vs2 == null ? null : new RuntimeVersion(vs2); + int h1 = v1.GetHashCode(); + int h2 = v2?.GetHashCode() ?? 0; + + switch (expected) + { + case Comparison.LessThan: + Assert.NotEqual(h1, h2); + break; + case Comparison.Equal: + Assert.Equal(h1, h2); + break; + case Comparison.GreaterThan: + Assert.NotEqual(h1, h2); + break; + } + } + public static IEnumerable ValidVersions() + { + yield return new object[] { "0" }; + yield return new object[] { "00" }; + yield return new object[] { "000" }; + yield return new object[] { "1" }; + yield return new object[] { "1.0" }; + yield return new object[] { "1.1" }; + yield return new object[] { "1.01" }; + yield return new object[] { "1.2.3.4" }; + yield return new object[] { "1.02.03.04" }; + } + + + [MemberData(nameof(ValidVersions))] + [Theory] + public static void RoundTripToString(string expected) + { + RuntimeVersion version = new RuntimeVersion(expected); + string actual = version.ToString(); + Assert.Equal(expected, actual); + } + + } +}