Skip to content
This repository has been archived by the owner on Jan 11, 2024. It is now read-only.

Commit

Permalink
Add GenerateRuntimeGraph task
Browse files Browse the repository at this point in the history
This task can be used to generate the RID heirarchy of a runtime.json from a simpler set of inputs.
  • Loading branch information
ericstj committed Sep 21, 2017
1 parent 5b80a24 commit e974396
Show file tree
Hide file tree
Showing 4 changed files with 365 additions and 1 deletion.
22 changes: 21 additions & 1 deletion src/Microsoft.DotNet.Build.Tasks.Packaging/src/Extensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,20 @@ public static class Extensions
{
private static readonly NuGetFramework NullFramework = new NuGetFramework("Null,Version=v1.0");

public static string GetString(this ITaskItem taskItem, string metadataName)
{
var metadataValue = taskItem.GetMetadata(metadataName)?.Trim();
return String.IsNullOrEmpty(metadataValue) ? null : metadataValue;
}

public static bool GetBoolean(this ITaskItem taskItem, string metadataName, bool defaultValue = false)
{
bool result = false;
var metadataValue = taskItem.GetMetadata(metadataName);
bool.TryParse(metadataValue, out result);
if (!bool.TryParse(metadataValue, out result))
{
result = defaultValue;
}
return result;
}

Expand Down Expand Up @@ -87,6 +96,17 @@ public static IReadOnlyList<string> GetValueList(this ITaskItem taskItem, string
return null;
}

public static IEnumerable<string> 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<string>();
}

public static IEnumerable<T> NullAsEmpty<T>(this IEnumerable<T> source)
{
if (source == null)
Expand Down
342 changes: 342 additions & 0 deletions src/Microsoft.DotNet.Build.Tasks.Packaging/src/GenerateRuntimeGraph.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,342 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.

using Microsoft.Build.Framework;
using Newtonsoft.Json;
using NuGet.RuntimeModel;
using NuGet.Versioning;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;

namespace Microsoft.DotNet.Build.Tasks.Packaging
{
public class GenerateRuntimeGraph : PackagingTask
{

/// <summary>
/// A set of RuntimeGroups that can be used to generate a runtime graph
/// Identity: the base string for the RID, without version architecture, or qualifiers.
/// Parent: the base string for the parent of this RID. This RID will be imported by the baseRID, architecture-specific,
/// and qualifier-specific RIDs (with the latter two appending appropriate architecture and qualifiers).
/// Versions: A list of strings delimited by semi-colons that represent the versions for this RID.
/// TreatVersionsAsCompatible: Default is true. When true, version-specific RIDs will import the previous
/// version-specific RID in the Versions list, with the first version importing the version-less RID.
/// When false all version-specific RIDs will import the version-less RID (bypassing previous version-specific RIDs)
/// OmitVersionDelimiter: Default is false. When true no characters will separate the base RID and version (EG: win7).
/// When false a '.' will separate the base RID and version (EG: osx.10.12).
/// ApplyVersionsToParent: Default is false. When true, version-specific RIDs will import version-specific Parent RIDs
/// similar to is done for architecture and qualifier (see Parent above).
/// Architectures: A list of strings delimited by semi-colons that represent the architectures for this RID.
/// AdditionalQualifiers: A list of strings delimited by semi-colons that represent the additional qualifiers for this RID.
/// Additional qualifers do not stack, each only applies to the qualifier-less RIDs (so as not to cause combinatorial
/// exponential growth of RIDs).
/// OmitRIDs: A list of strings delimited by semi-colons that represent RIDs calculated from this RuntimeGroup that should
/// be omitted from the RuntimeGraph. This is useful in cases where overlapping RuntimeGroups are used and one is
/// designated to be "best" for some set of calculated RIDs, the others may omit the overlapping RIDs.
/// </summary>
[Required]
public ITaskItem[] RuntimeGroups
{
get;
set;
}

/// <summary>
/// A set of QualifierPolicies that can be used to define the ordering of qualifer-specific RIDs in imports.
/// Identity: the qualifier name. This should match what is used in RuntimeGroups.AdditionalQualifiers
/// VersionPrecedence: default is false. When set to true, a qualifier-specific RID should import the
/// qualifier-less RID before any other RIDs that are not of the same version. When false, the qualifier-
/// less RID should be imported after all qualifier specific RIDs.
///
/// Given the RID of foo.1.0-x64 : [ foo.1.0, foo.0.8-x64 ]
/// Applying the qualifier "bar" the following is the result for each value of VersionPrecedence:
/// True: foo.1.0-x64-bar : [ foo.1.0-bar, foo.1.0-x64, foo.0.8-x64-bar ]
/// False: foo.1.0-x64-bar : [ foo.1.0-bar, foo.0.8-x64-bar, foo.1.0-x64 ]
/// </summary>
public ITaskItem[] QualifierPolicies
{
get;
set;
}


/// <summary>
/// Optional source Runtime.json to use as a starting point when merging additional RuntimeGroups
/// </summary>
public ITaskItem SourceRuntimeJson
{
get;
set;
}

/// <summary>
/// Where to write the final runtime.json
/// </summary>
[Required]
public ITaskItem RuntimeJson
{
get;
set;
}

private Dictionary<string, QualifierPolicy> qualifierPolicies = new Dictionary<string, QualifierPolicy>();

public override bool Execute()
{

if (RuntimeJson == null)
{
Log.LogError($"{nameof(RuntimeJson)} argument must be specified");
return false;
}

RuntimeGraph runtimeGraph;
if (SourceRuntimeJson != null)
{
var sourceRuntimeFilePath = SourceRuntimeJson.GetMetadata("FullPath");

if (!File.Exists(sourceRuntimeFilePath))
{
Log.LogError($"{nameof(SourceRuntimeJson)} did not exist at {sourceRuntimeFilePath}.");
return false;
}

runtimeGraph = JsonRuntimeFormat.ReadRuntimeGraph(sourceRuntimeFilePath);
}
else
{
runtimeGraph = new RuntimeGraph();
}

foreach(var qualifierPolicy in QualifierPolicies.NullAsEmpty().Select(i => new QualifierPolicy(i)))
{
qualifierPolicies[qualifierPolicy.Qualifier] = qualifierPolicy;
}

foreach(var runtimeGroup in RuntimeGroups.NullAsEmpty().Select(i => new RuntimeGroup(i)))
{
runtimeGraph = SafeMerge(runtimeGraph, runtimeGroup);
}

ValidateImports(runtimeGraph);

JsonRuntimeFormat.WriteRuntimeGraph(RuntimeJson.ItemSpec, runtimeGraph);

return !Log.HasLoggedErrors;
}


private RuntimeGraph SafeMerge(RuntimeGraph existingGraph, RuntimeGroup runtimeGroup)
{
var runtimeGraph = runtimeGroup.GetRuntimeGraph(qualifierPolicies);

foreach (var existingRuntimeDescription in existingGraph.Runtimes.Values)
{
RuntimeDescription newRuntimeDescription;

if (runtimeGraph.Runtimes.TryGetValue(existingRuntimeDescription.RuntimeIdentifier, out newRuntimeDescription))
{
// overlapping RID, ensure that the imports match (same ordering and content)
if (!existingRuntimeDescription.InheritedRuntimes.SequenceEqual(newRuntimeDescription.InheritedRuntimes))
{
Log.LogError($"RuntimeGroup {runtimeGroup.BaseRID} defines RID {newRuntimeDescription.RuntimeIdentifier} with imports {String.Join(";", newRuntimeDescription.InheritedRuntimes)} which differ from existing imports {String.Join(";", existingRuntimeDescription.InheritedRuntimes)}. You may avoid this by specifying OmitRIDs metadata with {newRuntimeDescription.RuntimeIdentifier}.");
}
}
}

return RuntimeGraph.Merge(existingGraph, runtimeGraph);
}

private void ValidateImports(RuntimeGraph runtimeGraph)
{
foreach(var runtimeDescription in runtimeGraph.Runtimes.Values)
{
foreach(var import in runtimeDescription.InheritedRuntimes)
{
if (!runtimeGraph.Runtimes.ContainsKey(import))
{
Log.LogError($"Runtime {runtimeDescription.RuntimeIdentifier} imports {import} which is not defined.");
}
}
}
}

class QualifierPolicy
{
public QualifierPolicy(ITaskItem item)
{
Qualifier = item.ItemSpec;
VersionPrecedence = item.GetBoolean(nameof(VersionPrecedence));
}

public string Qualifier { get; }
public bool VersionPrecedence { get; }
}

class RuntimeGroup
{
private const string rootRID = "any";
private const char VersionDelimiter = '.';
private const char ArchitectureDelimiter = '-';
private const char QualifierDelimiter = '-';

public RuntimeGroup(ITaskItem item)
{
BaseRID = item.ItemSpec;
Parent = item.GetString(nameof(Parent));
Versions = item.GetStrings(nameof(Versions));
TreatVersionsAsCompatible = item.GetBoolean(nameof(TreatVersionsAsCompatible), true);
OmitVersionDelimiter = item.GetBoolean(nameof(OmitVersionDelimiter));
ApplyVersionsToParent = item.GetBoolean(nameof(ApplyVersionsToParent));
Architectures = item.GetStrings(nameof(Architectures));
AdditionalQualifiers = item.GetStrings(nameof(AdditionalQualifiers));
OmitRIDs = new HashSet<string>(item.GetStrings(nameof(OmitRIDs)));
}

public string BaseRID { get; }
public string Parent { get; }
public IEnumerable<string> Versions { get; }
public bool TreatVersionsAsCompatible { get; }
public bool OmitVersionDelimiter { get; }
public bool ApplyVersionsToParent { get; }
public IEnumerable<string> Architectures { get; }
public IEnumerable<string> AdditionalQualifiers { get; }
public ICollection<string> OmitRIDs { get; }

public IEnumerable<RuntimeDescription> GetRuntimeDescriptions()
{
// define the base as importing the parent
yield return new RuntimeDescription(BaseRID, Parent == null ? Enumerable.Empty<string>() : new[] { Parent });

// define each arch as importing base and parent-arch
foreach(var architecture in Architectures)
{
var imports = new List<string>();
imports.Add(BaseRID);

if (Parent != null && Parent != rootRID)
{
imports.Add($"{Parent}-{architecture}");
}

yield return new RuntimeDescription($"{BaseRID}-{architecture}", imports);
}

var versionDelimiter = OmitVersionDelimiter ? String.Empty : VersionDelimiter.ToString();
string lastVersion = null;
foreach (var version in Versions)
{
var imports = new List<string>();

// define each version as importing the version-less base or previous version
if ((lastVersion == null) || !TreatVersionsAsCompatible)
{
imports.Add(BaseRID);
}
else
{
imports.Add($"{BaseRID}{versionDelimiter}{lastVersion}");
}

if (ApplyVersionsToParent)
{
imports.Add($"{Parent}{versionDelimiter}{version}");
}

yield return new RuntimeDescription($"{BaseRID}{versionDelimiter}{version}", imports);

foreach (var architecture in Architectures)
{
// define each arch-specific version as importing the versioned base and either a previous version arch-specific RID or the base arch-specific RID
var archImports = new List<string>();

archImports.Add($"{BaseRID}{versionDelimiter}{version}");

if ((lastVersion == null) || !TreatVersionsAsCompatible)
{
archImports.Add($"{BaseRID}{ArchitectureDelimiter}{architecture}");
}
else
{
archImports.Add($"{BaseRID}{versionDelimiter}{lastVersion}{ArchitectureDelimiter}{architecture}");
}

if (ApplyVersionsToParent)
{
archImports.Add($"{Parent}{versionDelimiter}{version}{ArchitectureDelimiter}{architecture}");
}


yield return new RuntimeDescription($"{BaseRID}{versionDelimiter}{version}{ArchitectureDelimiter}{architecture}", archImports);
}

lastVersion = version;
}
}

private IEnumerable<RuntimeDescription> AddQualifiers(IEnumerable<RuntimeDescription> unQualifiedruntimeDescriptions, Dictionary<string, QualifierPolicy> qualifierPolicies)
{
foreach(var runtimeDescription in unQualifiedruntimeDescriptions)
{
yield return runtimeDescription;

foreach(var qualifier in AdditionalQualifiers)
{
var imports = new List<string>(runtimeDescription.InheritedRuntimes
.NullAsEmpty()
.Select(rid => rid == null || rid == rootRID ? qualifier : $"{rid}-{qualifier}"));


QualifierPolicy qualifierPolicy = null;

if (qualifierPolicies.TryGetValue(qualifier, out qualifierPolicy) && qualifierPolicy.VersionPrecedence)
{
var versionedRid = runtimeDescription.RuntimeIdentifier;

var archIndex = versionedRid.IndexOf('-');

if (archIndex != -1)
{
versionedRid = versionedRid.Substring(0, archIndex);
}

int insertAt = 0;


while (insertAt < imports.Count && imports[insertAt].StartsWith(versionedRid))
{
insertAt++;
}

imports.Insert(insertAt, runtimeDescription.RuntimeIdentifier);
}
else
{
imports.Add(runtimeDescription.RuntimeIdentifier);
}


yield return new RuntimeDescription($"{runtimeDescription.RuntimeIdentifier}{QualifierDelimiter}{qualifier}", imports);

}
}
}

public RuntimeGraph GetRuntimeGraph(Dictionary<string, QualifierPolicy> qualifierPolicies)
{
var rids = GetRuntimeDescriptions();

rids = rids.Where(rid => !OmitRIDs.Contains(rid.RuntimeIdentifier));

rids = AddQualifiers(rids, qualifierPolicies);

rids = rids.Where(rid => !OmitRIDs.Contains(rid.RuntimeIdentifier));

return new RuntimeGraph(rids);
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
</PropertyGroup>
<ItemGroup>
<Compile Include="ApplyMetaPackages.cs" />
<Compile Include="GenerateRuntimeGraph.cs" />
<Compile Include="GetSupportedPackagesFromPackageReports.cs" />
<Compile Include="GetLayoutFiles.cs" />
<Compile Include="FilterUnknownPackages.cs" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
<UsingTask TaskName="FilterUnknownPackages" AssemblyFile="$(PackagingTaskDir)Microsoft.DotNet.Build.Tasks.Packaging.dll"/>
<UsingTask TaskName="GenerateNuSpec" AssemblyFile="$(PackagingTaskDir)Microsoft.DotNet.Build.Tasks.Packaging.dll"/>
<UsingTask TaskName="GeneratePackageReport" AssemblyFile="$(PackagingTaskDir)Microsoft.DotNet.Build.Tasks.Packaging.dll"/>
<UsingTask TaskName="GenerateRuntimeGraph" AssemblyFile="$(PackagingTaskDir)Microsoft.DotNet.Build.Tasks.Packaging.dll"/>
<UsingTask TaskName="GenerateRuntimeDependencies" AssemblyFile="$(PackagingTaskDir)Microsoft.DotNet.Build.Tasks.Packaging.dll"/>
<UsingTask TaskName="GetApplicableAssetsFromPackages" AssemblyFile="$(PackagingTaskDir)Microsoft.DotNet.Build.Tasks.Packaging.dll"/>
<UsingTask TaskName="GetAssemblyReferences" AssemblyFile="$(PackagingTaskDir)Microsoft.DotNet.Build.Tasks.Packaging.dll"/>
Expand Down

0 comments on commit e974396

Please sign in to comment.