|
| 1 | +using System.Net.Http; |
| 2 | +using System.Xml.Linq; |
| 3 | + |
| 4 | +using Mono.Options; |
| 5 | +using Newtonsoft.Json; |
| 6 | +using Newtonsoft.Json.Linq; |
| 7 | + |
| 8 | +const string AppName = "release-json"; |
| 9 | + |
| 10 | +var RequiredPackages = new HashSet<string> { |
| 11 | + "platform-tools", |
| 12 | + "cmdline-tools", |
| 13 | + "build-tool", |
| 14 | + "platform", |
| 15 | +}; |
| 16 | + |
| 17 | +var help = false; |
| 18 | +var feed = (string?) null; |
| 19 | +var output = (string?) null; |
| 20 | +int verbosity = 0; |
| 21 | +var workloadVersion = (string?) null; |
| 22 | + |
| 23 | +var options = new OptionSet { |
| 24 | + "Generate `release.json` from Feed XML file.", |
| 25 | + { "i|feed=", |
| 26 | + "The {PATH} to the Feed XML file.", |
| 27 | + v => feed = v }, |
| 28 | + { "o|output=", |
| 29 | + "The {PATH} to the output release.json file.", |
| 30 | + v => output = v }, |
| 31 | + { "workload-version=", |
| 32 | + "The {VERSION} of the workload to generate.", |
| 33 | + v => workloadVersion = v }, |
| 34 | + { "v|verbose:", |
| 35 | + "Set internal message verbosity", |
| 36 | + (int? v) => verbosity = v.HasValue ? v.Value : verbosity + 1 }, |
| 37 | + { "h|help", |
| 38 | + "Show this help message and exit", |
| 39 | + v => help = v != null }, |
| 40 | +}; |
| 41 | + |
| 42 | +XDocument doc; |
| 43 | + |
| 44 | +try { |
| 45 | + options.Parse (args); |
| 46 | + |
| 47 | + if (help) { |
| 48 | + options.WriteOptionDescriptions (Console.Out); |
| 49 | + return; |
| 50 | + } |
| 51 | + |
| 52 | + if (string.IsNullOrEmpty (feed)) { |
| 53 | + Console.Error.WriteLine ($"{AppName}: --feed is required."); |
| 54 | + Console.Error.WriteLine ($"{AppName}: Use --help for more information."); |
| 55 | + return; |
| 56 | + } |
| 57 | + doc = XDocument.Parse (await GetFeedContents (feed)); |
| 58 | + if (doc.Root == null) { |
| 59 | + throw new InvalidOperationException ("Missing root element in XML feed."); |
| 60 | + } |
| 61 | +} |
| 62 | +catch (OptionException e) { |
| 63 | + Console.Error.WriteLine ($"{AppName}: {e.Message}"); |
| 64 | + if (verbosity > 0) { |
| 65 | + Console.Error.WriteLine (e.ToString ()); |
| 66 | + } |
| 67 | + return; |
| 68 | +} |
| 69 | +catch (System.Xml.XmlException e) { |
| 70 | + Console.Error.WriteLine ($"{AppName}: invalid `--feed=PATH` value. {e.Message}"); |
| 71 | + if (verbosity > 0) { |
| 72 | + Console.Error.WriteLine (e.ToString ()); |
| 73 | + } |
| 74 | + return; |
| 75 | +} |
| 76 | + |
| 77 | +var PackageCreators = new Dictionary<string, Func<XDocument, IEnumerable<JObject>>> { |
| 78 | + ["extra"] = CreateExtraPackageEntries, |
| 79 | + ["addon"] = CreateAddonPackageEntries, |
| 80 | + ["licenses"] = doc => Array.Empty<JObject> (), |
| 81 | + ["jdk"] = doc => Array.Empty<JObject> (), |
| 82 | +}; |
| 83 | + |
| 84 | +var release = new JObject { |
| 85 | + new JProperty ("microsoft.net.sdk.android", new JObject { |
| 86 | + CreateWorkloadProperty (doc), |
| 87 | + CreateJdkProperty (doc), |
| 88 | + new JProperty ("androidsdk", new JObject { |
| 89 | + new JProperty ("packages", CreatePackagesArray (doc)), |
| 90 | + }), |
| 91 | + }), |
| 92 | +}; |
| 93 | + |
| 94 | +using var writer = CreateWriter (); |
| 95 | +release.WriteTo (writer); |
| 96 | +writer.Flush (); |
| 97 | + |
| 98 | +async Task<string> GetFeedContents (string feed) |
| 99 | +{ |
| 100 | + if (File.Exists (feed)) { |
| 101 | + return File.ReadAllText (feed); |
| 102 | + } |
| 103 | + if (Uri.TryCreate (feed, UriKind.Absolute, out var uri)) { |
| 104 | + return await GetFeedContentsFromUri (uri); |
| 105 | + } |
| 106 | + throw new NotSupportedException ($"Don't know what to do with --feed={feed}"); |
| 107 | +} |
| 108 | + |
| 109 | +async Task<string> GetFeedContentsFromUri (Uri feed) |
| 110 | +{ |
| 111 | + using var client = new HttpClient (); |
| 112 | + var response = await client.GetAsync (feed); |
| 113 | + return await response.Content.ReadAsStringAsync (); |
| 114 | +} |
| 115 | + |
| 116 | +JsonWriter CreateWriter () |
| 117 | +{ |
| 118 | + var w = string.IsNullOrEmpty (output) |
| 119 | + ? new JsonTextWriter (Console.Out) { CloseOutput = false} |
| 120 | + : new JsonTextWriter (File.CreateText (output)) { CloseOutput = true }; |
| 121 | + w.Formatting = Formatting.Indented; |
| 122 | + return w; |
| 123 | +} |
| 124 | + |
| 125 | +JProperty CreateWorkloadProperty (XDocument doc) |
| 126 | +{ |
| 127 | + var contents = new JObject ( |
| 128 | + new JProperty ("alias", new JArray ("android"))); |
| 129 | + if (!string.IsNullOrEmpty (workloadVersion)) |
| 130 | + contents.Add (new JProperty ("version", workloadVersion)); |
| 131 | + return new JProperty ("workload", contents); |
| 132 | +} |
| 133 | + |
| 134 | +JProperty CreateJdkProperty (XDocument doc) |
| 135 | +{ |
| 136 | + var latestRevision = GetLatestRevision (doc, "jdk"); |
| 137 | + var contents = new JObject ( |
| 138 | + new JProperty ("version", "[17.0,18.0)")); |
| 139 | + if (!string.IsNullOrEmpty (latestRevision)) |
| 140 | + contents.Add (new JProperty ("recommendedVersion", latestRevision)); |
| 141 | + return new JProperty ("jdk", contents); |
| 142 | +} |
| 143 | + |
| 144 | +IEnumerable<XElement> GetSupportedElements (XDocument doc, string element) |
| 145 | +{ |
| 146 | + if (doc.Root == null) { |
| 147 | + return Array.Empty<XElement> (); |
| 148 | + } |
| 149 | + return doc.Root.Elements (element) |
| 150 | + .Where (e => |
| 151 | + string.Equals ("False", e.ReqAttr ("obsolete"), StringComparison.OrdinalIgnoreCase) && |
| 152 | + string.Equals ("False", e.ReqAttr ("preview"), StringComparison.OrdinalIgnoreCase)); |
| 153 | +} |
| 154 | + |
| 155 | +IEnumerable<(XElement Element, string Revision)> GetByRevisions (XDocument doc, string element) |
| 156 | +{ |
| 157 | + return GetSupportedElements (doc, element) |
| 158 | + .OrderByRevision (); |
| 159 | +} |
| 160 | + |
| 161 | +string? GetLatestRevision (XDocument doc, string element) |
| 162 | +{ |
| 163 | + return GetByRevisions (doc, element) |
| 164 | + .LastOrDefault () |
| 165 | + .Revision; |
| 166 | +} |
| 167 | + |
| 168 | +IEnumerable<JObject> CreateExtraPackageEntries (XDocument doc) |
| 169 | +{ |
| 170 | + var allExtras = GetByRevisions (doc, "extra").ToList (); |
| 171 | + var paths = allExtras |
| 172 | + .Select (e => e.Element.ReqAttr ("path")) |
| 173 | + .Distinct (); |
| 174 | + foreach (var path in paths) { |
| 175 | + var extras = allExtras |
| 176 | + .Where (e => e.Element.ReqAttr ("path") == path); |
| 177 | + var version = string.Join (",", extras.Select (e => e.Revision)); |
| 178 | + var latest = extras.Last (); |
| 179 | + var entry = new JObject { |
| 180 | + new JProperty ("desc", latest.Element.ReqAttr ("description")), |
| 181 | + new JProperty ("sdkPackage", new JObject { |
| 182 | + new JProperty ("id", path), |
| 183 | + new JProperty ("version", "[" + version + "]"), |
| 184 | + new JProperty ("recommendedId", latest.Element.ReqAttr ("path")), |
| 185 | + new JProperty ("recommendedVersion", latest.Revision), |
| 186 | + }), |
| 187 | + new JProperty ("optional", "true"), |
| 188 | + }; |
| 189 | + yield return entry; |
| 190 | + } |
| 191 | +} |
| 192 | + |
| 193 | +IEnumerable<JObject> CreateAddonPackageEntries (XDocument doc) |
| 194 | +{ |
| 195 | + var allAddons = GetSupportedElements (doc, "addon").ToList () |
| 196 | + .OrderBy (e => e.ReqAttr ("path")); |
| 197 | + var paths = allAddons |
| 198 | + .Select (e => GetEntryId (e)) |
| 199 | + .Distinct (); |
| 200 | + foreach (var path in paths) { |
| 201 | + var addons = allAddons |
| 202 | + .Where (e => GetEntryId (e) == path); |
| 203 | + var version = string.Join (",", addons.Select (e => e.ReqAttr ("revision"))); |
| 204 | + var latest = addons.Last (); |
| 205 | + var entry = new JObject { |
| 206 | + new JProperty ("desc", latest.ReqAttr ("description")), |
| 207 | + new JProperty ("sdkPackage", new JObject { |
| 208 | + new JProperty ("id", path), |
| 209 | + new JProperty ("version", "[" + version + "]"), |
| 210 | + new JProperty ("recommendedId", latest.ReqAttr ("path")), |
| 211 | + new JProperty ("recommendedVersion", latest.ReqAttr ("revision")), |
| 212 | + }), |
| 213 | + new JProperty ("optional", "true"), |
| 214 | + }; |
| 215 | + yield return entry; |
| 216 | + } |
| 217 | +} |
| 218 | + |
| 219 | +JArray CreatePackagesArray (XDocument doc) |
| 220 | +{ |
| 221 | + var packages = new JArray (); |
| 222 | + var names = doc.Root!.Elements () |
| 223 | + .Select (e => e.Name.LocalName) |
| 224 | + .Distinct () |
| 225 | + .OrderBy (e => e); |
| 226 | + foreach (var name in names) { |
| 227 | + if (PackageCreators.TryGetValue (name, out var creator)) { |
| 228 | + foreach (var e in creator (doc)) { |
| 229 | + packages.Add (e); |
| 230 | + } |
| 231 | + continue; |
| 232 | + } |
| 233 | + var items = GetSupportedElements (doc, name) |
| 234 | + .OrderBy (e => e.ReqAttr ("path")); |
| 235 | + if (!items.Any ()) { |
| 236 | + continue; |
| 237 | + } |
| 238 | + var version = string.Join (",", items.Select (e => e.ReqAttr ("revision"))); |
| 239 | + var latest = items.Last (); |
| 240 | + |
| 241 | + var entry = new JObject { |
| 242 | + new JProperty ("desc", latest.ReqAttr ("description")), |
| 243 | + new JProperty ("sdkPackage", new JObject { |
| 244 | + new JProperty ("id", GetEntryId (latest)), |
| 245 | + new JProperty ("version", "[" + version + "]"), |
| 246 | + new JProperty ("recommendedId", latest.ReqAttr ("path")), |
| 247 | + new JProperty ("recommendedVersion", latest.ReqAttr ("revision")), |
| 248 | + }), |
| 249 | + new JProperty ("optional", (!RequiredPackages.Contains (name)).ToString ().ToLowerInvariant ()), |
| 250 | + }; |
| 251 | + |
| 252 | + packages.Add (entry); |
| 253 | + } |
| 254 | + return packages; |
| 255 | +} |
| 256 | + |
| 257 | +string GetEntryId (XElement entry) |
| 258 | +{ |
| 259 | + var path = entry.ReqAttr ("path"); |
| 260 | + var semic = path.LastIndexOf (';'); |
| 261 | + if (semic < 0) { |
| 262 | + return path; |
| 263 | + } |
| 264 | + var hyphen = path.LastIndexOf ('-'); |
| 265 | + if (hyphen < 0) { |
| 266 | + return path.Substring (0, semic+1) + "*"; |
| 267 | + } |
| 268 | + return path.Substring (0, Math.Max (hyphen, semic)+1) + "*"; |
| 269 | +} |
| 270 | + |
| 271 | +static class Extensions |
| 272 | +{ |
| 273 | + public static string ReqAttr (this XElement e, string attribute) |
| 274 | + { |
| 275 | + var v = (string?) e.Attribute (attribute); |
| 276 | + if (v == null) { |
| 277 | + throw new InvalidOperationException ($"Missing required attribute `{attribute}` in: `{e}"); |
| 278 | + } |
| 279 | + return v; |
| 280 | + } |
| 281 | + |
| 282 | + public static IEnumerable<(XElement Element, string Revision)> OrderByRevision (this IEnumerable<XElement> elements) |
| 283 | + { |
| 284 | + return from e in elements |
| 285 | + let revision = e.ReqAttr ("revision") |
| 286 | + let version = new Version (revision.Contains (".") ? revision : revision + ".0") |
| 287 | + orderby version |
| 288 | + select (e, revision); |
| 289 | + } |
| 290 | +} |
0 commit comments