diff --git a/build-tools/create-packs/Microsoft.NET.Sdk.Android.proj b/build-tools/create-packs/Microsoft.NET.Sdk.Android.proj index 3d5dd76a499..d978402ee20 100644 --- a/build-tools/create-packs/Microsoft.NET.Sdk.Android.proj +++ b/build-tools/create-packs/Microsoft.NET.Sdk.Android.proj @@ -43,9 +43,20 @@ about the various Microsoft.Android workloads. Replacements="@NET_PREVIOUS_VERSION@=$(AndroidNetPreviousVersion)"> + + <_AndroidPlatformSupportFeed>$(MSBuildThisFileDirectory)/../../external/android-platform-support/Feeds/AndroidManifestFeed_d17.12.xml + <_Feed Condition=" Exists($(_AndroidPlatformSupportFeed)) ">$(_AndroidPlatformSupportFeed) + <_Feed Condition=" '$(_Feed)' == '' ">https://aka.ms/AndroidManifestFeed/d17-12 + <_Project>$(MSBuildThisFileDirectory)/../../tools/workload-dependencies/workload-dependencies.csproj + $(OutputPath)workload-manifest\workload-dependencies.json + + + + <_PackageFiles Include="$(WorkloadManifestJsonPath)" PackagePath="data" /> <_PackageFiles Include="$(WorkloadManifestTargetsPath)" PackagePath="data" /> + <_PackageFiles Include="$(WorkloadDependenciesPath)" PackagePath="data" /> diff --git a/tools/workload-dependencies/Program.cs b/tools/workload-dependencies/Program.cs new file mode 100644 index 00000000000..5552067ae7e --- /dev/null +++ b/tools/workload-dependencies/Program.cs @@ -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 { + "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>> { + ["extra"] = CreateExtraPackageEntries, + ["addon"] = CreateAddonPackageEntries, + ["licenses"] = doc => Array.Empty (), + ["jdk"] = doc => Array.Empty (), +}; + +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 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 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 GetSupportedElements (XDocument doc, string element) +{ + if (doc.Root == null) { + return Array.Empty (); + } + 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 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 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 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); + } +} diff --git a/tools/workload-dependencies/workload-dependencies.csproj b/tools/workload-dependencies/workload-dependencies.csproj new file mode 100644 index 00000000000..3f9921bddd8 --- /dev/null +++ b/tools/workload-dependencies/workload-dependencies.csproj @@ -0,0 +1,15 @@ + + + + Exe + net9.0 + release_json + enable + enable + + + + + + +