From 21e69b49799cae6e282842a18997936603773ecc Mon Sep 17 00:00:00 2001 From: Dean Ellis Date: Wed, 26 Feb 2025 20:01:13 +0000 Subject: [PATCH] [Xamarin.Android.Build.Tasks] Remove use of Newtonsoft.Json (#9819) Fixes: https://github.com/dotnet/android/issues/9229 Remove use of Newtonsoft.Json from `Xamarin.Android.Build.Tasks.dll`. Notes: * `JsonElement` is read only. This is why we convert them to `JsonNode`. * `JsonNode.Parse()` does NOT have the same features as `JsonDocument.Parse()`, e.g you CANNOT tell it to ignore trailing comma's; it will just throw an exception. This is the reason why we have to load the `.json` via `JsonDocument` and then call the extension method `.ToNode ()` on the Root `JsonElement` to convert it to a `JsonNode`. (We use `JsonNode` to do the merging). --- .../installers/create-installers.targets | 1 - .../Tasks/BuildAppBundle.cs | 31 ++++--- .../Tasks/JavaDependencyVerification.cs | 23 +++-- .../Utilities/JsonExtensions.cs | 87 +++++++++++++++++++ .../Utilities/MamJsonParser.cs | 7 +- .../Xamarin.Android.Build.Tasks.csproj | 1 - 6 files changed, 122 insertions(+), 28 deletions(-) create mode 100644 src/Xamarin.Android.Build.Tasks/Utilities/JsonExtensions.cs diff --git a/build-tools/installers/create-installers.targets b/build-tools/installers/create-installers.targets index 2131682f2eb..d7ed385f907 100644 --- a/build-tools/installers/create-installers.targets +++ b/build-tools/installers/create-installers.targets @@ -116,7 +116,6 @@ <_MSBuildFiles Include="$(MicrosoftAndroidSdkOutDir)Mono.Options.dll" /> <_MSBuildFiles Include="$(MicrosoftAndroidSdkOutDir)Mono.Options.pdb" /> <_MSBuildFiles Include="$(MicrosoftAndroidSdkOutDir)MULTIDEX_JAR_LICENSE" /> - <_MSBuildFiles Include="$(MicrosoftAndroidSdkOutDir)Newtonsoft.Json.dll" /> <_MSBuildFiles Include="$(MicrosoftAndroidSdkOutDir)NuGet.Common.dll" /> <_MSBuildFiles Include="$(MicrosoftAndroidSdkOutDir)NuGet.Configuration.dll" /> <_MSBuildFiles Include="$(MicrosoftAndroidSdkOutDir)NuGet.DependencyResolver.Core.dll" /> diff --git a/src/Xamarin.Android.Build.Tasks/Tasks/BuildAppBundle.cs b/src/Xamarin.Android.Build.Tasks/Tasks/BuildAppBundle.cs index aeb91b1f25c..edd74511b5f 100644 --- a/src/Xamarin.Android.Build.Tasks/Tasks/BuildAppBundle.cs +++ b/src/Xamarin.Android.Build.Tasks/Tasks/BuildAppBundle.cs @@ -1,7 +1,7 @@ using Microsoft.Build.Framework; using Microsoft.Build.Utilities; -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; +using System.Text.Json; +using System.Text.Json.Nodes; using System; using System.Collections.Generic; using System.IO; @@ -88,26 +88,25 @@ public override bool RunTask () } } - var json = JObject.FromObject (new { }); + JsonNode json = JsonNode.Parse ("{}")!; if (!string.IsNullOrEmpty (CustomBuildConfigFile) && File.Exists (CustomBuildConfigFile)) { - using (StreamReader file = File.OpenText (CustomBuildConfigFile)) - using (JsonTextReader reader = new JsonTextReader (file)) { - json = (JObject)JToken.ReadFrom(reader); - } + using Stream fs = File.OpenRead (CustomBuildConfigFile); + using JsonDocument doc = JsonDocument.Parse (fs, new JsonDocumentOptions { AllowTrailingCommas = true }); + json = doc.RootElement.ToNode (); } - var jsonAddition = JObject.FromObject (new { + var jsonAddition = new { compression = new { uncompressedGlob = uncompressed, } - }); - - var mergeSettings = new JsonMergeSettings () { - MergeArrayHandling = MergeArrayHandling.Union, - MergeNullValueHandling = MergeNullValueHandling.Ignore }; - json.Merge (jsonAddition, mergeSettings); - Log.LogDebugMessage ("BundleConfig.json: {0}", json); - File.WriteAllText (temp, json.ToString ()); + + var jsonAdditionDoc = JsonSerializer.SerializeToNode (jsonAddition); + + var mergedJson = json.Merge (jsonAdditionDoc); + var output = mergedJson.ToJsonString (new JsonSerializerOptions { WriteIndented = true }); + + Log.LogDebugMessage ("BundleConfig.json: {0}", output); + File.WriteAllText (temp, output); //NOTE: bundletool will not overwrite if (File.Exists (Output)) diff --git a/src/Xamarin.Android.Build.Tasks/Tasks/JavaDependencyVerification.cs b/src/Xamarin.Android.Build.Tasks/Tasks/JavaDependencyVerification.cs index 5ccd8309b75..25956bc5b0b 100644 --- a/src/Xamarin.Android.Build.Tasks/Tasks/JavaDependencyVerification.cs +++ b/src/Xamarin.Android.Build.Tasks/Tasks/JavaDependencyVerification.cs @@ -11,7 +11,8 @@ using Microsoft.Android.Build.Tasks; using Microsoft.Build.Framework; using Microsoft.Build.Utilities; -using Newtonsoft.Json; +using System.Text.Json; +using System.Text.Json.Serialization; using NuGet.ProjectModel; namespace Xamarin.Android.Tasks; @@ -305,7 +306,7 @@ public Project Resolve (Artifact artifact) } } -class MicrosoftNuGetPackageFinder +partial class MicrosoftNuGetPackageFinder { readonly PackageListFile? package_list; @@ -318,7 +319,7 @@ public MicrosoftNuGetPackageFinder (string? file, TaskLoggingHelper log) try { var json = File.ReadAllText (file); - package_list = JsonConvert.DeserializeObject (json); + package_list = JsonSerializer.Deserialize (json, PackageListFileContext.Default.PackageListFile); } catch (Exception ex) { log.LogMessage ("There was an error reading 'microsoft-packages.json', Android NuGet suggestions will not be provided: {0}", ex); } @@ -331,18 +332,26 @@ public MicrosoftNuGetPackageFinder (string? file, TaskLoggingHelper log) public class PackageListFile { - [JsonProperty ("packages")] public List? Packages { get; set; } } public class Package { - [JsonProperty ("javaId")] public string? JavaId { get; set; } - - [JsonProperty ("nugetId")] public string? NuGetId { get; set; } } + + [JsonSourceGenerationOptions( + AllowTrailingCommas = true, + WriteIndented = true, + PropertyNameCaseInsensitive = true + )] + [JsonSerializable(typeof(PackageListFile))] + [JsonSerializable(typeof(List))] + [JsonSerializable(typeof(string))] + internal partial class PackageListFileContext : JsonSerializerContext + { + } } public class NuGetPackageVersionFinder diff --git a/src/Xamarin.Android.Build.Tasks/Utilities/JsonExtensions.cs b/src/Xamarin.Android.Build.Tasks/Utilities/JsonExtensions.cs new file mode 100644 index 00000000000..9a399d2f8e0 --- /dev/null +++ b/src/Xamarin.Android.Build.Tasks/Utilities/JsonExtensions.cs @@ -0,0 +1,87 @@ +using System; +using System.Collections.Generic; +using System.Text.Json; +using System.Text.Json.Nodes; + +public static class JsonExtensions +{ + public static JsonNode Merge (this JsonNode jsonBase, JsonNode jsonMerge) + { + if (jsonBase == null || jsonMerge == null) + return jsonBase; + + switch (jsonBase) + { + case JsonObject jsonBaseObj when jsonMerge is JsonObject jsonMergeObj: { + var mergeNodesArray = new KeyValuePair [jsonMergeObj.Count]; + int index = 0; + foreach (var prop in jsonMergeObj) { + mergeNodesArray [index++] = prop; + } + jsonMergeObj.Clear (); + + foreach (var prop in mergeNodesArray) { + jsonBaseObj [prop.Key] = jsonBaseObj [prop.Key] switch { + JsonObject jsonBaseChildObj when prop.Value is JsonObject jsonMergeChildObj => jsonBaseChildObj.Merge (jsonMergeChildObj), + JsonArray jsonBaseChildArray when prop.Value is JsonArray jsonMergeChildArray => jsonBaseChildArray.Merge (jsonMergeChildArray), + _ => prop.Value + }; + } + break; + } + case JsonArray jsonBaseArray when jsonMerge is JsonArray jsonMergeArray: { + var mergeNodesArray = new JsonNode? [jsonMergeArray.Count]; + int index = 0; + foreach (var mergeNode in jsonMergeArray) { + mergeNodesArray [index++] = mergeNode; + } + jsonMergeArray.Clear (); + foreach (var mergeNode in mergeNodesArray) { + jsonBaseArray.Add (mergeNode); + } + break; + } + default: + throw new ArgumentException ($"The JsonNode type [{jsonBase.GetType ().Name}] is incompatible for merging with the target/base " + + $"type [{jsonMerge.GetType ().Name}]; merge requires the types to be the same."); + } + return jsonBase; + } + + public static JsonNode? ToNode (this JsonElement element) + { + switch (element.ValueKind) { + case JsonValueKind.Object: + var obj = new JsonObject (); + foreach (JsonProperty prop in element.EnumerateObject()) { + obj [prop.Name] = prop.Value.ToNode (); + } + return obj; + + case JsonValueKind.Array: + var arr = new JsonArray(); + foreach (JsonElement item in element.EnumerateArray ()) { + arr.Add (item.ToNode ()); + } + return arr; + + case JsonValueKind.String: + return element.GetString (); + + case JsonValueKind.Number: + return element.TryGetInt32 (out int intValue) ? intValue : element.GetDouble (); + + case JsonValueKind.True: + return true; + + case JsonValueKind.False: + return false; + + case JsonValueKind.Null: + return null; + + default: + throw new NotSupportedException ($"Unsupported JSON value kind: {element.ValueKind}"); + } + } +} \ No newline at end of file diff --git a/src/Xamarin.Android.Build.Tasks/Utilities/MamJsonParser.cs b/src/Xamarin.Android.Build.Tasks/Utilities/MamJsonParser.cs index cde98c9cc56..1db618d91c2 100644 --- a/src/Xamarin.Android.Build.Tasks/Utilities/MamJsonParser.cs +++ b/src/Xamarin.Android.Build.Tasks/Utilities/MamJsonParser.cs @@ -57,10 +57,11 @@ public XElement ToXml () GetReplacementMethods ()); } - static JsonObject ReadJson (string path) + static JsonNode ReadJson (string path) { - using (var f = File.OpenRead (path)) { - return JsonNode.Parse (f)!.AsObject (); + using (var fs = File.OpenRead (path)) { + using JsonDocument doc = JsonDocument.Parse (fs, new JsonDocumentOptions { AllowTrailingCommas = true }); + return doc.RootElement.ToNode () ?? JsonNode.Parse ("{}")!; } } diff --git a/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Build.Tasks.csproj b/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Build.Tasks.csproj index 98e552cdf44..1eef89eeb0c 100644 --- a/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Build.Tasks.csproj +++ b/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Build.Tasks.csproj @@ -29,7 +29,6 @@ -