diff --git a/src/libraries/Microsoft.NETCore.Platforms/src/GenerateRuntimeGraph.cs b/src/libraries/Microsoft.NETCore.Platforms/src/GenerateRuntimeGraph.cs index cbee78b71a6b06..1421200ead56ae 100644 --- a/src/libraries/Microsoft.NETCore.Platforms/src/GenerateRuntimeGraph.cs +++ b/src/libraries/Microsoft.NETCore.Platforms/src/GenerateRuntimeGraph.cs @@ -53,6 +53,24 @@ public ITaskItem[] RuntimeGroups set; } + /// + /// Additional runtime identifiers to add to the graph. + /// + public string[] AdditionalRuntimeIdentifiers + { + get; + set; + } + + /// + /// Parent RID to use for any unknown AdditionalRuntimeIdentifer. + /// + public string AdditionalRuntimeIdentifierParent + { + get; + set; + } + /// /// Optional source Runtime.json to use as a starting point when merging additional RuntimeGroups /// @@ -134,7 +152,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); + + foreach (var runtimeGroup in runtimeGroups) { runtimeGraph = SafeMerge(runtimeGraph, runtimeGroup); } @@ -291,6 +313,21 @@ private void ValidateImports(RuntimeGraph runtimeGraph, IDictionary runtimeGroups) + { + if (AdditionalRuntimeIdentifiers == null || AdditionalRuntimeIdentifiers.Length == 0) + { + return; + } + + RuntimeGroupCollection runtimeGroupCollection = new RuntimeGroupCollection(runtimeGroups); + + foreach (string additionalRuntimeIdentifier in AdditionalRuntimeIdentifiers) + { + runtimeGroupCollection.AddRuntimeIdentifier(additionalRuntimeIdentifier, AdditionalRuntimeIdentifierParent); + } + } + private static IDictionary> GetCompatibilityMap(RuntimeGraph graph) { Dictionary> compatibilityMap = new Dictionary>(); diff --git a/src/libraries/Microsoft.NETCore.Platforms/src/Microsoft.NETCore.Platforms.csproj b/src/libraries/Microsoft.NETCore.Platforms/src/Microsoft.NETCore.Platforms.csproj index 3c00187ddb18e1..6fa925cc2caeb9 100644 --- a/src/libraries/Microsoft.NETCore.Platforms/src/Microsoft.NETCore.Platforms.csproj +++ b/src/libraries/Microsoft.NETCore.Platforms/src/Microsoft.NETCore.Platforms.csproj @@ -1,4 +1,4 @@ - + $(NetCoreAppToolCurrent);net472 @@ -12,6 +12,9 @@ true $(NoWarn);NU5128 Provides runtime information required to resolve target framework, platform, and runtime specific implementations of .NETCore packages. + + + $(AdditionalRuntimeIdentifiers);$(OutputRID) @@ -24,11 +27,14 @@ + + - + + @@ -38,5 +44,14 @@ + + + + + \ No newline at end of file diff --git a/src/libraries/Microsoft.NETCore.Platforms/src/RID.cs b/src/libraries/Microsoft.NETCore.Platforms/src/RID.cs index 357790fbfdd54d..6f08d1a510e567 100644 --- a/src/libraries/Microsoft.NETCore.Platforms/src/RID.cs +++ b/src/libraries/Microsoft.NETCore.Platforms/src/RID.cs @@ -1,37 +1,44 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System; +using System.Diagnostics; using System.Text; namespace Microsoft.NETCore.Platforms.BuildTasks { - internal class RID + public class RID { + internal const char VersionDelimiter = '.'; + internal const char ArchitectureDelimiter = '-'; + internal const char QualifierDelimiter = '-'; + public string BaseRID { get; set; } - public string VersionDelimiter { get; set; } - public string Version { get; set; } - public string ArchitectureDelimiter { get; set; } + public bool OmitVersionDelimiter { get; set; } + public RuntimeVersion Version { 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()) + if (HasVersion) { - builder.Append(VersionDelimiter); + if (!OmitVersionDelimiter) + { + builder.Append(VersionDelimiter); + } builder.Append(Version); } - if (HasArchitecture()) + if (HasArchitecture) { builder.Append(ArchitectureDelimiter); builder.Append(Architecture); } - if (HasQualifier()) + if (HasQualifier) { builder.Append(QualifierDelimiter); builder.Append(Qualifier); @@ -40,20 +47,153 @@ public override string ToString() return builder.ToString(); } - public bool HasVersion() + private enum RIDPart : int + { + Base = 0, + Version, + Architecture, + Qualifier, + Max = Qualifier + } + + public static RID Parse(string runtimeIdentifier) + { + string[] parts = new string[(int)RIDPart.Max + 1]; + bool omitVersionDelimiter = true; + RIDPart parseState = RIDPart.Base; + + int partStart = 0, partLength = 0; + + // qualifier is indistinguishable from arch so we cannot distinguish it for parsing purposes + Debug.Assert(ArchitectureDelimiter == QualifierDelimiter); + + for (int i = 0; i < runtimeIdentifier.Length; i++) + { + char current = runtimeIdentifier[i]; + partLength = i - partStart; + + switch (parseState) + { + case RIDPart.Base: + // treat any number as the start of the version + if (current == VersionDelimiter || (current >= '0' && current <= '9')) + { + SetPart(); + partStart = i; + if (current == VersionDelimiter) + { + omitVersionDelimiter = false; + partStart = i + 1; + } + parseState = RIDPart.Version; + } + // version might be omitted + else if (current == ArchitectureDelimiter) + { + // ensure there's no version later in the string + if (runtimeIdentifier.IndexOf(VersionDelimiter, i) != -1) + { + break; + } + SetPart(); + partStart = i + 1; // skip delimiter + parseState = RIDPart.Architecture; + } + break; + case RIDPart.Version: + if (current == ArchitectureDelimiter) + { + SetPart(); + partStart = i + 1; // skip delimiter + parseState = RIDPart.Architecture; + } + break; + case RIDPart.Architecture: + if (current == QualifierDelimiter) + { + SetPart(); + partStart = i + 1; // skip delimiter + parseState = RIDPart.Qualifier; + } + break; + default: + break; + } + } + + partLength = runtimeIdentifier.Length - partStart; + if (partLength > 0) + { + SetPart(); + } + + string GetPart(RIDPart part) + { + return parts[(int)part]; + } + + void SetPart() + { + if (partLength == 0) + { + throw new ArgumentException($"Unexpected delimiter at position {partStart} in \"{runtimeIdentifier}\""); + } + + parts[(int)parseState] = runtimeIdentifier.Substring(partStart, partLength); + } + + string version = GetPart(RIDPart.Version); + + if (version == null) + { + omitVersionDelimiter = false; + } + + return new RID() + { + BaseRID = GetPart(RIDPart.Base), + OmitVersionDelimiter = omitVersionDelimiter, + Version = version == null ? null : new RuntimeVersion(version), + Architecture = GetPart(RIDPart.Architecture), + Qualifier = GetPart(RIDPart.Qualifier) + }; + } + + public bool HasVersion => Version != null; + + public bool HasArchitecture => Architecture != null; + + public bool HasQualifier => Qualifier != null; + + public override bool Equals(object obj) { - return Version != null; + return Equals(obj as RID); } - public bool HasArchitecture() + public bool Equals(RID obj) { - return Architecture != null; + return object.ReferenceEquals(obj, this) || + (obj is not null && + BaseRID == obj.BaseRID && + (Version == null || OmitVersionDelimiter == obj.OmitVersionDelimiter) && + Version == obj.Version && + Architecture == obj.Architecture && + Qualifier == obj.Qualifier); + } - public bool HasQualifier() + public override int GetHashCode() { - return Qualifier != null; +#if NETFRAMEWORK + return BaseRID.GetHashCode(); +#else + HashCode hashCode = default; + hashCode.Add(BaseRID); + hashCode.Add(Version); + hashCode.Add(Architecture); + hashCode.Add(Qualifier); + return hashCode.ToHashCode(); +#endif } } - } diff --git a/src/libraries/Microsoft.NETCore.Platforms/src/RuntimeGroup.cs b/src/libraries/Microsoft.NETCore.Platforms/src/RuntimeGroup.cs index 2ddd60022c426a..76fde331a1a3a8 100644 --- a/src/libraries/Microsoft.NETCore.Platforms/src/RuntimeGroup.cs +++ b/src/libraries/Microsoft.NETCore.Platforms/src/RuntimeGroup.cs @@ -1,49 +1,84 @@ // 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 System.Linq; using Microsoft.Build.Framework; using NuGet.RuntimeModel; +using System; +using System.Collections.Generic; +using System.Linq; namespace Microsoft.NETCore.Platforms.BuildTasks { - - internal class RuntimeGroup + public 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)); + Versions = new HashSet(item.GetStrings(nameof(Versions)).Select(v => new RuntimeVersion(v))); 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)); + Architectures = new HashSet(item.GetStrings(nameof(Architectures))); + AdditionalQualifiers = new HashSet(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 RuntimeGroup(string baseRID, string parent, bool treatVersionsAsCompatible = true, bool omitVersionDelimiter = false, bool applyVersionsToParent = false, IEnumerable additionalQualifiers = null) + { + BaseRID = baseRID; + Parent = parent; + Versions = new HashSet(); + TreatVersionsAsCompatible = treatVersionsAsCompatible; + OmitVersionDelimiter = omitVersionDelimiter; + ApplyVersionsToParent = applyVersionsToParent; + Architectures = new HashSet(); + AdditionalQualifiers = new HashSet(additionalQualifiers.NullAsEmpty()); + OmitRIDs = new HashSet(); + OmitRIDDefinitions = new HashSet(); + OmitRIDReferences = new HashSet(); + } + public string BaseRID { get; } public string Parent { get; } - public IEnumerable Versions { get; } + public ICollection Versions { get; } public bool TreatVersionsAsCompatible { get; } public bool OmitVersionDelimiter { get; } public bool ApplyVersionsToParent { get; } - public IEnumerable Architectures { get; } - public IEnumerable AdditionalQualifiers { get; } + public ICollection Architectures { get; } + public ICollection AdditionalQualifiers { get; } public ICollection OmitRIDs { get; } public ICollection OmitRIDDefinitions { get; } public ICollection OmitRIDReferences { get; } - private class RIDMapping + public void ApplyRid(RID rid) + { + if (!rid.BaseRID.Equals(BaseRID, StringComparison.Ordinal)) + { + throw new ArgumentException($"Cannot apply {nameof(RID)} with {nameof(RID.BaseRID)} {rid.BaseRID} to {nameof(RuntimeGroup)} with {nameof(RuntimeGroup.BaseRID)} {BaseRID}.", nameof(rid)); + } + + if (rid.HasArchitecture) + { + Architectures.Add(rid.Architecture); + } + + if (rid.HasVersion) + { + Versions.Add(rid.Version); + } + + if (rid.HasQualifier) + { + AdditionalQualifiers.Add(rid.Qualifier); + } + } + + internal class RIDMapping { public RIDMapping(RID runtimeIdentifier) { @@ -62,21 +97,20 @@ public RIDMapping(RID runtimeIdentifier, IEnumerable imports) public IEnumerable Imports { get; } } - private RID CreateRuntime(string baseRid, string version = null, string architecture = null, string qualifier = null) + + internal RID CreateRuntime(string baseRid, RuntimeVersion version = null, string architecture = null, string qualifier = null) { return new RID() { BaseRID = baseRid, - VersionDelimiter = OmitVersionDelimiter ? string.Empty : VersionDelimiter.ToString(), Version = version, - ArchitectureDelimiter = ArchitectureDelimiter.ToString(), + OmitVersionDelimiter = OmitVersionDelimiter, Architecture = architecture, - QualifierDelimiter = QualifierDelimiter.ToString(), Qualifier = qualifier }; } - private IEnumerable GetRIDMappings() + internal IEnumerable GetRIDMappings() { // base => // Parent @@ -102,7 +136,7 @@ private IEnumerable GetRIDMappings() yield return new RIDMapping(CreateRuntime(BaseRID, architecture: architecture), imports); } - string lastVersion = null; + RuntimeVersion lastVersion = null; foreach (var version in Versions) { // base + version => diff --git a/src/libraries/Microsoft.NETCore.Platforms/src/RuntimeGroupCollection.cs b/src/libraries/Microsoft.NETCore.Platforms/src/RuntimeGroupCollection.cs new file mode 100644 index 00000000000000..970da5c1f86652 --- /dev/null +++ b/src/libraries/Microsoft.NETCore.Platforms/src/RuntimeGroupCollection.cs @@ -0,0 +1,159 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Build.Framework; +using NuGet.RuntimeModel; +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; + +namespace Microsoft.NETCore.Platforms.BuildTasks +{ + public class RuntimeGroupCollection + { + private ICollection allRuntimeGroups; + private Dictionary> runtimeGroupsByBaseRID; + private HashSet knownRIDs; + + public RuntimeGroupCollection(ICollection runtimeGroups) + { + allRuntimeGroups = runtimeGroups; + runtimeGroupsByBaseRID = runtimeGroups.GroupBy(rg => rg.BaseRID).ToDictionary(g => g.Key, g => new List(g.AsEnumerable())); + + knownRIDs = new HashSet(allRuntimeGroups.SelectMany(rg => rg.GetRIDMappings()).Select(mapping => mapping.RuntimeIdentifier)); + } + + /// + /// Locate an existing RuntimeGroup to append to. + /// Existing group must have matching baseRID, then we choose based on closest version, + /// and prefer matching arch and qualifier. + /// If no match is found, then a new RID heirarchy is created. + /// + /// + /// + public void AddRuntimeIdentifier(string runtimeIdentifier, string parent) + { + RID rid = RID.Parse(runtimeIdentifier); + + AddRuntimeIdentifier(rid, parent); + } + + public void AddRuntimeIdentifier(RID rid, string parent) + { + // Do nothing if we already know about the RID + if (knownRIDs.Contains(rid)) + { + return; + } + + RuntimeGroup runtimeGroup = null; + + if (runtimeGroupsByBaseRID.TryGetValue(rid.BaseRID, out var candidateRuntimeGroups)) + { + RuntimeVersion closestVersion = null; + + foreach (var candidate in candidateRuntimeGroups) + { + if (rid.HasVersion) + { + // Find the closest previous version + foreach (var version in candidate.Versions) + { + // a previous version + if (version <= rid.Version) + { + // haven't yet found a match or this is a closer match + if (closestVersion == null || version > closestVersion) + { + closestVersion = version; + runtimeGroup = candidate; + } + else if (version == closestVersion) + { + // found a tie in version, examine other fields + considerCandidate(); + } + } + } + } + + // if we don't have a version, or if we couldn't find any match, consider other fields + if (!rid.HasVersion) + { + considerCandidate(); + } + + // if we don't have a match yet, take this as it matches on baseRID + runtimeGroup ??= candidate; + + void considerCandidate() + { + // is this a better match? + if (!rid.HasArchitecture || candidate.Architectures.Contains(rid.Architecture)) + { + if (!rid.HasQualifier || candidate.AdditionalQualifiers.Contains(rid.Qualifier)) + { + // matched on arch and qualifier. + runtimeGroup = candidate; + } + else if (rid.HasArchitecture && !runtimeGroup.Architectures.Contains(rid.Architecture)) + { + // matched only on arch and existing match doesn't match arch + runtimeGroup = candidate; + } + } + } + } + + Debug.Assert(runtimeGroup != null, "Empty candidates?"); + } + else + { + // This is an unknown base RID, we'll need to add a new group. + if (string.IsNullOrEmpty(parent)) + { + throw new InvalidOperationException($"AdditionalRuntimeIdentifier {rid} was specified, which could not be found in any existing {nameof(RuntimeGroup)}, and no {nameof(parent)} was specified."); + } + + runtimeGroup = new RuntimeGroup(rid.BaseRID, parent); + + AddRuntimeGroup(runtimeGroup); + } + + runtimeGroup.ApplyRid(rid); + + // Compute the portion of the RID graph produced from this modified RuntimeGroup + var ridMappings = runtimeGroup.GetRIDMappings(); + + // Record any newly defined RIDs in our set of known RIDs + foreach (RID definedRID in ridMappings.Select(mapping => mapping.RuntimeIdentifier)) + { + knownRIDs.Add(definedRID); + } + + // Make sure that any RID imported is added as well. This allows users to specify + // a single new RID and we'll add any new RIDs up the parent chain that might be needed. + foreach (RID importedRID in ridMappings.SelectMany(mapping => mapping.Imports)) + { + // This should not introduce any new RuntimeGroups, so we specify parent as null + AddRuntimeIdentifier(importedRID, null); + } + + } + + private void AddRuntimeGroup(RuntimeGroup runtimeGroup) + { + List baseRuntimeGroups; + + if (!runtimeGroupsByBaseRID.TryGetValue(runtimeGroup.BaseRID, out baseRuntimeGroups)) + { + runtimeGroupsByBaseRID[runtimeGroup.BaseRID] = baseRuntimeGroups = new List(); + } + + baseRuntimeGroups.Add(runtimeGroup); + allRuntimeGroups.Add(runtimeGroup); + } + + } +} diff --git a/src/libraries/Microsoft.NETCore.Platforms/src/RuntimeVersion.cs b/src/libraries/Microsoft.NETCore.Platforms/src/RuntimeVersion.cs new file mode 100644 index 00000000000000..954659fa6e0724 --- /dev/null +++ b/src/libraries/Microsoft.NETCore.Platforms/src/RuntimeVersion.cs @@ -0,0 +1,140 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; + +namespace Microsoft.NETCore.Platforms.BuildTasks +{ + + /// + /// A Version class that also supports a single integer (major only) + /// + public sealed class RuntimeVersion : IComparable, IComparable, IEquatable + { + private string versionString; + private Version version; + private bool hasMinor; + + public RuntimeVersion(string versionString) + { + // intentionally don't support the type of version that omits the separators as it is abiguous. + // for example Windows 8.1 was encoded as win81, where as Windows 10.0 was encoded as win10 + this.versionString = versionString; + string toParse = versionString; +#if NETCOREAPP + if (!toParse.Contains('.')) +#else + if (toParse.IndexOf('.') == -1) +#endif + { + toParse += ".0"; + hasMinor = false; + } + else + { + hasMinor = true; + } + version = Version.Parse(toParse); + } + + public int CompareTo(object obj) + { + if (obj == null) + { + return 1; + } + + if (obj is RuntimeVersion version) + { + return CompareTo(version); + } + + throw new ArgumentException($"Cannot compare {nameof(RuntimeVersion)} to object of type {obj.GetType()}.", nameof(obj)); + } + + public int CompareTo(RuntimeVersion other) + { + if (other == null) + { + return 1; + } + + int versionResult = version.CompareTo(other?.version); + + if (versionResult == 0) + { + if (!hasMinor && other.hasMinor) + { + return -1; + } + + if (hasMinor && !other.hasMinor) + { + return 1; + } + + return string.CompareOrdinal(versionString, other.versionString); + } + + return versionResult; + } + + public bool Equals(RuntimeVersion other) + { + return object.ReferenceEquals(other, this) || + (other != null && + versionString.Equals(other.versionString, StringComparison.Ordinal)); + } + + public override bool Equals(object obj) + { + return Equals(obj as RuntimeVersion); + } + + public override int GetHashCode() + { + return versionString.GetHashCode(); + } + + public override string ToString() + { + return versionString; + } + + public static bool operator ==(RuntimeVersion v1, RuntimeVersion v2) + { + if (v2 is null) + { + return (v1 is null) ? true : false; + } + + return ReferenceEquals(v2, v1) ? true : v2.Equals(v1); + } + + public static bool operator !=(RuntimeVersion v1, RuntimeVersion v2) => !(v1 == v2); + + public static bool operator <(RuntimeVersion v1, RuntimeVersion v2) + { + if (v1 is null) + { + return !(v2 is null); + } + + return v1.CompareTo(v2) < 0; + } + + public static bool operator <=(RuntimeVersion v1, RuntimeVersion v2) + { + if (v1 is null) + { + return true; + } + + return v1.CompareTo(v2) <= 0; + } + + public static bool operator >(RuntimeVersion v1, RuntimeVersion v2) => v2 < v1; + + public static bool operator >=(RuntimeVersion v1, RuntimeVersion v2) => v2 <= v1; + } +} diff --git a/src/libraries/Microsoft.NETCore.Platforms/src/runtimeGroups.props b/src/libraries/Microsoft.NETCore.Platforms/src/runtimeGroups.props index 75f41db11693e4..af77b2320c8b4a 100644 --- a/src/libraries/Microsoft.NETCore.Platforms/src/runtimeGroups.props +++ b/src/libraries/Microsoft.NETCore.Platforms/src/runtimeGroups.props @@ -269,7 +269,7 @@ - + diff --git a/src/libraries/Microsoft.NETCore.Platforms/tests/GenerateRuntimeGraphTests.cs b/src/libraries/Microsoft.NETCore.Platforms/tests/GenerateRuntimeGraphTests.cs index 652ca7f87ec334..53bc785e09deee 100644 --- a/src/libraries/Microsoft.NETCore.Platforms/tests/GenerateRuntimeGraphTests.cs +++ b/src/libraries/Microsoft.NETCore.Platforms/tests/GenerateRuntimeGraphTests.cs @@ -2,9 +2,11 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Linq; +using System.Runtime.CompilerServices; using Microsoft.Build.Evaluation; using Microsoft.Build.Framework; using Microsoft.Build.Utilities; +using NuGet.RuntimeModel; using Xunit; using Xunit.Abstractions; @@ -21,11 +23,11 @@ public GenerateRuntimeGraphTests(ITestOutputHelper output) _engine = new TestBuildEngine(_log); } - [Fact] - public void CanCreateRuntimeGraph() - { - string runtimeFile = "runtime.json"; + const string DefaultRuntimeFile = "runtime.json"; + private static ITaskItem[] DefaultRuntimeGroupItems { get; } = GetDefaultRuntimeGroupItems(); + private static ITaskItem[] GetDefaultRuntimeGroupItems() + { Project runtimeGroupProps = new Project("runtimeGroups.props"); ITaskItem[] runtimeGroups = runtimeGroupProps.GetItems("RuntimeGroupWithQualifiers") @@ -33,26 +35,207 @@ public void CanCreateRuntimeGraph() Assert.NotEmpty(runtimeGroups); + return runtimeGroups; + } + + private static ITaskItem CreateItem(ProjectItem projectItem) + { + TaskItem item = new TaskItem(projectItem.EvaluatedInclude); + foreach (var metadatum in projectItem.Metadata) + { + item.SetMetadata(metadatum.Name, metadatum.EvaluatedValue); + } + return item; + } + + [Fact] + public void CanCreateRuntimeGraph() + { // will generate and compare to existing file. GenerateRuntimeGraph task = new GenerateRuntimeGraph() { BuildEngine = _engine, - RuntimeGroups = runtimeGroups, - RuntimeJson = runtimeFile + RuntimeGroups = DefaultRuntimeGroupItems, + RuntimeJson = DefaultRuntimeFile, + UpdateRuntimeFiles = false }; task.Execute(); _log.AssertNoErrorsOrWarnings(); } - private static ITaskItem CreateItem(ProjectItem projectItem) + + [Fact] + public void CanIgnoreExistingInferRids() { - TaskItem item = new TaskItem(projectItem.EvaluatedInclude); - foreach(var metadatum in projectItem.Metadata) + // will generate and compare to existing file. + GenerateRuntimeGraph task = new GenerateRuntimeGraph() { - item.SetMetadata(metadatum.Name, metadatum.EvaluatedValue); - } - return item; + BuildEngine = _engine, + RuntimeGroups = DefaultRuntimeGroupItems, + RuntimeJson = DefaultRuntimeFile, + AdditionalRuntimeIdentifiers = new[] { "rhel.9-x64", "centos.9-arm64", "win-x64" }, + UpdateRuntimeFiles = false + }; + + _log.Reset(); + task.Execute(); + _log.AssertNoErrorsOrWarnings(); + } + + /// + /// Runs GenerateRuntimeGraph task specifying AdditionalRuntimeIdentifiers then asserts that the + /// generated runtime.json has the expected additions (and no more). + /// + /// additional RIDs + /// entries that are expected to be added to the RuntimeGraph + /// parent to use when adding a new RID + /// a unique prefix to use for the generated + private void AssertRuntimeGraphAdditions(string[] additionalRIDs, RuntimeDescription[] expectedAdditions, string additionalRIDParent = null, [CallerMemberName] string runtimeFilePrefix = null) + { + string runtimeFile = runtimeFilePrefix + ".runtime.json"; + + GenerateRuntimeGraph task = new GenerateRuntimeGraph() + { + BuildEngine = _engine, + RuntimeGroups = DefaultRuntimeGroupItems, + RuntimeJson = runtimeFile, + AdditionalRuntimeIdentifiers = additionalRIDs, + AdditionalRuntimeIdentifierParent = additionalRIDParent, + UpdateRuntimeFiles = true + }; + + _log.Reset(); + task.Execute(); + _log.AssertNoErrorsOrWarnings(); + + RuntimeGraph expected = RuntimeGraph.Merge( + JsonRuntimeFormat.ReadRuntimeGraph(DefaultRuntimeFile), + new RuntimeGraph(expectedAdditions)); + + RuntimeGraph actual = JsonRuntimeFormat.ReadRuntimeGraph(runtimeFile); + + // Should this assert fail, it's helpful to diff DefaultRuntimeFile and runtimeFile to see the additions. + Assert.Equal(expected, actual); } + + [Fact] + public void CanAddVersionsToExistingGroups() + { + var additionalRIDs = new[] { "ubuntu.22.04-arm64" }; + var expectedAdditions = new[] + { + new RuntimeDescription("ubuntu.22.04", new[] { "ubuntu" }), + new RuntimeDescription("ubuntu.22.04-x64", new[] { "ubuntu.22.04", "ubuntu-x64" }), + new RuntimeDescription("ubuntu.22.04-x86", new[] { "ubuntu.22.04", "ubuntu-x86" }), + new RuntimeDescription("ubuntu.22.04-arm", new[] { "ubuntu.22.04", "ubuntu-arm" }), + new RuntimeDescription("ubuntu.22.04-arm64", new[] { "ubuntu.22.04", "ubuntu-arm64" }) + }; + + AssertRuntimeGraphAdditions(additionalRIDs, expectedAdditions); + } + + [Fact] + public void CanAddParentVersionsToExistingGroups() + { + var additionalRIDs = new[] { "centos.9.2-arm64" }; + var expectedAdditions = new[] + { + new RuntimeDescription("centos.9.2", new[] { "centos", "rhel.9.2" }), + new RuntimeDescription("centos.9.2-x64", new[] { "centos.9.2", "centos-x64", "rhel.9.2-x64" }), + new RuntimeDescription("centos.9.2-arm64", new[] { "centos.9.2", "centos-arm64", "rhel.9.2-arm64" }), + + // rhel RIDs are implicitly created since centos imports versioned RHEL RIDs + new RuntimeDescription("rhel.9.2", new[] { "rhel.9" }), + new RuntimeDescription("rhel.9.2-x64", new[] { "rhel.9.2", "rhel.9-x64" }), + new RuntimeDescription("rhel.9.2-arm64", new[] { "rhel.9.2", "rhel.9-arm64" }) + }; + + AssertRuntimeGraphAdditions(additionalRIDs, expectedAdditions); + } + + [Fact] + public void CanAddMajorVersionsToExistingGroups() + { + + var additionalRIDs = new[] { "rhel.10-x64" }; + var expectedAdditions = new[] + { + // Note that rhel doesn't treat major versions as compatible, however we do since it's closest and we don't represent this policy in the RuntimeGroups explicitly. + // We could add a rule that wouldn't insert a new major version if we see existing groups are split by major version. + new RuntimeDescription("rhel.10", new[] { "rhel.9" }), + new RuntimeDescription("rhel.10-x64", new[] { "rhel.10", "rhel.9-x64" }), + new RuntimeDescription("rhel.10-arm64", new[] { "rhel.10", "rhel.9-arm64" }) + }; + + AssertRuntimeGraphAdditions(additionalRIDs, expectedAdditions); + } + + [Fact] + public void CanAddArchitectureToExistingGroups() + { + var additionalRIDs = new[] { "win10-x128" }; + var expectedAdditions = new[] + { + new RuntimeDescription("win10-x128", new[] { "win10", "win81-x128" }), + new RuntimeDescription("win10-x128-aot", new[] { "win10-aot", "win10-x128", "win10", "win81-x128-aot" }), + new RuntimeDescription("win81-x128-aot", new[] { "win81-aot", "win81-x128", "win81", "win8-x128-aot" }), + new RuntimeDescription("win81-x128", new[] { "win81", "win8-x128" }), + new RuntimeDescription("win8-x128-aot", new[] { "win8-aot", "win8-x128", "win8", "win7-x128-aot" }), + new RuntimeDescription("win8-x128", new[] { "win8", "win7-x128" }), + new RuntimeDescription("win7-x128-aot", new[] { "win7-aot", "win7-x128", "win7", "win-x128-aot" }), + new RuntimeDescription("win7-x128", new[] { "win7", "win-x128" }), + new RuntimeDescription("win-x128-aot", new[] { "win-aot", "win-x128" }), + new RuntimeDescription("win-x128", new[] { "win" }) + }; + + AssertRuntimeGraphAdditions(additionalRIDs, expectedAdditions); + } + + + [Fact] + public void CanAddArchitectureAndVersionToExistingGroups() + { + var additionalRIDs = new[] { "osx.12-powerpc" }; + var expectedAdditions = new[] + { + new RuntimeDescription("osx.12-powerpc", new[] { "osx.12", "osx.11.0-powerpc" }), + new RuntimeDescription("osx.12-arm64", new[] { "osx.12", "osx.11.0-arm64" }), + new RuntimeDescription("osx.12-x64", new[] { "osx.12", "osx.11.0-x64" }), + new RuntimeDescription("osx.12", new[] { "osx.11.0" }), + // our RID model doesn't give priority to architecture, so the new architecture is applied to all past versions + new RuntimeDescription("osx.11.0-powerpc", new[] { "osx.11.0", "osx.10.16-powerpc" }), + new RuntimeDescription("osx.10.16-powerpc", new[] { "osx.10.16", "osx.10.15-powerpc" }), + new RuntimeDescription("osx.10.15-powerpc", new[] { "osx.10.15", "osx.10.14-powerpc" }), + new RuntimeDescription("osx.10.14-powerpc", new[] { "osx.10.14", "osx.10.13-powerpc" }), + new RuntimeDescription("osx.10.13-powerpc", new[] { "osx.10.13", "osx.10.12-powerpc" }), + new RuntimeDescription("osx.10.12-powerpc", new[] { "osx.10.12", "osx.10.11-powerpc" }), + new RuntimeDescription("osx.10.11-powerpc", new[] { "osx.10.11", "osx.10.10-powerpc" }), + new RuntimeDescription("osx.10.10-powerpc", new[] { "osx.10.10", "osx-powerpc" }), + new RuntimeDescription("unix-powerpc", new[] { "unix" }), + new RuntimeDescription("osx-powerpc", new[] { "osx", "unix-powerpc" }), + }; + + AssertRuntimeGraphAdditions(additionalRIDs, expectedAdditions); + } + + [Fact] + public void CanAddNewGroups() + { + var additionalRIDs = new[] { "yolinux.42.0-quantum" }; + var expectedAdditions = new[] + { + new RuntimeDescription("unix-quantum", new[] { "unix" }), + new RuntimeDescription("linux-quantum", new[] { "linux", "unix-quantum" }), + new RuntimeDescription("linux-musl-quantum", new[] { "linux-musl", "linux-quantum" }), + new RuntimeDescription("yolinux", new[] { "linux-musl" }), + new RuntimeDescription("yolinux-quantum", new[] { "yolinux", "linux-musl-quantum" }), + new RuntimeDescription("yolinux.42.0", new[] { "yolinux" }), + new RuntimeDescription("yolinux.42.0-quantum", new[] { "yolinux.42.0", "yolinux-quantum" }) + }; + + AssertRuntimeGraphAdditions(additionalRIDs, expectedAdditions, "linux-musl"); + } + } } diff --git a/src/libraries/Microsoft.NETCore.Platforms/tests/Microsoft.NETCore.Platforms.Tests.csproj b/src/libraries/Microsoft.NETCore.Platforms/tests/Microsoft.NETCore.Platforms.Tests.csproj index cb254a5cb5070b..d39b20d6d9f5e2 100644 --- a/src/libraries/Microsoft.NETCore.Platforms/tests/Microsoft.NETCore.Platforms.Tests.csproj +++ b/src/libraries/Microsoft.NETCore.Platforms/tests/Microsoft.NETCore.Platforms.Tests.csproj @@ -1,4 +1,4 @@ - + $(NetCoreAppCurrent);net472 true @@ -14,6 +14,8 @@ + + diff --git a/src/libraries/Microsoft.NETCore.Platforms/tests/RidTests.cs b/src/libraries/Microsoft.NETCore.Platforms/tests/RidTests.cs new file mode 100644 index 00000000000000..227bcbdd10d4db --- /dev/null +++ b/src/libraries/Microsoft.NETCore.Platforms/tests/RidTests.cs @@ -0,0 +1,44 @@ +// 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.NETCore.Platforms.BuildTasks.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 + yield return new object[] { "linux-musl-x64", new RID() { BaseRID = "linux", Architecture = "musl", Qualifier = "x64" } }; // yes, we already have ambiguous RIDs + } + + [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/libraries/Microsoft.NETCore.Platforms/tests/RuntimeVersionTests.cs b/src/libraries/Microsoft.NETCore.Platforms/tests/RuntimeVersionTests.cs new file mode 100644 index 00000000000000..2822974acfacc4 --- /dev/null +++ b/src/libraries/Microsoft.NETCore.Platforms/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.NETCore.Platforms.BuildTasks.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); + } + + } +}