From ba66433257506774233c0e992e8ed7786313be6b Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 13 Aug 2025 11:31:44 +0000 Subject: [PATCH 1/7] Initial plan From 7394c10325d60b3663053f6f7d15f7ad659b8213 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 13 Aug 2025 12:03:08 +0000 Subject: [PATCH 2/7] Fix AndroidAppBuilder to use assembly name for runtimeconfig.json lookup Co-authored-by: davidnguyen-tech <87228593+davidnguyen-tech@users.noreply.github.com> --- src/tasks/AndroidAppBuilder/ApkBuilder.cs | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/src/tasks/AndroidAppBuilder/ApkBuilder.cs b/src/tasks/AndroidAppBuilder/ApkBuilder.cs index 109583df46120f..d203028928100f 100644 --- a/src/tasks/AndroidAppBuilder/ApkBuilder.cs +++ b/src/tasks/AndroidAppBuilder/ApkBuilder.cs @@ -395,7 +395,7 @@ public ApkBuilder(TaskLoggingHelper logger) string monodroidContent = Utils.GetEmbeddedResource(monodroidSource); if (IsCoreCLR) { - monodroidContent = RenderMonodroidCoreClrTemplate(monodroidContent); + monodroidContent = RenderMonodroidCoreClrTemplate(monodroidContent, mainLibraryFileName); } File.WriteAllText(Path.Combine(OutputDir, monodroidSource), monodroidContent); @@ -667,7 +667,7 @@ private static string NormalizePathToUnix(string path) [GeneratedRegex(@"\.(\d)")] private static partial Regex DotNumberRegex(); - private string RenderMonodroidCoreClrTemplate(string monodroidContent) + private string RenderMonodroidCoreClrTemplate(string monodroidContent, string mainLibraryFileName) { // At the moment, we only set the AppContext properties, so it's all done here for simplicity. // If we need to add more rendering logic, we can refactor this method later. @@ -686,7 +686,7 @@ private string RenderMonodroidCoreClrTemplate(string monodroidContent) appContextValues.AppendLine(); // Parse runtime config properties and add them to the AppContext keys and values. - Dictionary configProperties = ParseRuntimeConfigProperties(); + Dictionary configProperties = ParseRuntimeConfigProperties(mainLibraryFileName); int hardwiredAppContextProperties = 3; // For the hardwired AppContext keys and values above. int i = 0; foreach ((string key, string value) in configProperties) @@ -703,10 +703,13 @@ private string RenderMonodroidCoreClrTemplate(string monodroidContent) return updatedContent; } - private Dictionary ParseRuntimeConfigProperties() + private Dictionary ParseRuntimeConfigProperties(string mainLibraryFileName) { var configProperties = new Dictionary(); - string runtimeConfigPath = Path.Combine(AppDir ?? throw new InvalidOperationException("AppDir is not set"), $"{ProjectName}.runtimeconfig.json"); + + // Extract assembly name from mainLibraryFileName (e.g., "AndroidSampleApp.dll" -> "AndroidSampleApp") + string assemblyName = Path.GetFileNameWithoutExtension(mainLibraryFileName); + string runtimeConfigPath = Path.Combine(AppDir ?? throw new InvalidOperationException("AppDir is not set"), $"{assemblyName}.runtimeconfig.json"); try { From 44200a90ae2fe5862d45c9ca26844e2cf3f6598d Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 13 Aug 2025 12:46:34 +0000 Subject: [PATCH 3/7] Update AndroidAppBuilder to use runtimeconfig.bin instead of assembly-specific runtimeconfig.json Co-authored-by: davidnguyen-tech <87228593+davidnguyen-tech@users.noreply.github.com> --- src/tasks/AndroidAppBuilder/ApkBuilder.cs | 69 ++++++++++++++++++----- 1 file changed, 55 insertions(+), 14 deletions(-) diff --git a/src/tasks/AndroidAppBuilder/ApkBuilder.cs b/src/tasks/AndroidAppBuilder/ApkBuilder.cs index d203028928100f..71a11f6962f041 100644 --- a/src/tasks/AndroidAppBuilder/ApkBuilder.cs +++ b/src/tasks/AndroidAppBuilder/ApkBuilder.cs @@ -395,7 +395,7 @@ public ApkBuilder(TaskLoggingHelper logger) string monodroidContent = Utils.GetEmbeddedResource(monodroidSource); if (IsCoreCLR) { - monodroidContent = RenderMonodroidCoreClrTemplate(monodroidContent, mainLibraryFileName); + monodroidContent = RenderMonodroidCoreClrTemplate(monodroidContent); } File.WriteAllText(Path.Combine(OutputDir, monodroidSource), monodroidContent); @@ -667,7 +667,7 @@ private static string NormalizePathToUnix(string path) [GeneratedRegex(@"\.(\d)")] private static partial Regex DotNumberRegex(); - private string RenderMonodroidCoreClrTemplate(string monodroidContent, string mainLibraryFileName) + private string RenderMonodroidCoreClrTemplate(string monodroidContent) { // At the moment, we only set the AppContext properties, so it's all done here for simplicity. // If we need to add more rendering logic, we can refactor this method later. @@ -686,7 +686,7 @@ private string RenderMonodroidCoreClrTemplate(string monodroidContent, string ma appContextValues.AppendLine(); // Parse runtime config properties and add them to the AppContext keys and values. - Dictionary configProperties = ParseRuntimeConfigProperties(mainLibraryFileName); + Dictionary configProperties = ParseRuntimeConfigProperties(); int hardwiredAppContextProperties = 3; // For the hardwired AppContext keys and values above. int i = 0; foreach ((string key, string value) in configProperties) @@ -703,26 +703,33 @@ private string RenderMonodroidCoreClrTemplate(string monodroidContent, string ma return updatedContent; } - private Dictionary ParseRuntimeConfigProperties(string mainLibraryFileName) + private Dictionary ParseRuntimeConfigProperties() { var configProperties = new Dictionary(); - - // Extract assembly name from mainLibraryFileName (e.g., "AndroidSampleApp.dll" -> "AndroidSampleApp") - string assemblyName = Path.GetFileNameWithoutExtension(mainLibraryFileName); - string runtimeConfigPath = Path.Combine(AppDir ?? throw new InvalidOperationException("AppDir is not set"), $"{assemblyName}.runtimeconfig.json"); + string runtimeConfigPath = Path.Combine(AppDir ?? throw new InvalidOperationException("AppDir is not set"), "runtimeconfig.bin"); try { - string jsonContent = File.ReadAllText(runtimeConfigPath); - using JsonDocument doc = JsonDocument.Parse(jsonContent); - JsonElement root = doc.RootElement; - if (root.TryGetProperty("runtimeOptions", out JsonElement runtimeOptions) && runtimeOptions.TryGetProperty("configProperties", out JsonElement propertiesJson)) + if (File.Exists(runtimeConfigPath)) { - foreach (JsonProperty property in propertiesJson.EnumerateObject()) + using var stream = new FileStream(runtimeConfigPath, FileMode.Open, FileAccess.Read, FileShare.Read); + using var reader = new BinaryReader(stream); + + // Read the compressed integer count + int count = ReadCompressedInteger(reader); + + // Read each key-value pair + for (int i = 0; i < count; i++) { - configProperties[property.Name] = property.Value.ToString(); + string key = ReadSerializedString(reader); + string value = ReadSerializedString(reader); + configProperties[key] = value; } } + else + { + logger.LogMessage(MessageImportance.Normal, $"Runtime config file not found at {runtimeConfigPath}"); + } } catch (Exception ex) { @@ -731,4 +738,38 @@ private Dictionary ParseRuntimeConfigProperties(string mainLibra return configProperties; } + + private static int ReadCompressedInteger(BinaryReader reader) + { + // This mirrors the format used by BlobBuilder.WriteCompressedInteger + byte firstByte = reader.ReadByte(); + + if ((firstByte & 0x80) == 0) + { + // Single byte format + return firstByte; + } + else if ((firstByte & 0xC0) == 0x80) + { + // Two byte format + byte secondByte = reader.ReadByte(); + return ((firstByte & 0x3F) << 8) | secondByte; + } + else + { + // Four byte format + byte secondByte = reader.ReadByte(); + byte thirdByte = reader.ReadByte(); + byte fourthByte = reader.ReadByte(); + return ((firstByte & 0x1F) << 24) | (secondByte << 16) | (thirdByte << 8) | fourthByte; + } + } + + private static string ReadSerializedString(BinaryReader reader) + { + // This mirrors the format used by BlobBuilder.WriteSerializedString + int length = ReadCompressedInteger(reader); + byte[] bytes = reader.ReadBytes(length); + return System.Text.Encoding.UTF8.GetString(bytes); + } } From 5f25ed5d512613d7038c3d27056d2689f73e1a06 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 13 Aug 2025 13:29:20 +0000 Subject: [PATCH 4/7] Add documentation comment explaining binary format source in ParseRuntimeConfigProperties Co-authored-by: davidnguyen-tech <87228593+davidnguyen-tech@users.noreply.github.com> --- src/tasks/AndroidAppBuilder/ApkBuilder.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/tasks/AndroidAppBuilder/ApkBuilder.cs b/src/tasks/AndroidAppBuilder/ApkBuilder.cs index 71a11f6962f041..915ac0e9f1feaa 100644 --- a/src/tasks/AndroidAppBuilder/ApkBuilder.cs +++ b/src/tasks/AndroidAppBuilder/ApkBuilder.cs @@ -705,6 +705,10 @@ private string RenderMonodroidCoreClrTemplate(string monodroidContent) private Dictionary ParseRuntimeConfigProperties() { + // This method reads the binary runtimeconfig.bin file created by RuntimeConfigParserTask.ConvertDictionaryToBlob. + // The binary format is: compressed integer count, followed by count pairs of length-prefixed UTF8 strings (key, value). + // See src/tasks/MonoTargetsTasks/RuntimeConfigParser/RuntimeConfigParser.cs for the corresponding write logic. + var configProperties = new Dictionary(); string runtimeConfigPath = Path.Combine(AppDir ?? throw new InvalidOperationException("AppDir is not set"), "runtimeconfig.bin"); From 410ea940d6881d3cbc53fd48854bdd000c874cef Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 13 Aug 2025 13:56:50 +0000 Subject: [PATCH 5/7] Replace custom Read methods with existing BlobReader from System.Reflection.Metadata Co-authored-by: davidnguyen-tech <87228593+davidnguyen-tech@users.noreply.github.com> --- .../AndroidAppBuilder.csproj | 1 + src/tasks/AndroidAppBuilder/ApkBuilder.cs | 61 ++++++------------- 2 files changed, 18 insertions(+), 44 deletions(-) diff --git a/src/tasks/AndroidAppBuilder/AndroidAppBuilder.csproj b/src/tasks/AndroidAppBuilder/AndroidAppBuilder.csproj index 00b863c5ea59a4..5614e09c8b106d 100644 --- a/src/tasks/AndroidAppBuilder/AndroidAppBuilder.csproj +++ b/src/tasks/AndroidAppBuilder/AndroidAppBuilder.csproj @@ -5,6 +5,7 @@ true false enable + true $(NoWarn),CA1050,CA1850 diff --git a/src/tasks/AndroidAppBuilder/ApkBuilder.cs b/src/tasks/AndroidAppBuilder/ApkBuilder.cs index 915ac0e9f1feaa..a66c463361080f 100644 --- a/src/tasks/AndroidAppBuilder/ApkBuilder.cs +++ b/src/tasks/AndroidAppBuilder/ApkBuilder.cs @@ -6,6 +6,7 @@ using System.IO; using System.IO.Compression; using System.Linq; +using System.Reflection.Metadata; using System.Text; using System.Text.Json; using System.Text.RegularExpressions; @@ -716,18 +717,24 @@ private Dictionary ParseRuntimeConfigProperties() { if (File.Exists(runtimeConfigPath)) { - using var stream = new FileStream(runtimeConfigPath, FileMode.Open, FileAccess.Read, FileShare.Read); - using var reader = new BinaryReader(stream); + byte[] fileBytes = File.ReadAllBytes(runtimeConfigPath); + unsafe + { + fixed (byte* ptr = fileBytes) + { + var blobReader = new BlobReader(ptr, fileBytes.Length); - // Read the compressed integer count - int count = ReadCompressedInteger(reader); + // Read the compressed integer count + int count = blobReader.ReadCompressedInteger(); - // Read each key-value pair - for (int i = 0; i < count; i++) - { - string key = ReadSerializedString(reader); - string value = ReadSerializedString(reader); - configProperties[key] = value; + // Read each key-value pair + for (int i = 0; i < count; i++) + { + string key = blobReader.ReadSerializedString() ?? string.Empty; + string value = blobReader.ReadSerializedString() ?? string.Empty; + configProperties[key] = value; + } + } } } else @@ -742,38 +749,4 @@ private Dictionary ParseRuntimeConfigProperties() return configProperties; } - - private static int ReadCompressedInteger(BinaryReader reader) - { - // This mirrors the format used by BlobBuilder.WriteCompressedInteger - byte firstByte = reader.ReadByte(); - - if ((firstByte & 0x80) == 0) - { - // Single byte format - return firstByte; - } - else if ((firstByte & 0xC0) == 0x80) - { - // Two byte format - byte secondByte = reader.ReadByte(); - return ((firstByte & 0x3F) << 8) | secondByte; - } - else - { - // Four byte format - byte secondByte = reader.ReadByte(); - byte thirdByte = reader.ReadByte(); - byte fourthByte = reader.ReadByte(); - return ((firstByte & 0x1F) << 24) | (secondByte << 16) | (thirdByte << 8) | fourthByte; - } - } - - private static string ReadSerializedString(BinaryReader reader) - { - // This mirrors the format used by BlobBuilder.WriteSerializedString - int length = ReadCompressedInteger(reader); - byte[] bytes = reader.ReadBytes(length); - return System.Text.Encoding.UTF8.GetString(bytes); - } } From 0582ce4255c5786a3ab68af0739c3eb846f7993c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 13 Aug 2025 14:26:41 +0000 Subject: [PATCH 6/7] Simplify runtime config parsing by removing manual file existence check Co-authored-by: davidnguyen-tech <87228593+davidnguyen-tech@users.noreply.github.com> --- src/tasks/AndroidAppBuilder/ApkBuilder.cs | 31 +++++++++-------------- 1 file changed, 12 insertions(+), 19 deletions(-) diff --git a/src/tasks/AndroidAppBuilder/ApkBuilder.cs b/src/tasks/AndroidAppBuilder/ApkBuilder.cs index a66c463361080f..30e79a29dfdf8a 100644 --- a/src/tasks/AndroidAppBuilder/ApkBuilder.cs +++ b/src/tasks/AndroidAppBuilder/ApkBuilder.cs @@ -715,32 +715,25 @@ private Dictionary ParseRuntimeConfigProperties() try { - if (File.Exists(runtimeConfigPath)) + byte[] fileBytes = File.ReadAllBytes(runtimeConfigPath); + unsafe { - byte[] fileBytes = File.ReadAllBytes(runtimeConfigPath); - unsafe + fixed (byte* ptr = fileBytes) { - fixed (byte* ptr = fileBytes) - { - var blobReader = new BlobReader(ptr, fileBytes.Length); + var blobReader = new BlobReader(ptr, fileBytes.Length); - // Read the compressed integer count - int count = blobReader.ReadCompressedInteger(); + // Read the compressed integer count + int count = blobReader.ReadCompressedInteger(); - // Read each key-value pair - for (int i = 0; i < count; i++) - { - string key = blobReader.ReadSerializedString() ?? string.Empty; - string value = blobReader.ReadSerializedString() ?? string.Empty; - configProperties[key] = value; - } + // Read each key-value pair + for (int i = 0; i < count; i++) + { + string key = blobReader.ReadSerializedString() ?? string.Empty; + string value = blobReader.ReadSerializedString() ?? string.Empty; + configProperties[key] = value; } } } - else - { - logger.LogMessage(MessageImportance.Normal, $"Runtime config file not found at {runtimeConfigPath}"); - } } catch (Exception ex) { From 0e141db8947ca75f856d4317ae9126af0fbd46ec Mon Sep 17 00:00:00 2001 From: David Nguyen <87228593+davidnguyen-tech@users.noreply.github.com> Date: Thu, 14 Aug 2025 12:11:23 +0200 Subject: [PATCH 7/7] Allow unsafe blocks because of ApkBuilder (#118721) --- src/tasks/TestExclusionListTasks/TestExclusionListTasks.csproj | 1 + 1 file changed, 1 insertion(+) diff --git a/src/tasks/TestExclusionListTasks/TestExclusionListTasks.csproj b/src/tasks/TestExclusionListTasks/TestExclusionListTasks.csproj index ffc0d16e0ea031..87270cd0634dd8 100644 --- a/src/tasks/TestExclusionListTasks/TestExclusionListTasks.csproj +++ b/src/tasks/TestExclusionListTasks/TestExclusionListTasks.csproj @@ -6,6 +6,7 @@ false enable $(NoWarn),CA1050,CA1850 + true