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
+
+
+
+
+
+
+