diff --git a/build-tools/create-packs/Microsoft.NET.Sdk.Android.proj b/build-tools/create-packs/Microsoft.NET.Sdk.Android.proj index 3d5dd76a499..dc761f56110 100644 --- a/build-tools/create-packs/Microsoft.NET.Sdk.Android.proj +++ b/build-tools/create-packs/Microsoft.NET.Sdk.Android.proj @@ -26,6 +26,7 @@ about the various Microsoft.Android workloads. + $(OutputPath)workload-manifest\WorkloadDependencies.json $(OutputPath)workload-manifest\WorkloadManifest.json $(OutputPath)workload-manifest\WorkloadManifest.targets $(AndroidPackVersionLong) @@ -43,9 +44,27 @@ about the various Microsoft.Android workloads. Replacements="@NET_PREVIOUS_VERSION@=$(AndroidNetPreviousVersion)"> + + <_WorkloadDepProp Include="AndroidLatestStablePlatformId=$(AndroidLatestStablePlatformId)" /> + <_WorkloadDepProp Include="AndroidLatestUnstablePlatformId=$(AndroidLatestUnstablePlatformId)" /> + <_WorkloadDepProp Include="DotNetStableTargetFramework=$(DotNetStableTargetFramework)" /> + <_WorkloadDepProp Include="MicrosoftAndroidSdkOutDir=$(MicrosoftAndroidSdkOutDir)" /> + <_WorkloadDepProp Include="MonoOptionsVersion=$(MonoOptionsVersion)" /> + <_WorkloadDepProp Include="NewtonsoftJsonPackageVersion=$(NewtonsoftJsonPackageVersion)" /> + <_WorkloadDepProp Include="WorkloadDependenciesPath=$(WorkloadDependenciesPath)" /> + <_WorkloadDepProp Include="WorkloadVersion=$(WorkloadVersion)" /> + + + + <_PackageFiles Include="$(WorkloadManifestJsonPath)" PackagePath="data" /> <_PackageFiles Include="$(WorkloadManifestTargetsPath)" PackagePath="data" /> + <_PackageFiles Include="$(WorkloadDependenciesPath)" PackagePath="data" /> diff --git a/tools/workload-dependencies/Directory.Build.props b/tools/workload-dependencies/Directory.Build.props new file mode 100644 index 00000000000..f779f1ff252 --- /dev/null +++ b/tools/workload-dependencies/Directory.Build.props @@ -0,0 +1,8 @@ + + + diff --git a/tools/workload-dependencies/Program.cs b/tools/workload-dependencies/Program.cs new file mode 100644 index 00000000000..c61ab3b1d39 --- /dev/null +++ b/tools/workload-dependencies/Program.cs @@ -0,0 +1,371 @@ +using System.Net.Http; +using System.Xml.Linq; + +using Mono.Options; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; + +const string AppName = "workload-dependencies"; + +var help = false; +var feed = (string?) null; +var output = (string?) null; +int Verbosity = 0; +var CmdlineToolsVersion = (string?) null; +var BuildToolsVersion = (string?) null; +var JdkVersion = (string?) null; +var NdkVersion = (string?) null; +var PlatformToolsVersion = (string?) null; +var PlatformVersion = (string?) null; +var PreviewPlatformVersion = (string?) null; +var WorkloadVersion = (string?) null; + +var options = new OptionSet { + "Generate `release.json` from Feed XML file.", + { "i|feed=", + "The {PATH} to the Feed XML file to process.", + v => feed = v }, + { "o|output=", + "The {FILE_PATH} for the JSON output.\nDefault is stdout.", + v => output = v }, + { "build-tools-version=", + "The Android SDK Build-Tools {VERSION} dotnet/android is built against.", + v => BuildToolsVersion = v }, + { "cmdline-tools-version=", + "The Android SDK cmdline-tools {VERSION} dotnet/android is built against.", + v => CmdlineToolsVersion = v }, + { "jdk-version=", + "The JDK {VERSION} dotnet/android is built against.", + v => JdkVersion = v }, + { "ndk-version=", + "The Android NDK {VERSION} dotnet/android is built against.", + v => NdkVersion = v }, + { "platform-tools-version=", + "The Android SDK platform-tools version dotnet/android is built against.", + v => PlatformToolsVersion = v }, + { "platform-version=", + "The stable Android SDK Platform {VERSION} dotnet/android binds.", + v => PlatformVersion = v }, + { "preview-platform-version=", + "The preview Android SDK Platform {VERSION} dotnet/android binds.", + v => PreviewPlatformVersion = v }, + { "workload-version=", + "The {VERSION} of the dotnet/android workload.", + 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>> { + ["build-tool"] = doc => CreatePackageEntries (doc, "build-tool", BuildToolsVersion), + ["emulator"] = doc => CreatePackageEntries (doc, "emulator", null, optional: true), + ["cmdline-tools"] = doc => CreatePackageEntries (doc, "cmdline-tools", CmdlineToolsVersion), + ["ndk"] = doc => CreatePackageEntries (doc, "ndk", NdkVersion, optional: true), + ["platform-tools"] = doc => CreatePackageEntries (doc, "platform-tools", PlatformToolsVersion), + ["platform"] = CreatePlatformPackageEntries, + ["system-image"] = CreateSystemImagePackageEntries, + // ndk +}; + +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 v = new Version (JdkVersion ?? "17.0"); + var start = new Version (v.Major, v.Minor); + var end = new Version (v.Major+1, 0); + var latestRevision = JdkVersion ?? GetLatestRevision (doc, "jdk"); + var contents = new JObject ( + new JProperty ("version", $"[{start},{end})")); + 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 CreatePackageEntries (XDocument doc, string element, string? revision, bool optional = false) +{ + var item = GetElementRevision (doc, element, revision); + if (item == null) { + yield break; + } + var path = item.ReqAttr ("path"); + var reqRev = item.ReqAttr ("revision"); + var sdkPackage = new JObject { + new JProperty ("id", path), + }; + + // special-case platform-tools, which doesn't have a revision + if (!path.Contains (reqRev)) { + sdkPackage.Add (new JProperty ("recommendedVersion", reqRev)); + } + var entry = new JObject { + new JProperty ("desc", item.ReqAttr ("description")), + new JProperty ("sdkPackage", sdkPackage), + new JProperty ("optional", optional.ToString ().ToLowerInvariant ()), + }; + yield return entry; +} + +XElement? GetElementRevision (XDocument doc, string element, string? revision) +{ + Version? reqVersion = revision != null ? new Version (revision) : null;; + Version? maxVersion = null; + XElement? entry = null; + foreach (var e in GetSupportedElements (doc, element)) { + var r = e.ReqAttr ("revision"); + var rv = new Version (r); + if (rv == reqVersion) { + return e; + } + if (rv > maxVersion) { + maxVersion = rv; + entry = e; + } + } + return entry; +} + +IEnumerable CreatePlatformPackageEntries (XDocument doc) +{ + string? reqVersion = PlatformVersion != null + ? $"platforms;{PlatformVersion}" + : null; + string? maxVersion = null; + XElement? entry = null; + foreach (var e in GetSupportedElements (doc, "platform")) { + var path = e.ReqAttr ("path"); + if (path == reqVersion) { + entry = e; + break; + } + if (string.Compare (path, maxVersion) > 0) { + maxVersion = path; + entry = e; + } + } + if (entry == null) { + yield break; + } + var platform = new JObject { + new JProperty ("desc", entry.ReqAttr ("description")), + new JProperty ("sdkPackage", new JObject { + new JProperty ("id", entry.ReqAttr ("path")), + }), + new JProperty ("optional", "false"), + }; + yield return platform; + + string? previewPath = PreviewPlatformVersion != null + ? $"platforms;android-{PreviewPlatformVersion}" + : null; + XElement? previewEntry = doc.Elements ("platform") + .FirstOrDefault (e => e.ReqAttr ("path") == previewPath); + if (PreviewPlatformVersion != null) { + yield return new JObject { + new JProperty ("desc", previewEntry?.ReqAttr ("description") ?? $"Android SDK Platform {PreviewPlatformVersion} (Preview)"), + new JProperty ("sdkPackage", new JObject { + new JProperty ("id", previewPath), + }), + new JProperty ("optional", "true"), + }; + } +} + +IEnumerable CreateSystemImagePackageEntries (XDocument doc) +{ + // path="system-images;android-21;default;armeabi-v7a" + var images = from image in GetSupportedElements (doc, "system-image") + let path = image.ReqAttr ("path") + let parts = path.Split (';') + where parts.Length > 3 + let targetApi = parts [1] + let apiImpl = parts [2] // google_apis or default + let targetAbi = parts [3] + where apiImpl == "google_apis" // prefer google_apis + select new { + Element = image, + Path = path, + TargetApi = targetApi, + ApiImpl = apiImpl, + TargetAbi = targetAbi, + }; + var maxTarget = images.Select (image => image.TargetApi).OrderBy (v => v).Last (); + var maxImages = images.Where (image => image.TargetApi == maxTarget); + + var x64 = maxImages.Where (image => image.TargetAbi == "x86_64").FirstOrDefault (); + var arm64 = maxImages.Where (image => image.TargetAbi == "arm64-v8a").FirstOrDefault (); + if (x64 == null && arm64 == null) { + yield break; + } + + var id = new JObject (); + if (x64 != null) { + id.Add (new JProperty ("win-x64", x64.Path)); + id.Add (new JProperty ("mac-x64", x64.Path)); + id.Add (new JProperty ("linux-x64", x64.Path)); + } + if (arm64 != null) { + id.Add (new JProperty ("mac-arm64", arm64.Path)); + id.Add (new JProperty ("linux-arm64", arm64.Path)); + } + + var entry = new JObject { + new JProperty ("desc", maxImages.First ().Element.ReqAttr ("description")), + new JProperty ("sdkPackage", new JObject { + new JProperty ("id", id), + }), + 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)) { + continue; + } + foreach (var e in creator (doc)) { + packages.Add (e); + } + } + return packages; +} + +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/WorkloadDependencies.proj b/tools/workload-dependencies/WorkloadDependencies.proj new file mode 100644 index 00000000000..e8831e28a87 --- /dev/null +++ b/tools/workload-dependencies/WorkloadDependencies.proj @@ -0,0 +1,85 @@ + + + + netstandard2.0 + + + + + + + + + + + + + + + + + + + + + + <_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)workload-dependencies.csproj + $(OutputPath)workload-manifest\WorkloadDependencies.json + + + + <_WorkloadDeps Include="--project "$(_Project)"" /> + <_WorkloadDeps Include="-p:MonoOptionsVersion=$(MonoOptionsVersion)" /> + <_WorkloadDeps Include="-p:NewtonsoftJsonPackageVersion=$(NewtonsoftJsonPackageVersion)" /> + <_WorkloadDeps Include="--" /> + <_WorkloadDeps Include=""--feed=$(_Feed)"" /> + <_WorkloadDeps Include="-o "$(WorkloadDependenciesPath)"" /> + <_WorkloadDeps Include="--build-tools-version=$(AndroidSdkBuildToolsVersion)" /> + <_WorkloadDeps Include="--cmdline-tools-version=$(AndroidCommandLineToolsVersion)" /> + <_WorkloadDeps Include="--jdk-version=$(JavaSdkVersion)" /> + <_WorkloadDeps Include="--ndk-version=$(AndroidNdkVersion)" /> + <_WorkloadDeps Include="--platform-tools-version=$(AndroidSdkPlatformToolsVersion)" /> + <_WorkloadDeps Include="--platform-version=$(AndroidSdkPlatformVersion)" /> + <_WorkloadDeps + Condition=" '$(AndroidLatestUnstablePlatformId)' != '$(AndroidLatestStablePlatformId)' " + Include="--preview-platform-version=$(AndroidLatestUnstablePlatformId)" + /> + <_WorkloadDeps Include="--workload-version=$(WorkloadVersion)" /> + + + + + + + diff --git a/tools/workload-dependencies/workload-dependencies.csproj b/tools/workload-dependencies/workload-dependencies.csproj new file mode 100644 index 00000000000..13a80e19fc3 --- /dev/null +++ b/tools/workload-dependencies/workload-dependencies.csproj @@ -0,0 +1,15 @@ + + + + Exe + $(DotNetStableTargetFramework) + release_json + enable + enable + + + + + + +