diff --git a/Directory.Build.props b/Directory.Build.props index 784fa37b6f2b78..be3d42e18ba93f 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -228,7 +228,7 @@ https://github.com/dotnet/$(GitHubRepositoryName) https://dot.net microsoft,dotnetframework - $([MSBuild]::NormalizePath('$(LibrariesProjectRoot)', 'Microsoft.NETCore.Platforms', 'pkg', 'runtime.json')) + $([MSBuild]::NormalizePath('$(LibrariesProjectRoot)', 'Microsoft.NETCore.Platforms', 'src', 'runtime.json')) $(MSBuildThisFileDirectory)LICENSE.TXT MIT $(CopyrightNetFoundation) diff --git a/eng/Versions.props b/eng/Versions.props index bdb9ffcb33dbe7..d74990f09b32d7 100644 --- a/eng/Versions.props +++ b/eng/Versions.props @@ -139,12 +139,12 @@ These are used as reference assemblies only, so they must not take a ProdCon/source-build version. Insert "RefOnly" to avoid assignment via PVP. --> - 15.7.179 + 16.8.0 $(RefOnlyMicrosoftBuildVersion) $(RefOnlyMicrosoftBuildVersion) $(RefOnlyMicrosoftBuildVersion) - 4.9.4 - 4.9.4 + 5.8.0 + 5.8.0 1.0.1-prerelease-00006 16.9.0-preview-20201201-01 diff --git a/src/libraries/Directory.Build.props b/src/libraries/Directory.Build.props index 7803ca3e6f1c5b..f36f83de45246b 100644 --- a/src/libraries/Directory.Build.props +++ b/src/libraries/Directory.Build.props @@ -21,7 +21,7 @@ $(RepositoryEngineeringDir)LicenseHeader.txt - + $([System.Text.RegularExpressions.Regex]::Replace('$(TargetFramework)', '(-[^;]+)', '')) diff --git a/src/libraries/Directory.Build.targets b/src/libraries/Directory.Build.targets index 96fc0a734f8fa3..a04a93dfb0d6be 100644 --- a/src/libraries/Directory.Build.targets +++ b/src/libraries/Directory.Build.targets @@ -116,7 +116,7 @@ - + diff --git a/src/libraries/Microsoft.NETCore.Platforms/Microsoft.NETCore.Platforms.sln b/src/libraries/Microsoft.NETCore.Platforms/Microsoft.NETCore.Platforms.sln new file mode 100644 index 00000000000000..a31d754662018e --- /dev/null +++ b/src/libraries/Microsoft.NETCore.Platforms/Microsoft.NETCore.Platforms.sln @@ -0,0 +1,31 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 16 +VisualStudioVersion = 16.0.31004.24 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.NETCore.Platforms", "src\Microsoft.NETCore.Platforms.csproj", "{BFFF96CC-06AA-4291-9F93-3E77F23DBB11}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.NETCore.Platforms.Tests", "tests\Microsoft.NETCore.Platforms.Tests.csproj", "{0C60F372-5C73-4BFA-9B91-5659C88F9750}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {BFFF96CC-06AA-4291-9F93-3E77F23DBB11}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {BFFF96CC-06AA-4291-9F93-3E77F23DBB11}.Debug|Any CPU.Build.0 = Debug|Any CPU + {BFFF96CC-06AA-4291-9F93-3E77F23DBB11}.Release|Any CPU.ActiveCfg = Release|Any CPU + {BFFF96CC-06AA-4291-9F93-3E77F23DBB11}.Release|Any CPU.Build.0 = Release|Any CPU + {0C60F372-5C73-4BFA-9B91-5659C88F9750}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {0C60F372-5C73-4BFA-9B91-5659C88F9750}.Debug|Any CPU.Build.0 = Debug|Any CPU + {0C60F372-5C73-4BFA-9B91-5659C88F9750}.Release|Any CPU.ActiveCfg = Release|Any CPU + {0C60F372-5C73-4BFA-9B91-5659C88F9750}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {E946A528-C3E7-48EC-AD6D-AE84ED2B11AC} + EndGlobalSection +EndGlobal diff --git a/src/libraries/Microsoft.NETCore.Platforms/pkg/Microsoft.NETCore.Platforms.pkgproj b/src/libraries/Microsoft.NETCore.Platforms/pkg/Microsoft.NETCore.Platforms.pkgproj deleted file mode 100644 index e24d21ae013bb4..00000000000000 --- a/src/libraries/Microsoft.NETCore.Platforms/pkg/Microsoft.NETCore.Platforms.pkgproj +++ /dev/null @@ -1,18 +0,0 @@ - - - - true - - false - Provides runtime information required to resolve target framework, platform, and runtime specific implementations of .NETCore packages. - - - - - - lib/netstandard1.0 - - - - - diff --git a/src/libraries/Microsoft.NETCore.Platforms/src/AssemblyResolver.cs b/src/libraries/Microsoft.NETCore.Platforms/src/AssemblyResolver.cs new file mode 100644 index 00000000000000..a0f4aaad29bc0c --- /dev/null +++ b/src/libraries/Microsoft.NETCore.Platforms/src/AssemblyResolver.cs @@ -0,0 +1,108 @@ +// 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.IO; +using System.Reflection; + +namespace Microsoft.NETCore.Platforms.BuildTasks +{ + /// + /// Used to enable app-local assembly unification. + /// + internal static class AssemblyResolver + { + static AssemblyResolver() + { + AppDomain.CurrentDomain.AssemblyResolve += CurrentDomain_AssemblyResolve; + } + + /// + /// Call to enable the assembly resolver for the current AppDomain. + /// + public static void Enable() + { + // intentionally empty. This is just meant to ensure the static constructor + // has run. + } + + private static Assembly CurrentDomain_AssemblyResolve(object sender, ResolveEventArgs args) + { + // apply any existing policy + AssemblyName referenceName = new AssemblyName(AppDomain.CurrentDomain.ApplyPolicy(args.Name)); + + string fileName = referenceName.Name + ".dll"; + string assemblyPath = null; + string probingPath = null; + Assembly assm = null; + + // look next to requesting assembly + assemblyPath = args.RequestingAssembly?.Location; + if (!string.IsNullOrEmpty(assemblyPath)) + { + probingPath = Path.Combine(Path.GetDirectoryName(assemblyPath), fileName); + Debug.WriteLine($"Considering {probingPath} based on RequestingAssembly"); + if (Probe(probingPath, referenceName.Version, out assm)) + { + return assm; + } + } + + // look next to the executing assembly + assemblyPath = Assembly.GetExecutingAssembly().Location; + if (!string.IsNullOrEmpty(assemblyPath)) + { + probingPath = Path.Combine(Path.GetDirectoryName(assemblyPath), fileName); + + Debug.WriteLine($"Considering {probingPath} based on ExecutingAssembly"); + if (Probe(probingPath, referenceName.Version, out assm)) + { + return assm; + } + } + + // look in AppDomain base directory + probingPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, fileName); + Debug.WriteLine($"Considering {probingPath} based on BaseDirectory"); + if (Probe(probingPath, referenceName.Version, out assm)) + { + return assm; + } + + // look in current directory + Debug.WriteLine($"Considering {fileName}"); + if (Probe(fileName, referenceName.Version, out assm)) + { + return assm; + } + + return null; + } + + /// + /// Considers a path to load for satisfying an assembly ref and loads it + /// if the file exists and version is sufficient. + /// + /// Path to consider for load + /// Minimum version to consider + /// loaded assembly + /// true if assembly was loaded + private static bool Probe(string filePath, Version minimumVersion, out Assembly assembly) + { + if (File.Exists(filePath)) + { + AssemblyName name = AssemblyName.GetAssemblyName(filePath); + + if (name.Version >= minimumVersion) + { + assembly = Assembly.Load(name); + return true; + } + } + + assembly = null; + return false; + } + } +} diff --git a/src/libraries/Microsoft.NETCore.Platforms/src/BuildTask.Desktop.cs b/src/libraries/Microsoft.NETCore.Platforms/src/BuildTask.Desktop.cs new file mode 100644 index 00000000000000..29af5f186cfb41 --- /dev/null +++ b/src/libraries/Microsoft.NETCore.Platforms/src/BuildTask.Desktop.cs @@ -0,0 +1,13 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.NETCore.Platforms.BuildTasks +{ + public partial class BuildTask + { + static BuildTask() + { + AssemblyResolver.Enable(); + } + } +} diff --git a/src/libraries/Microsoft.NETCore.Platforms/src/BuildTask.cs b/src/libraries/Microsoft.NETCore.Platforms/src/BuildTask.cs new file mode 100644 index 00000000000000..fa7dd7edabda9a --- /dev/null +++ b/src/libraries/Microsoft.NETCore.Platforms/src/BuildTask.cs @@ -0,0 +1,151 @@ +// 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 Microsoft.Build.Utilities; +using System; + +namespace Microsoft.NETCore.Platforms.BuildTasks +{ + public abstract partial class BuildTask : ITask + { + private Log _log; + + internal Log Log + { + get { return _log ?? (_log = new Log(new TaskLoggingHelper(this))); } + } + + public BuildTask() + { + } + + public IBuildEngine BuildEngine + { + get; + set; + } + + public ITaskHost HostObject + { + get; + set; + } + + public abstract bool Execute(); + } + + internal class Log : ILog + { + private readonly TaskLoggingHelper _logger; + public Log(TaskLoggingHelper logger) + { + _logger = logger; + } + + public void LogError(string message, params object[] messageArgs) + { + _logger.LogError(message, messageArgs); + } + + public void LogErrorFromException(Exception exception, bool showStackTrace) + { + _logger.LogErrorFromException(exception, showStackTrace); + } + + public void LogMessage(string message, params object[] messageArgs) + { + _logger.LogMessage(message, messageArgs); + } + + public void LogMessage(LogImportance importance, string message, params object[] messageArgs) + { + _logger.LogMessage((MessageImportance)importance, message, messageArgs); + } + + public void LogWarning(string message, params object[] messageArgs) + { + _logger.LogWarning(message, messageArgs); + } + + public bool HasLoggedErrors { get { return _logger.HasLoggedErrors; } } + } + + public enum LogImportance + { + Low = MessageImportance.Low, + Normal = MessageImportance.Normal, + High = MessageImportance.High + } + + + public interface ILog + { + // + // Summary: + // Logs an error with the specified message. + // + // Parameters: + // message: + // The message. + // + // messageArgs: + // Optional arguments for formatting the message string. + // + // Exceptions: + // T:System.ArgumentNullException: + // message is null. + void LogError(string message, params object[] messageArgs); + + // + // Summary: + // Logs a message with the specified string. + // + // Parameters: + // message: + // The message. + // + // messageArgs: + // The arguments for formatting the message. + // + // Exceptions: + // T:System.ArgumentNullException: + // message is null. + void LogMessage(string message, params object[] messageArgs); + + // + // Summary: + // Logs a message with the specified string and importance. + // + // Parameters: + // importance: + // One of the enumeration values that specifies the importance of the message. + // + // message: + // The message. + // + // messageArgs: + // The arguments for formatting the message. + // + // Exceptions: + // T:System.ArgumentNullException: + // message is null. + void LogMessage(LogImportance importance, string message, params object[] messageArgs); + + // + // Summary: + // Logs a warning with the specified message. + // + // Parameters: + // message: + // The message. + // + // messageArgs: + // Optional arguments for formatting the message string. + // + // Exceptions: + // T:System.ArgumentNullException: + // message is null. + void LogWarning(string message, params object[] messageArgs); + } +} diff --git a/src/libraries/Microsoft.NETCore.Platforms/src/Directory.Build.props b/src/libraries/Microsoft.NETCore.Platforms/src/Directory.Build.props new file mode 100644 index 00000000000000..ea706165b9f58f --- /dev/null +++ b/src/libraries/Microsoft.NETCore.Platforms/src/Directory.Build.props @@ -0,0 +1,6 @@ + + + false + + + \ No newline at end of file diff --git a/src/libraries/Microsoft.NETCore.Platforms/src/Extensions.cs b/src/libraries/Microsoft.NETCore.Platforms/src/Extensions.cs new file mode 100644 index 00000000000000..128b56082e1fec --- /dev/null +++ b/src/libraries/Microsoft.NETCore.Platforms/src/Extensions.cs @@ -0,0 +1,46 @@ +// 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; + +namespace Microsoft.NETCore.Platforms.BuildTasks +{ + public static class Extensions + { + 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); + if (!bool.TryParse(metadataValue, out result)) + { + result = defaultValue; + } + return result; + } + + 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) + { + return source ?? Enumerable.Empty(); + } + + } +} diff --git a/src/libraries/Microsoft.NETCore.Platforms/src/GenerateRuntimeGraph.cs b/src/libraries/Microsoft.NETCore.Platforms/src/GenerateRuntimeGraph.cs new file mode 100644 index 00000000000000..cbee78b71a6b06 --- /dev/null +++ b/src/libraries/Microsoft.NETCore.Platforms/src/GenerateRuntimeGraph.cs @@ -0,0 +1,392 @@ +// 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.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using System.Xml.Linq; +using Microsoft.Build.Framework; +using Newtonsoft.Json; +using NuGet.RuntimeModel; + +namespace Microsoft.NETCore.Platforms.BuildTasks +{ + public class GenerateRuntimeGraph : BuildTask + { + + /// + /// 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). + /// + /// The following options can be used under special circumstances but break the normal precedence rules we try to establish + /// by generating the RID graph from common logic. These options make it possible to create a RID fallback chain that doesn't + /// match the rest of the RIDs and therefore is hard for developers/package authors to reason about. + /// Only use these options for cases where you know what you are doing and have carefully reviewed the resulting RID fallbacks + /// using the CompatibliltyMap. + /// OmitRIDs: A list of strings delimited by semi-colons that represent RIDs calculated from this RuntimeGroup that should + /// be omitted from the RuntimeGraph. These RIDs will not be referenced nor defined. + /// OmitRIDDefinitions: A list of strings delimited by semi-colons that represent RIDs calculated from this RuntimeGroup + /// that should be omitted from the RuntimeGraph. These RIDs will not be defined by this RuntimeGroup, but will be + /// referenced: useful in case some other RuntimeGroup (or runtime.json template) defines them. + /// OmitRIDReferences: A list of strings delimited by semi-colons that represent RIDs calculated from this RuntimeGroup + /// that should be omitted from the RuntimeGraph. These RIDs will be defined but not referenced by this RuntimeGroup. + /// + public ITaskItem[] RuntimeGroups + { + get; + set; + } + + /// + /// Optional source Runtime.json to use as a starting point when merging additional RuntimeGroups + /// + public string SourceRuntimeJson + { + get; + set; + } + + /// + /// Where to write the final runtime.json + /// + public string RuntimeJson + { + get; + set; + } + + /// + /// Optionally, other runtime.jsons which may contain imported RIDs + /// + public string[] ExternalRuntimeJsons + { + get; + set; + } + + /// + /// When defined, specifies the file to write compatibility precedence for each RID in the graph. + /// + public string CompatibilityMap + { + get; + set; + } + + + /// + /// True to write the generated runtime.json to RuntimeJson and compatibility map to CompatibilityMap, otherwise files are read and diffed + /// with generated versions and an error is emitted if they differ. + /// Setting UpdateRuntimeFiles will overwrite files even when the file is marked ReadOnly. + /// + public bool UpdateRuntimeFiles + { + get; + set; + } + + /// + /// When defined, specifies the file to write a DGML representation of the runtime graph. + /// + public string RuntimeDirectedGraph + { + get; + set; + } + + public override bool Execute() + { + if (RuntimeGroups != null && RuntimeGroups.Any() && RuntimeJson == null) + { + Log.LogError($"{nameof(RuntimeJson)} argument must be specified when {nameof(RuntimeGroups)} is specified."); + return false; + } + + RuntimeGraph runtimeGraph; + if (!string.IsNullOrEmpty(SourceRuntimeJson)) + { + if (!File.Exists(SourceRuntimeJson)) + { + Log.LogError($"{nameof(SourceRuntimeJson)} did not exist at {SourceRuntimeJson}."); + return false; + } + + runtimeGraph = JsonRuntimeFormat.ReadRuntimeGraph(SourceRuntimeJson); + } + else + { + runtimeGraph = new RuntimeGraph(); + } + + foreach (var runtimeGroup in RuntimeGroups.NullAsEmpty().Select(i => new RuntimeGroup(i))) + { + runtimeGraph = SafeMerge(runtimeGraph, runtimeGroup); + } + + Dictionary externalRids = new Dictionary(); + if (ExternalRuntimeJsons != null) + { + foreach (var externalRuntimeJson in ExternalRuntimeJsons) + { + RuntimeGraph externalRuntimeGraph = JsonRuntimeFormat.ReadRuntimeGraph(externalRuntimeJson); + + foreach (var runtime in externalRuntimeGraph.Runtimes.Keys) + { + // don't check for duplicates, we merely care what is external + externalRids.Add(runtime, externalRuntimeJson); + } + } + } + + ValidateImports(runtimeGraph, externalRids); + + if (!string.IsNullOrEmpty(RuntimeJson)) + { + if (UpdateRuntimeFiles) + { + EnsureWritable(RuntimeJson); + WriteRuntimeGraph(RuntimeJson, runtimeGraph); + + } + else + { + // validate that existing file matches generated file + if (!File.Exists(RuntimeJson)) + { + Log.LogError($"{nameof(RuntimeJson)} did not exist at {RuntimeJson} and {nameof(UpdateRuntimeFiles)} was not specified."); + } + else + { + var existingRuntimeGraph = JsonRuntimeFormat.ReadRuntimeGraph(RuntimeJson); + + if (!existingRuntimeGraph.Equals(runtimeGraph)) + { + Log.LogError($"The generated {nameof(RuntimeJson)} differs from {RuntimeJson} and {nameof(UpdateRuntimeFiles)} was not specified. Please specify {nameof(UpdateRuntimeFiles)}=true to commit the changes."); + } + } + } + } + + if (!string.IsNullOrEmpty(CompatibilityMap)) + { + var compatibilityMap = GetCompatibilityMap(runtimeGraph); + if (UpdateRuntimeFiles) + { + EnsureWritable(CompatibilityMap); + WriteCompatibilityMap(compatibilityMap, CompatibilityMap); + } + else + { + // validate that existing file matches generated file + if (!File.Exists(CompatibilityMap)) + { + Log.LogError($"{nameof(CompatibilityMap)} did not exist at {CompatibilityMap} and {nameof(UpdateRuntimeFiles)} was not specified."); + } + else + { + var existingCompatibilityMap = ReadCompatibilityMap(CompatibilityMap); + + if (!CompatibilityMapEquals(existingCompatibilityMap, compatibilityMap)) + { + Log.LogError($"The generated {nameof(CompatibilityMap)} differs from {CompatibilityMap} and {nameof(UpdateRuntimeFiles)} was not specified. Please specify {nameof(UpdateRuntimeFiles)}=true to commit the changes."); + } + } + } + } + + if (!string.IsNullOrEmpty(RuntimeDirectedGraph)) + { + WriteRuntimeGraph(runtimeGraph, RuntimeDirectedGraph); + } + + return !Log.HasLoggedErrors; + } + + private void EnsureWritable(string file) + { + if (File.Exists(file)) + { + var existingAttributes = File.GetAttributes(file); + + if ((existingAttributes & FileAttributes.ReadOnly) != 0) + { + File.SetAttributes(file, existingAttributes &= ~FileAttributes.ReadOnly); + } + } + } + + public static void WriteRuntimeGraph(string filePath, RuntimeGraph runtimeGraph) + { + using (var fileStream = new FileStream(filePath, FileMode.Create)) + using (var textWriter = new StreamWriter(fileStream)) + using (var jsonWriter = new JsonTextWriter(textWriter)) + using (var writer = new JsonObjectWriter(jsonWriter)) + { + jsonWriter.Formatting = Formatting.Indented; + + // workaround https://github.com/NuGet/Home/issues/9532 + writer.WriteObjectStart(); + + JsonRuntimeFormat.WriteRuntimeGraph(writer, runtimeGraph); + + writer.WriteObjectEnd(); + } + } + + private RuntimeGraph SafeMerge(RuntimeGraph existingGraph, RuntimeGroup runtimeGroup) + { + var runtimeGraph = runtimeGroup.GetRuntimeGraph(); + + 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 {nameof(RuntimeGroup.OmitRIDDefinitions)} metadata with {newRuntimeDescription.RuntimeIdentifier}."); + } + } + } + + return RuntimeGraph.Merge(existingGraph, runtimeGraph); + } + + private void ValidateImports(RuntimeGraph runtimeGraph, IDictionary externalRIDs) + { + foreach (var runtimeDescription in runtimeGraph.Runtimes.Values) + { + string externalRuntimeJson; + + if (externalRIDs.TryGetValue(runtimeDescription.RuntimeIdentifier, out externalRuntimeJson)) + { + Log.LogError($"Runtime {runtimeDescription.RuntimeIdentifier} is defined in both this RuntimeGraph and {externalRuntimeJson}."); + } + + foreach (var import in runtimeDescription.InheritedRuntimes) + { + if (!runtimeGraph.Runtimes.ContainsKey(import) && !externalRIDs.ContainsKey(import)) + { + Log.LogError($"Runtime {runtimeDescription.RuntimeIdentifier} imports {import} which is not defined."); + } + } + } + } + + private static IDictionary> GetCompatibilityMap(RuntimeGraph graph) + { + Dictionary> compatibilityMap = new Dictionary>(); + + foreach (var rid in graph.Runtimes.Keys.OrderBy(rid => rid, StringComparer.Ordinal)) + { + compatibilityMap.Add(rid, graph.ExpandRuntime(rid)); + } + + return compatibilityMap; + } + + private static IDictionary> ReadCompatibilityMap(string mapFile) + { + var serializer = new JsonSerializer(); + using (var file = File.OpenText(mapFile)) + using (var jsonTextReader = new JsonTextReader(file)) + { + return serializer.Deserialize>>(jsonTextReader); + } + } + + private static void WriteCompatibilityMap(IDictionary> compatibilityMap, string mapFile) + { + var serializer = new JsonSerializer() + { + Formatting = Formatting.Indented, + StringEscapeHandling = StringEscapeHandling.EscapeNonAscii + }; + + string directory = Path.GetDirectoryName(mapFile); + if (!string.IsNullOrEmpty(directory) && !Directory.Exists(directory)) + { + Directory.CreateDirectory(directory); + } + + using (var file = File.CreateText(mapFile)) + { + serializer.Serialize(file, compatibilityMap); + } + } + + private static bool CompatibilityMapEquals(IDictionary> left, IDictionary> right) + { + if (left.Count != right.Count) + { + return false; + } + + foreach (var leftPair in left) + { + IEnumerable rightValue; + + if (!right.TryGetValue(leftPair.Key, out rightValue)) + { + return false; + } + + if (!rightValue.SequenceEqual(leftPair.Value)) + { + return false; + } + } + + return true; + } + + private static XNamespace s_dgmlns = @"http://schemas.microsoft.com/vs/2009/dgml"; + private static void WriteRuntimeGraph(RuntimeGraph graph, string dependencyGraphFilePath) + { + + var doc = new XDocument(new XElement(s_dgmlns + "DirectedGraph")); + var nodesElement = new XElement(s_dgmlns + "Nodes"); + var linksElement = new XElement(s_dgmlns + "Links"); + doc.Root.Add(nodesElement); + doc.Root.Add(linksElement); + + var nodeIds = new HashSet(StringComparer.OrdinalIgnoreCase); + + foreach (var runtimeDescription in graph.Runtimes.Values) + { + nodesElement.Add(new XElement(s_dgmlns + "Node", + new XAttribute("Id", runtimeDescription.RuntimeIdentifier))); + + foreach (var import in runtimeDescription.InheritedRuntimes) + { + linksElement.Add(new XElement(s_dgmlns + "Link", + new XAttribute("Source", runtimeDescription.RuntimeIdentifier), + new XAttribute("Target", import))); + } + } + + using (var file = File.Create(dependencyGraphFilePath)) + { + doc.Save(file); + } + } + } +} 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 987d53fcabe74f..3c00187ddb18e1 100644 --- a/src/libraries/Microsoft.NETCore.Platforms/src/Microsoft.NETCore.Platforms.csproj +++ b/src/libraries/Microsoft.NETCore.Platforms/src/Microsoft.NETCore.Platforms.csproj @@ -1,9 +1,42 @@ - + - netstandard1.0 - true - - false + $(NetCoreAppToolCurrent);net472 + $(MSBuildProjectName) + true + Microsoft.NETCore.Platforms.BuildTasks + + false + true + false + true + $(NoWarn);NU5128 + Provides runtime information required to resolve target framework, platform, and runtime specific implementations of .NETCore packages. + + + + + + + + + + + + + + + + + + + + + + + + + + \ 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 new file mode 100644 index 00000000000000..357790fbfdd54d --- /dev/null +++ b/src/libraries/Microsoft.NETCore.Platforms/src/RID.cs @@ -0,0 +1,59 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Text; + +namespace Microsoft.NETCore.Platforms.BuildTasks +{ + internal 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; + } + } + +} diff --git a/src/libraries/Microsoft.NETCore.Platforms/src/RuntimeGroup.cs b/src/libraries/Microsoft.NETCore.Platforms/src/RuntimeGroup.cs new file mode 100644 index 00000000000000..2ddd60022c426a --- /dev/null +++ b/src/libraries/Microsoft.NETCore.Platforms/src/RuntimeGroup.cs @@ -0,0 +1,264 @@ +// 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; + +namespace Microsoft.NETCore.Platforms.BuildTasks +{ + + internal 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))); + 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 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) + }; + + 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) + { + // 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); + + foreach (var architecture in Architectures) + { + // 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) + { + archImports.Add(CreateRuntime(Parent, version: version, architecture: architecture)); + } + + 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() + { + CreateRuntime(BaseRID, qualifier: qualifier), + CreateRuntime(BaseRID, architecture: architecture) + }; + + if (!IsNullOrRoot(Parent)) + { + imports.Add(CreateRuntime(Parent, architecture: architecture, qualifier: qualifier)); + } + + 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() + { + 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) + { + imports.Add(CreateRuntime(Parent, version: version, architecture: architecture, qualifier: qualifier)); + } + + 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; + } + + + public IEnumerable GetRuntimeDescriptions() + { + foreach (var mapping in GetRIDMappings()) + { + var rid = mapping.RuntimeIdentifier.ToString(); + + if (OmitRIDs.Contains(rid) || OmitRIDDefinitions.Contains(rid)) + { + continue; + } + + var imports = mapping.Imports + .Select(i => i.ToString()) + .Where(i => !OmitRIDs.Contains(i) && !OmitRIDReferences.Contains(i)) + .ToArray(); + + yield return new RuntimeDescription(rid, imports); + } + } + + public RuntimeGraph GetRuntimeGraph() + { + return new RuntimeGraph(GetRuntimeDescriptions()); + } + } +} diff --git a/src/libraries/Microsoft.NETCore.Platforms/src/_._ b/src/libraries/Microsoft.NETCore.Platforms/src/_._ new file mode 100644 index 00000000000000..e69de29bb2d1d6 diff --git a/src/libraries/Microsoft.NETCore.Platforms/pkg/runtime.compatibility.json b/src/libraries/Microsoft.NETCore.Platforms/src/runtime.compatibility.json similarity index 100% rename from src/libraries/Microsoft.NETCore.Platforms/pkg/runtime.compatibility.json rename to src/libraries/Microsoft.NETCore.Platforms/src/runtime.compatibility.json diff --git a/src/libraries/Microsoft.NETCore.Platforms/pkg/runtime.json b/src/libraries/Microsoft.NETCore.Platforms/src/runtime.json similarity index 100% rename from src/libraries/Microsoft.NETCore.Platforms/pkg/runtime.json rename to src/libraries/Microsoft.NETCore.Platforms/src/runtime.json diff --git a/src/libraries/Microsoft.NETCore.Platforms/pkg/runtimeGroups.props b/src/libraries/Microsoft.NETCore.Platforms/src/runtimeGroups.props similarity index 91% rename from src/libraries/Microsoft.NETCore.Platforms/pkg/runtimeGroups.props rename to src/libraries/Microsoft.NETCore.Platforms/src/runtimeGroups.props index 5d4a3edf9e9561..75f41db11693e4 100644 --- a/src/libraries/Microsoft.NETCore.Platforms/pkg/runtimeGroups.props +++ b/src/libraries/Microsoft.NETCore.Platforms/src/runtimeGroups.props @@ -262,15 +262,22 @@ - + + <_generateRuntimeGraphTargetFramework Condition="'$(MSBuildRuntimeType)' == 'core'">$(NetCoreAppToolCurrent) + <_generateRuntimeGraphTargetFramework Condition="'$(MSBuildRuntimeType)' != 'core'">net472 + <_generateRuntimeGraphTask>$([MSBuild]::NormalizePath('$(BaseOutputPath)', '$(_generateRuntimeGraphTargetFramework)-$(Configuration)', '$(AssemblyName).dll')) + + + + - + diff --git a/src/libraries/Microsoft.NETCore.Platforms/tests/AssemblyInfo.cs b/src/libraries/Microsoft.NETCore.Platforms/tests/AssemblyInfo.cs new file mode 100644 index 00000000000000..baad52408c410d --- /dev/null +++ b/src/libraries/Microsoft.NETCore.Platforms/tests/AssemblyInfo.cs @@ -0,0 +1,6 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Xunit; + +[assembly: SkipOnMono("MSBuild is not supported on Browser", TestPlatforms.Browser)] \ No newline at end of file diff --git a/src/libraries/Microsoft.NETCore.Platforms/tests/GenerateRuntimeGraphTests.cs b/src/libraries/Microsoft.NETCore.Platforms/tests/GenerateRuntimeGraphTests.cs new file mode 100644 index 00000000000000..652ca7f87ec334 --- /dev/null +++ b/src/libraries/Microsoft.NETCore.Platforms/tests/GenerateRuntimeGraphTests.cs @@ -0,0 +1,58 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Linq; +using Microsoft.Build.Evaluation; +using Microsoft.Build.Framework; +using Microsoft.Build.Utilities; +using Xunit; +using Xunit.Abstractions; + +namespace Microsoft.NETCore.Platforms.BuildTasks.Tests +{ + public class GenerateRuntimeGraphTests + { + private Log _log; + private TestBuildEngine _engine; + + public GenerateRuntimeGraphTests(ITestOutputHelper output) + { + _log = new Log(output); + _engine = new TestBuildEngine(_log); + } + + [Fact] + public void CanCreateRuntimeGraph() + { + string runtimeFile = "runtime.json"; + + Project runtimeGroupProps = new Project("runtimeGroups.props"); + + ITaskItem[] runtimeGroups = runtimeGroupProps.GetItems("RuntimeGroupWithQualifiers") + .Select(i => CreateItem(i)).ToArray(); + + Assert.NotEmpty(runtimeGroups); + + // will generate and compare to existing file. + GenerateRuntimeGraph task = new GenerateRuntimeGraph() + { + BuildEngine = _engine, + RuntimeGroups = runtimeGroups, + RuntimeJson = runtimeFile + }; + task.Execute(); + + _log.AssertNoErrorsOrWarnings(); + } + + 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; + } + } +} diff --git a/src/libraries/Microsoft.NETCore.Platforms/tests/Log.cs b/src/libraries/Microsoft.NETCore.Platforms/tests/Log.cs new file mode 100644 index 00000000000000..d1e7368ec0882b --- /dev/null +++ b/src/libraries/Microsoft.NETCore.Platforms/tests/Log.cs @@ -0,0 +1,56 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Xunit; +using Xunit.Abstractions; + +namespace Microsoft.NETCore.Platforms.BuildTasks.Tests +{ + internal class Log : ILog + { + private readonly ITestOutputHelper _output; + + public Log(ITestOutputHelper output) + { + _output = output; + Reset(); + } + + public int ErrorsLogged { get; set; } + public int WarningsLogged { get; set; } + + public void LogError(string message, params object[] messageArgs) + { + ErrorsLogged++; + _output.WriteLine("Error: " + message, messageArgs); + } + + public void LogMessage(string message, params object[] messageArgs) + { + _output.WriteLine(message, messageArgs); + } + + public void LogMessage(LogImportance importance, string message, params object[] messageArgs) + { + _output.WriteLine(message, messageArgs); + } + + public void LogWarning(string message, params object[] messageArgs) + { + WarningsLogged++; + _output.WriteLine("Warning: " + message, messageArgs); + } + + public void Reset() + { + ErrorsLogged = 0; + WarningsLogged = 0; + } + + public void AssertNoErrorsOrWarnings() + { + Assert.Equal(0, ErrorsLogged); + Assert.Equal(0, WarningsLogged); + } + } +} 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 new file mode 100644 index 00000000000000..cb254a5cb5070b --- /dev/null +++ b/src/libraries/Microsoft.NETCore.Platforms/tests/Microsoft.NETCore.Platforms.Tests.csproj @@ -0,0 +1,24 @@ + + + $(NetCoreAppCurrent);net472 + true + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/libraries/Microsoft.NETCore.Platforms/tests/TestBuildEngine.cs b/src/libraries/Microsoft.NETCore.Platforms/tests/TestBuildEngine.cs new file mode 100644 index 00000000000000..67dfefbe8c8c9b --- /dev/null +++ b/src/libraries/Microsoft.NETCore.Platforms/tests/TestBuildEngine.cs @@ -0,0 +1,56 @@ +// 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 System; +using System.Collections; + +namespace Microsoft.NETCore.Platforms.BuildTasks.Tests +{ + public class TestBuildEngine : IBuildEngine + { + private ILog _log; + + public TestBuildEngine(ILog log) + { + ColumnNumberOfTaskNode = 0; + ContinueOnError = true; + LineNumberOfTaskNode = 0; + ProjectFileOfTaskNode = "test"; + _log = log; + } + + public int ColumnNumberOfTaskNode { get; set; } + + public bool ContinueOnError { get; set; } + + public int LineNumberOfTaskNode { get; set; } + + public string ProjectFileOfTaskNode { get; set; } + + public bool BuildProjectFile(string projectFileName, string[] targetNames, IDictionary globalProperties, IDictionary targetOutputs) + { + throw new NotImplementedException(); + } + + public void LogCustomEvent(CustomBuildEventArgs e) + { + _log.LogMessage(e.Message); + } + + public void LogErrorEvent(BuildErrorEventArgs e) + { + _log.LogError(e.Message); + } + + public void LogMessageEvent(BuildMessageEventArgs e) + { + _log.LogMessage((LogImportance)e.Importance, e.Message); + } + + public void LogWarningEvent(BuildWarningEventArgs e) + { + _log.LogWarning(e.Message); + } + } +}