Skip to content

Commit

Permalink
[build] Generate workload-dependencies.json
Browse files Browse the repository at this point in the history
Context: [`Releases.json` loop][0]
Context: dotnet/macios#21779 (comment)

There is a desire to have the .NET Workloads have a machine readable
description of what dependencies they require in order to run, in
order to facilitate tooling that would check for these dependencies.

Add `tools/workload-dependencies`, a new tool which parses a
"Xamarin Manifest" to generate `workload-dependencies.json`.
The "canonical" location for the "Xamarin Manifest" is within
`external/android-platform-support/Feeds/AndroidManifestFeed_d17.12.xml`;
failing that, <https://aka.ms/AndroidManifestFeed/d17-12> can be used.

Output of the tool is a JSON document specifying ther required JDK
and Android SDK packages which the .NET for Android workload requires:

	{
	  "microsoft.net.sdk.android": {
	    "workload": {
	      "alias": [
	        "android"
	      ],
	      "version": "35.0.100"
	    },
	    "jdk": {
	      "version": "[17.0,18.0)",
	      "recommendedVersion": "17.0.12"
	    },
	    "androidsdk": {
	      "packages": [
	        {
	          "desc": "Android SDK Build-Tools 35",
	          "sdkPackage": {
	            "id": "build-tools;*",
	            "version": "[30.0.2,30.0.3,31.0.0,32.0.0,33.0.0,33.0.1,33.0.2,33.0.3,34.0.0,35.0.0]",
	            "recommendedId": "build-tools;35.0.0",
	            "recommendedVersion": "35.0.0"
	          },
	          "optional": "false"
	        },
	        {
	          "desc": "Android SDK Command-line Tools",
	          "sdkPackage": {
	            "id": "cmdline-tools;*",
	            "version": "[5.0,6.0,7.0,8.0,9.0,10.0,11.0,12.0,13.0]",
	            "recommendedId": "cmdline-tools;13.0",
	            "recommendedVersion": "13.0"
	          },
	          "optional": "false"
	        },
	        {
	          "desc": "Android SDK Platform 28",
	          "sdkPackage": {
	            "id": "platforms;android-*",
	            "version": "[1,1,1,2,2,2,2,3,3,3,3,3,3,5,6]",
	            "recommendedId": "platforms;android-28",
	            "recommendedVersion": "6"
	          },
	          "optional": "false"
	        },
	        {
	          "desc": "Android SDK Platform-Tools",
	          "sdkPackage": {
	            "id": "platform-tools",
	            "version": "[33.0.2,33.0.3,34.0.1,34.0.3,34.0.4,34.0.5,35.0.1,35.0.2]",
	            "recommendedId": "platform-tools",
	            "recommendedVersion": "35.0.2"
	          },
	          "optional": "false"
	        },
	        …
	      ]
	    }
	  }
	}

[0]: https://loop.cloud.microsoft/p/eyJ1IjoiaHR0cHM6Ly9taWNyb3NvZnQuc2hhcmVwb2ludC1kZi5jb20vc2l0ZXMvYzIyZmVjMDMtN2I4OS00OTJhLTgzNzQtZmZjMTI4YjMwMWRhP25hdj1jejBsTWtaemFYUmxjeVV5Um1NeU1tWmxZekF6TFRkaU9Ea3RORGt5WVMwNE16YzBMV1ptWXpFeU9HSXpNREZrWVNaa1BXSWxNakZXTUhSeU9XY3dRbk5WYlhVdFJWUjNRVEZNY0dOSmQwdG1VVEZUZFVFeFRuRk5XbVZ3TUhVd1dUaEhkVVpKVlRSUGIxWnlVMWxoZFRaT2RFODRTamhISm1ZOU1ERlhSelkwU0RNMU56TlZRbEpITWs1TU1rSkdTemRZV1ZCWFJqSlNTRVJQVENaalBTVXlSaVpoUFV4dmIzQkJjSEFtY0QwbE5EQm1iSFZwWkhnbE1rWnNiMjl3TFhCaFoyVXRZMjl1ZEdGcGJtVnlKbmc5SlRkQ0pUSXlkeVV5TWlVelFTVXlNbFF3VWxSVlNIaDBZVmRPZVdJelRuWmFibEYxWXpKb2FHTnRWbmRpTW14MVpFTXhhMXBwTldwaU1qRTRXV2xHVjAxSVVubFBWMk4zVVc1T1ZtSllWWFJTVmxJelVWUkdUV05IVGtwa01IUnRWVlJHVkdSVlJYaFVia1pPVjIxV2QwMUlWWGRYVkdoSVpGVmFTbFpVVWxCaU1WcDVWVEZzYUdSVVdrOWtSVGcwVTJwb1NHWkVRWGhXTUdNeVRrVm5lazU2VGtwVU1WRXdWMnQwUmxGNldrdFRSbXhaVlRCYVVWUlZhRlJXUlVaRFYyeEZKVE5FSlRJeUpUSkRKVEl5YVNVeU1pVXpRU1V5TWprNE5HUXhZMlpoTFRnMVpXUXROR1kyWXkxaU9EWmlMVFJtTXpZMU1EQXlaak5tTnlVeU1pVTNSQT09In0%3D?ct=1728330820992&
  • Loading branch information
jonpryor committed Dec 12, 2024
1 parent 68da9e9 commit 6c1525c
Show file tree
Hide file tree
Showing 3 changed files with 316 additions and 0 deletions.
11 changes: 11 additions & 0 deletions build-tools/create-packs/Microsoft.NET.Sdk.Android.proj
Original file line number Diff line number Diff line change
Expand Up @@ -43,9 +43,20 @@ about the various Microsoft.Android workloads.
Replacements="@NET_PREVIOUS_VERSION@=$(AndroidNetPreviousVersion)">
</ReplaceFileContents>

<PropertyGroup>
<_AndroidPlatformSupportFeed>$(MSBuildThisFileDirectory)/../../external/android-platform-support/Feeds/AndroidManifestFeed_d17.12.xml</_AndroidPlatformSupportFeed>
<_Feed Condition=" Exists($(_AndroidPlatformSupportFeed)) ">$(_AndroidPlatformSupportFeed)</_Feed>
<_Feed Condition=" '$(_Feed)' == '' ">https://aka.ms/AndroidManifestFeed/d17-12</_Feed>
<_Project>$(MSBuildThisFileDirectory)/../../tools/workload-dependencies/workload-dependencies.csproj</_Project>
<WorkloadDependenciesPath Condition="'$(WorkloadDependenciesPath)' == ''">$(OutputPath)workload-manifest\workload-dependencies.json</WorkloadDependenciesPath>
</PropertyGroup>

<Exec Command="dotnet run --project &quot;$(_Project)&quot; -- &quot;--feed=$(_Feed)&quot; --workload-version=$(WorkloadVersion) -o &quot;$(WorkloadDependenciesPath)&quot;" />

<ItemGroup>
<_PackageFiles Include="$(WorkloadManifestJsonPath)" PackagePath="data" />
<_PackageFiles Include="$(WorkloadManifestTargetsPath)" PackagePath="data" />
<_PackageFiles Include="$(WorkloadDependenciesPath)" PackagePath="data" />
</ItemGroup>
</Target>

Expand Down
290 changes: 290 additions & 0 deletions tools/workload-dependencies/Program.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,290 @@
using System.Net.Http;
using System.Xml.Linq;

using Mono.Options;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;

const string AppName = "release-json";

var RequiredPackages = new HashSet<string> {
"platform-tools",
"cmdline-tools",
"build-tool",
"platform",
};

var help = false;
var feed = (string?) null;
var output = (string?) null;
int verbosity = 0;
var workloadVersion = (string?) null;

var options = new OptionSet {
"Generate `release.json` from Feed XML file.",
{ "i|feed=",
"The {PATH} to the Feed XML file.",
v => feed = v },
{ "o|output=",
"The {PATH} to the output release.json file.",
v => output = v },
{ "workload-version=",
"The {VERSION} of the workload to generate.",
v => workloadVersion = v },
{ "v|verbose:",
"Set internal message verbosity",
(int? v) => verbosity = v.HasValue ? v.Value : verbosity + 1 },
{ "h|help",
"Show this help message and exit",
v => help = v != null },
};

XDocument doc;

try {
options.Parse (args);

if (help) {
options.WriteOptionDescriptions (Console.Out);
return;
}

if (string.IsNullOrEmpty (feed)) {
Console.Error.WriteLine ($"{AppName}: --feed is required.");
Console.Error.WriteLine ($"{AppName}: Use --help for more information.");
return;
}
doc = XDocument.Parse (await GetFeedContents (feed));
if (doc.Root == null) {
throw new InvalidOperationException ("Missing root element in XML feed.");
}
}
catch (OptionException e) {
Console.Error.WriteLine ($"{AppName}: {e.Message}");
if (verbosity > 0) {
Console.Error.WriteLine (e.ToString ());
}
return;
}
catch (System.Xml.XmlException e) {
Console.Error.WriteLine ($"{AppName}: invalid `--feed=PATH` value. {e.Message}");
if (verbosity > 0) {
Console.Error.WriteLine (e.ToString ());
}
return;
}

var PackageCreators = new Dictionary<string, Func<XDocument, IEnumerable<JObject>>> {
["extra"] = CreateExtraPackageEntries,
["addon"] = CreateAddonPackageEntries,
["licenses"] = doc => Array.Empty<JObject> (),
["jdk"] = doc => Array.Empty<JObject> (),
};

var release = new JObject {
new JProperty ("microsoft.net.sdk.android", new JObject {
CreateWorkloadProperty (doc),
CreateJdkProperty (doc),
new JProperty ("androidsdk", new JObject {
new JProperty ("packages", CreatePackagesArray (doc)),
}),
}),
};

using var writer = CreateWriter ();
release.WriteTo (writer);
writer.Flush ();

async Task<string> GetFeedContents (string feed)
{
if (File.Exists (feed)) {
return File.ReadAllText (feed);
}
if (Uri.TryCreate (feed, UriKind.Absolute, out var uri)) {
return await GetFeedContentsFromUri (uri);
}
throw new NotSupportedException ($"Don't know what to do with --feed={feed}");
}

async Task<string> GetFeedContentsFromUri (Uri feed)
{
using var client = new HttpClient ();
var response = await client.GetAsync (feed);
return await response.Content.ReadAsStringAsync ();
}

JsonWriter CreateWriter ()
{
var w = string.IsNullOrEmpty (output)
? new JsonTextWriter (Console.Out) { CloseOutput = false}
: new JsonTextWriter (File.CreateText (output)) { CloseOutput = true };
w.Formatting = Formatting.Indented;
return w;
}

JProperty CreateWorkloadProperty (XDocument doc)
{
var contents = new JObject (
new JProperty ("alias", new JArray ("android")));
if (!string.IsNullOrEmpty (workloadVersion))
contents.Add (new JProperty ("version", workloadVersion));
return new JProperty ("workload", contents);
}

JProperty CreateJdkProperty (XDocument doc)
{
var latestRevision = GetLatestRevision (doc, "jdk");
var contents = new JObject (
new JProperty ("version", "[17.0,18.0)"));
if (!string.IsNullOrEmpty (latestRevision))
contents.Add (new JProperty ("recommendedVersion", latestRevision));
return new JProperty ("jdk", contents);
}

IEnumerable<XElement> GetSupportedElements (XDocument doc, string element)
{
if (doc.Root == null) {
return Array.Empty<XElement> ();
}
return doc.Root.Elements (element)
.Where (e =>
string.Equals ("False", e.ReqAttr ("obsolete"), StringComparison.OrdinalIgnoreCase) &&
string.Equals ("False", e.ReqAttr ("preview"), StringComparison.OrdinalIgnoreCase));
}

IEnumerable<(XElement Element, string Revision)> GetByRevisions (XDocument doc, string element)
{
return GetSupportedElements (doc, element)
.OrderByRevision ();
}

string? GetLatestRevision (XDocument doc, string element)
{
return GetByRevisions (doc, element)
.LastOrDefault ()
.Revision;
}

IEnumerable<JObject> CreateExtraPackageEntries (XDocument doc)
{
var allExtras = GetByRevisions (doc, "extra").ToList ();
var paths = allExtras
.Select (e => e.Element.ReqAttr ("path"))
.Distinct ();
foreach (var path in paths) {
var extras = allExtras
.Where (e => e.Element.ReqAttr ("path") == path);
var version = string.Join (",", extras.Select (e => e.Revision));
var latest = extras.Last ();
var entry = new JObject {
new JProperty ("desc", latest.Element.ReqAttr ("description")),
new JProperty ("sdkPackage", new JObject {
new JProperty ("id", path),
new JProperty ("version", "[" + version + "]"),
new JProperty ("recommendedId", latest.Element.ReqAttr ("path")),
new JProperty ("recommendedVersion", latest.Revision),
}),
new JProperty ("optional", "true"),
};
yield return entry;
}
}

IEnumerable<JObject> CreateAddonPackageEntries (XDocument doc)
{
var allAddons = GetSupportedElements (doc, "addon").ToList ()
.OrderBy (e => e.ReqAttr ("path"));
var paths = allAddons
.Select (e => GetEntryId (e))
.Distinct ();
foreach (var path in paths) {
var addons = allAddons
.Where (e => GetEntryId (e) == path);
var version = string.Join (",", addons.Select (e => e.ReqAttr ("revision")));
var latest = addons.Last ();
var entry = new JObject {
new JProperty ("desc", latest.ReqAttr ("description")),
new JProperty ("sdkPackage", new JObject {
new JProperty ("id", path),
new JProperty ("version", "[" + version + "]"),
new JProperty ("recommendedId", latest.ReqAttr ("path")),
new JProperty ("recommendedVersion", latest.ReqAttr ("revision")),
}),
new JProperty ("optional", "true"),
};
yield return entry;
}
}

JArray CreatePackagesArray (XDocument doc)
{
var packages = new JArray ();
var names = doc.Root!.Elements ()
.Select (e => e.Name.LocalName)
.Distinct ()
.OrderBy (e => e);
foreach (var name in names) {
if (PackageCreators.TryGetValue (name, out var creator)) {
foreach (var e in creator (doc)) {
packages.Add (e);
}
continue;
}
var items = GetSupportedElements (doc, name)
.OrderBy (e => e.ReqAttr ("path"));
if (!items.Any ()) {
continue;
}
var version = string.Join (",", items.Select (e => e.ReqAttr ("revision")));
var latest = items.Last ();

var entry = new JObject {
new JProperty ("desc", latest.ReqAttr ("description")),
new JProperty ("sdkPackage", new JObject {
new JProperty ("id", GetEntryId (latest)),
new JProperty ("version", "[" + version + "]"),
new JProperty ("recommendedId", latest.ReqAttr ("path")),
new JProperty ("recommendedVersion", latest.ReqAttr ("revision")),
}),
new JProperty ("optional", (!RequiredPackages.Contains (name)).ToString ().ToLowerInvariant ()),
};

packages.Add (entry);
}
return packages;
}

string GetEntryId (XElement entry)
{
var path = entry.ReqAttr ("path");
var semic = path.LastIndexOf (';');
if (semic < 0) {
return path;
}
var hyphen = path.LastIndexOf ('-');
if (hyphen < 0) {
return path.Substring (0, semic+1) + "*";
}
return path.Substring (0, Math.Max (hyphen, semic)+1) + "*";
}

static class Extensions
{
public static string ReqAttr (this XElement e, string attribute)
{
var v = (string?) e.Attribute (attribute);
if (v == null) {
throw new InvalidOperationException ($"Missing required attribute `{attribute}` in: `{e}");
}
return v;
}

public static IEnumerable<(XElement Element, string Revision)> OrderByRevision (this IEnumerable<XElement> elements)
{
return from e in elements
let revision = e.ReqAttr ("revision")
let version = new Version (revision.Contains (".") ? revision : revision + ".0")
orderby version
select (e, revision);
}
}
15 changes: 15 additions & 0 deletions tools/workload-dependencies/workload-dependencies.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net9.0</TargetFramework>
<RootNamespace>release_json</RootNamespace>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Mono.Options" Version="$(MonoOptionsVersion)" />
<PackageReference Include="Newtonsoft.Json" Version="$(NewtonsoftJsonPackageVersion)" />
</ItemGroup>
</Project>

0 comments on commit 6c1525c

Please sign in to comment.