-
Notifications
You must be signed in to change notification settings - Fork 538
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
[build] Generate
workload-dependencies.json
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
Showing
3 changed files
with
316 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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> |