From f60a5059ca35824e5d6dcdd1c7807e7d2598e574 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Fri, 4 Sep 2026 11:02:02 +0200 Subject: [PATCH 1/9] [xabt] Move trimmable typemap tasks to net11 Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .github/skills/tests/SKILL.md | 1 + .../skills/tests/references/test-catalog.md | 1 + Xamarin.Android.Build.Tasks.slnx | 1 + Xamarin.Android.slnx | 1 + .../Microsoft.Android.Build.Tasks.csproj | 10 +- .../Tasks/GenerateMissingTypeMapStubs.cs | 2 +- .../Tasks/GenerateTrimmableTypeMap.cs | 40 ++++-- .../BaseTest.cs | 31 ++++ .../GenerateMissingTypeMapStubsTests.cs | 1 + .../GenerateTrimmableTypeMapTests.cs | 134 ++---------------- ...Microsoft.Android.Build.Tasks.Tests.csproj | 24 ++++ ...rosoft.Android.Sdk.TrimmableTypeMap.csproj | 4 +- .../README.md | 4 +- .../Scanner/JavaAnnotationParser.cs | 5 +- .../Scanner/JavaPeerScanner.cs | 3 +- .../Scanner/ScannerHashingHelper.cs | 15 +- ...roid.Sdk.TypeMap.Trimmable.CoreCLR.targets | 3 +- ...soft.Android.Sdk.TypeMap.Trimmable.targets | 3 +- .../targets/Microsoft.Android.Sdk.targets | 1 + ...rateNativeAotProguardConfigurationTests.cs | 128 +++++++++++++++++ .../Xamarin.Android.Build.Tasks.csproj | 2 +- ....Android.Sdk.TrimmableTypeMap.Tests.csproj | 2 +- 22 files changed, 261 insertions(+), 155 deletions(-) rename src/{Xamarin.Android.Build.Tasks => Microsoft.Android.Build.Tasks}/Tasks/GenerateMissingTypeMapStubs.cs (99%) rename src/{Xamarin.Android.Build.Tasks => Microsoft.Android.Build.Tasks}/Tasks/GenerateTrimmableTypeMap.cs (93%) create mode 100644 src/Microsoft.Android.Build.Tasks/Tests/Microsoft.Android.Build.Tasks.Tests/BaseTest.cs rename src/{Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Tasks => Microsoft.Android.Build.Tasks/Tests/Microsoft.Android.Build.Tasks.Tests}/GenerateMissingTypeMapStubsTests.cs (99%) rename src/{Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Tasks => Microsoft.Android.Build.Tasks/Tests/Microsoft.Android.Build.Tasks.Tests}/GenerateTrimmableTypeMapTests.cs (72%) create mode 100644 src/Microsoft.Android.Build.Tasks/Tests/Microsoft.Android.Build.Tasks.Tests/Microsoft.Android.Build.Tasks.Tests.csproj create mode 100644 src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Tasks/GenerateNativeAotProguardConfigurationTests.cs diff --git a/.github/skills/tests/SKILL.md b/.github/skills/tests/SKILL.md index 43b9072ae72..abb34aa1299 100644 --- a/.github/skills/tests/SKILL.md +++ b/.github/skills/tests/SKILL.md @@ -41,6 +41,7 @@ Classify the user's request: | `src/Mono.Android/Xamarin.Android.Net/` | On-device: networking tests in `Mono.Android.NET-Tests.csproj`. Host: `CheckClientHandlerTypeTests`, `LinkerTests` | | `src/Mono.Android/Android.*/**` | On-device: corresponding namespace tests in `Mono.Android.NET-Tests.csproj` | | `src/Xamarin.Android.Build.Tasks/Tasks/` | Host: task tests in `Xamarin.Android.Build.Tests.dll` (filter by task name) | +| `src/Microsoft.Android.Build.Tasks/Tasks/` | Standalone: `Microsoft.Android.Build.Tasks.Tests.csproj` | | `src/Xamarin.Android.Build.Tasks/Utilities/` | Host: `Xamarin.Android.Build.Tests.dll` — grep for test classes referencing the utility | | `src/Xamarin.Android.Build.Tasks/**/*.targets` | Host: `Xamarin.Android.Build.Tests.dll`. Device: `MSBuildDeviceIntegration.dll` | | `src/Microsoft.Android.Sdk.TrimmableTypeMap/` | Standalone: `TrimmableTypeMap.Tests.csproj`. Full-build: `TrimmableTypeMap.IntegrationTests` | diff --git a/.github/skills/tests/references/test-catalog.md b/.github/skills/tests/references/test-catalog.md index ded64ccbd2a..6847cab63fd 100644 --- a/.github/skills/tests/references/test-catalog.md +++ b/.github/skills/tests/references/test-catalog.md @@ -22,6 +22,7 @@ These tests can be run immediately with `dotnet test` on the `.csproj`, even if |-----------|---------|---------| | **assembly store reader** | `.github/skills/read-assembly-store/tests/AssemblyStore.Tests/` | `dotnet test .github/skills/read-assembly-store/tests/AssemblyStore.Tests/AssemblyStore.Tests.csproj -v minimal` | | **trimmable type map** (unit) | `tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/` | `dotnet test tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests.csproj -v minimal` | +| **modern Android build tasks** | `src/Microsoft.Android.Build.Tasks/Tests/Microsoft.Android.Build.Tasks.Tests/` | `dotnet test src/Microsoft.Android.Build.Tasks/Tests/Microsoft.Android.Build.Tasks.Tests/Microsoft.Android.Build.Tasks.Tests.csproj -v minimal` | | **aidl** | `tests/Xamarin.Android.Tools.Aidl-Tests/` | `dotnet test tests/Xamarin.Android.Tools.Aidl-Tests/Xamarin.Android.Tools.Aidl-Tests.csproj -v minimal` | | **source writer** | `external/Java.Interop/tests/Xamarin.SourceWriter-Tests/` | `dotnet test external/Java.Interop/tests/Xamarin.SourceWriter-Tests/Xamarin.SourceWriter-Tests.csproj -v minimal` | | **java source** | `external/Java.Interop/tests/Java.Interop.Tools.JavaSource-Tests/` | `dotnet test external/Java.Interop/tests/Java.Interop.Tools.JavaSource-Tests/Java.Interop.Tools.JavaSource-Tests.csproj -v minimal` | diff --git a/Xamarin.Android.Build.Tasks.slnx b/Xamarin.Android.Build.Tasks.slnx index af35653f7fa..b323dd936bc 100644 --- a/Xamarin.Android.Build.Tasks.slnx +++ b/Xamarin.Android.Build.Tasks.slnx @@ -1,5 +1,6 @@ + diff --git a/Xamarin.Android.slnx b/Xamarin.Android.slnx index 4c1a64bd0c8..8bdb38e571e 100644 --- a/Xamarin.Android.slnx +++ b/Xamarin.Android.slnx @@ -57,6 +57,7 @@ + diff --git a/src/Microsoft.Android.Build.Tasks/Microsoft.Android.Build.Tasks.csproj b/src/Microsoft.Android.Build.Tasks/Microsoft.Android.Build.Tasks.csproj index e40cbdc93b9..1d34595c3c8 100644 --- a/src/Microsoft.Android.Build.Tasks/Microsoft.Android.Build.Tasks.csproj +++ b/src/Microsoft.Android.Build.Tasks/Microsoft.Android.Build.Tasks.csproj @@ -21,14 +21,22 @@ We share $(OutputPath) with Xamarin.Android.Build.Tasks.csproj. Letting NuGet copy this project's transitive runtime assets there can overwrite the netstandard2.0 assemblies Build.Tasks already deposited with incompatible framework-specific implementations. - Only our compiled assembly needs to land in the SDK pack tools folder. + Task runtime dependencies are direct project references below; package assets stay isolated. --> false + + + + + + + + - + + <_TrimmableRuntimeProviderJavaName Condition=" '$(_TrimmableRuntimeProviderJavaName)' == '' ">mono.MonoRuntimeProvider diff --git a/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.TypeMap.Trimmable.targets b/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.TypeMap.Trimmable.targets index df55572c604..57f94353910 100644 --- a/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.TypeMap.Trimmable.targets +++ b/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.TypeMap.Trimmable.targets @@ -1,7 +1,8 @@ - + + diff --git a/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.targets b/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.targets index 9b21a3c5777..ee2422a58b3 100644 --- a/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.targets +++ b/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.targets @@ -5,6 +5,7 @@ Android <_XamarinAndroidBuildTasksAssembly>..\tools\Xamarin.Android.Build.Tasks.dll + <_MicrosoftAndroidBuildTasksAssembly>..\tools\Microsoft.Android.Build.Tasks.dll true true diff --git a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Tasks/GenerateNativeAotProguardConfigurationTests.cs b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Tasks/GenerateNativeAotProguardConfigurationTests.cs new file mode 100644 index 00000000000..fd9679e8c27 --- /dev/null +++ b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Tasks/GenerateNativeAotProguardConfigurationTests.cs @@ -0,0 +1,128 @@ +using System.IO; +using Microsoft.Build.Utilities; +using NUnit.Framework; +using Xamarin.Android.Tasks; + +namespace Xamarin.Android.Build.Tests; + +[TestFixture] +[Parallelizable (ParallelScope.Children)] +public class GenerateNativeAotProguardConfigurationTests : BaseTest +{ + [Test] + public void Execute_UsesDgmlTypeMetadata () + { + var path = Path.Combine (Root, "temp", TestName); + var dgmlFile = Path.Combine (path, "app.scan.dgml.xml"); + var acwMapFile = Path.Combine (path, "acw-map.txt"); + var outputFile = Path.Combine (path, "proguard", "proguard_project_references.cfg"); + Directory.CreateDirectory (path); + File.WriteAllText (dgmlFile, """ + + + + + + + + + + + + + + """); + File.WriteAllText (acwMapFile, """ + UnnamedProject.MainActivity, UnnamedProject;crc64a1.MainActivity + Android.App.Activity, Mono.Android;android.app.Activity + Duplicate.Type, My.Assembly;my.app.Duplicate + AndroidX.Activity.Result.Contract.ActivityResultContracts+TakePicture, Xamarin.AndroidX.Activity;androidx.activity.result.contract.ActivityResultContracts$TakePicture + Duplicate.Type;wrong.Duplicate + Other.Type;other.Type + """); + + var task = new GenerateNativeAotProguardConfiguration { + BuildEngine = new MockBuildEngine (TestContext.Out), + NativeAotDgmlFiles = new [] { new TaskItem (dgmlFile) }, + AcwMapFile = acwMapFile, + OutputFile = outputFile, + TrimJavaCallableWrappers = true, + }; + + Assert.IsTrue (task.Execute (), "Task should succeed."); + var proguard = File.ReadAllText (outputFile); + StringAssert.Contains ("-keep class crc64a1.MainActivity { *; }", proguard); + StringAssert.Contains ("-keep class android.app.Activity { *; }", proguard); + StringAssert.Contains ("-keep class my.app.Duplicate { *; }", proguard); + StringAssert.Contains ("-keep class androidx.activity.result.contract.ActivityResultContracts$TakePicture { *; }", proguard); + StringAssert.DoesNotContain ("wrong.Duplicate", proguard); + StringAssert.DoesNotContain ("other.Type", proguard); + } + + [Test] + public void Execute_KeepsAllWhenTrimmingDisabled () + { + var path = Path.Combine (Root, "temp", TestName); + var acwMapFile = Path.Combine (path, "acw-map.txt"); + var outputFile = Path.Combine (path, "proguard", "proguard_project_references.cfg"); + Directory.CreateDirectory (path); + File.WriteAllText (acwMapFile, """ + UnnamedProject.MainActivity, UnnamedProject;crc64a1.MainActivity + Android.App.Activity, Mono.Android;android.app.Activity + Duplicate.Type, My.Assembly;my.app.Duplicate + Other.Type;other.Type + """); + + var task = new GenerateNativeAotProguardConfiguration { + BuildEngine = new MockBuildEngine (TestContext.Out), + AcwMapFile = acwMapFile, + OutputFile = outputFile, + TrimJavaCallableWrappers = false, + }; + + Assert.IsTrue (task.Execute (), "Task should succeed without a DGML when trimming is disabled."); + var proguard = File.ReadAllText (outputFile); + StringAssert.Contains ("-keep class crc64a1.MainActivity { *; }", proguard); + StringAssert.Contains ("-keep class android.app.Activity { *; }", proguard); + StringAssert.Contains ("-keep class my.app.Duplicate { *; }", proguard); + StringAssert.Contains ("-keep class other.Type { *; }", proguard); + } + + [Test] + public void Execute_IgnoresDgmlWhenTrimmingDisabled () + { + var path = Path.Combine (Root, "temp", TestName); + var dgmlFile = Path.Combine (path, "app.scan.dgml.xml"); + var acwMapFile = Path.Combine (path, "acw-map.txt"); + var outputFile = Path.Combine (path, "proguard", "proguard_project_references.cfg"); + Directory.CreateDirectory (path); + File.WriteAllText (dgmlFile, """ + + + + + + + + """); + File.WriteAllText (acwMapFile, """ + UnnamedProject.MainActivity, UnnamedProject;crc64a1.MainActivity + Android.App.Activity, Mono.Android;android.app.Activity + Other.Type;other.Type + """); + + var task = new GenerateNativeAotProguardConfiguration { + BuildEngine = new MockBuildEngine (TestContext.Out), + NativeAotDgmlFiles = new [] { new TaskItem (dgmlFile) }, + AcwMapFile = acwMapFile, + OutputFile = outputFile, + TrimJavaCallableWrappers = false, + }; + + Assert.IsTrue (task.Execute (), "Task should succeed and ignore the DGML when trimming is disabled."); + var proguard = File.ReadAllText (outputFile); + StringAssert.Contains ("-keep class crc64a1.MainActivity { *; }", proguard); + StringAssert.Contains ("-keep class android.app.Activity { *; }", proguard); + StringAssert.Contains ("-keep class other.Type { *; }", proguard); + } +} 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 2be97ec0043..eeb02950445 100644 --- a/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Build.Tasks.csproj +++ b/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Build.Tasks.csproj @@ -31,6 +31,7 @@ + @@ -237,7 +238,6 @@ - diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests.csproj b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests.csproj index 0586a0b1b95..b4645496c90 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests.csproj +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests.csproj @@ -1,7 +1,7 @@ - $(DotNetStableTargetFramework) + $(DotNetTargetFramework) latest enable false From 1bc75ceead302ef0493b2d5511d5d60ccb510168 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Fri, 4 Sep 2026 11:07:40 +0200 Subject: [PATCH 2/9] [typemap] Preserve stack-based CRC64 hashing Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- ...rosoft.Android.Sdk.TrimmableTypeMap.csproj | 1 - .../Scanner/ScannerHashingHelper.cs | 42 +++++++++++++++---- 2 files changed, 35 insertions(+), 8 deletions(-) diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Microsoft.Android.Sdk.TrimmableTypeMap.csproj b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Microsoft.Android.Sdk.TrimmableTypeMap.csproj index f194f868819..222aba4ec74 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Microsoft.Android.Sdk.TrimmableTypeMap.csproj +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Microsoft.Android.Sdk.TrimmableTypeMap.csproj @@ -12,7 +12,6 @@ - diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/ScannerHashingHelper.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/ScannerHashingHelper.cs index 72375fef25d..f75c9ce4bba 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/ScannerHashingHelper.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/ScannerHashingHelper.cs @@ -8,6 +8,9 @@ namespace Microsoft.Android.Sdk.TrimmableTypeMap; internal static class ScannerHashingHelper { + const ulong Crc64Polynomial = 0x42F0E1EBA9EA3693; + static readonly ulong [] Crc64Lookup = CreateCrc64Lookup (); + internal static string ToLegacyCrc64 (string ns, string assemblyName) { int byteCount = GetNamespaceAssemblyUtf8ByteCount (ns, assemblyName); @@ -27,19 +30,44 @@ internal static string ToLegacyCrc64 (string ns, string assemblyName) internal static string ToCrc64 (string ns, string assemblyName) { + const int stackallocThresholdBytes = 256; int byteCount = GetNamespaceAssemblyUtf8ByteCount (ns, assemblyName); - var utf8Buffer = new byte [byteCount]; - int bytesWritten = GetNamespaceAssemblyUtf8Bytes (ns, assemblyName, utf8Buffer); - var hasher = new System.IO.Hashing.Crc64 (); - // MSBuild can load the netstandard2.0 hashing assembly first, whose Span signatures - // cannot bind from this net11.0 assembly. The array API is compatible in both contexts. - hasher.Append (utf8Buffer); - ulong hashValue = BinaryPrimitives.ReverseEndianness (hasher.GetCurrentHashAsUInt64 ()); + Span utf8Buffer = byteCount <= stackallocThresholdBytes + ? stackalloc byte [stackallocThresholdBytes] + : new byte [byteCount]; + + int bytesWritten = GetNamespaceAssemblyUtf8Bytes (ns, assemblyName, utf8Buffer.Slice (0, byteCount)); + ulong hashValue = BinaryPrimitives.ReverseEndianness (ComputeCrc64 (utf8Buffer.Slice (0, bytesWritten))); Span hash = stackalloc byte [8]; BinaryPrimitives.WriteUInt64LittleEndian (hash, hashValue ^ (ulong) bytesWritten); return HexUtilities.ToHexString (hash, upperCase: false); } + static ulong ComputeCrc64 (ReadOnlySpan source) + { + ulong crc = 0; + foreach (byte value in source) { + int index = (byte) ((crc >> 56) ^ value); + crc = Crc64Lookup [index] ^ (crc << 8); + } + return crc; + } + + static ulong [] CreateCrc64Lookup () + { + var lookup = new ulong [256]; + for (int i = 0; i < lookup.Length; i++) { + ulong value = (ulong) i << 56; + for (int bit = 0; bit < 8; bit++) { + value = (value & 0x8000000000000000) != 0 + ? (value << 1) ^ Crc64Polynomial + : value << 1; + } + lookup [i] = value; + } + return lookup; + } + static int GetNamespaceAssemblyUtf8ByteCount (string ns, string assemblyName) { return System.Text.Encoding.UTF8.GetByteCount (ns) + 1 + System.Text.Encoding.UTF8.GetByteCount (assemblyName); From 57bb5aa99e7a018319db9917fbdbebf1a66f7015 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Fri, 4 Sep 2026 11:08:31 +0200 Subject: [PATCH 3/9] [typemap] Use invariant Java source formatting Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../Tasks/GenerateTrimmableTypeMap.cs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/Microsoft.Android.Build.Tasks/Tasks/GenerateTrimmableTypeMap.cs b/src/Microsoft.Android.Build.Tasks/Tasks/GenerateTrimmableTypeMap.cs index 20e6b2f9b28..b62e6fe327a 100644 --- a/src/Microsoft.Android.Build.Tasks/Tasks/GenerateTrimmableTypeMap.cs +++ b/src/Microsoft.Android.Build.Tasks/Tasks/GenerateTrimmableTypeMap.cs @@ -547,9 +547,7 @@ static string GenerateApplicationRegistrationJava (IReadOnlyList registr sb.AppendLine ("\tpublic static void registerApplications ()"); sb.AppendLine ("\t{"); foreach (var javaClassName in registrationTypes) { - sb.Append ("\t\tmono.android.Runtime.registerNatives ("); - sb.Append (javaClassName); - sb.AppendLine (".class);"); + sb.AppendLine (CultureInfo.InvariantCulture, $"\t\tmono.android.Runtime.registerNatives ({javaClassName}.class);"); } sb.AppendLine ("\t}"); sb.AppendLine ("}"); From a3ad735adb30123ed0e15aa3074881a3b7bebe96 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Fri, 4 Sep 2026 11:30:15 +0200 Subject: [PATCH 4/9] [typemap] Use System.IO.Hashing with stack buffers Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../Microsoft.Android.Build.BaseTasks.csproj | 1 + .../SystemCrc64.cs | 20 +++++++++++ ...Microsoft.Android.Build.Tasks.Tests.csproj | 1 + ...rosoft.Android.Sdk.TrimmableTypeMap.csproj | 1 + .../Scanner/ScannerHashingHelper.cs | 34 +++++-------------- ...k.TrimmableTypeMap.IntegrationTests.csproj | 1 + ....Android.Sdk.TrimmableTypeMap.Tests.csproj | 1 + 7 files changed, 34 insertions(+), 25 deletions(-) create mode 100644 src/Microsoft.Android.Build.BaseTasks/SystemCrc64.cs diff --git a/src/Microsoft.Android.Build.BaseTasks/Microsoft.Android.Build.BaseTasks.csproj b/src/Microsoft.Android.Build.BaseTasks/Microsoft.Android.Build.BaseTasks.csproj index d99669434b4..0ce4510c44c 100644 --- a/src/Microsoft.Android.Build.BaseTasks/Microsoft.Android.Build.BaseTasks.csproj +++ b/src/Microsoft.Android.Build.BaseTasks/Microsoft.Android.Build.BaseTasks.csproj @@ -33,6 +33,7 @@ + diff --git a/src/Microsoft.Android.Build.BaseTasks/SystemCrc64.cs b/src/Microsoft.Android.Build.BaseTasks/SystemCrc64.cs new file mode 100644 index 00000000000..ef1befcd2bd --- /dev/null +++ b/src/Microsoft.Android.Build.BaseTasks/SystemCrc64.cs @@ -0,0 +1,20 @@ +using System; + +namespace Microsoft.Android.Build.Tasks; + +static class SystemCrc64 +{ + public static unsafe void Hash (byte* source, int sourceLength, byte* destination, int destinationLength) + { + if (sourceLength < 0) { + throw new ArgumentOutOfRangeException (nameof (sourceLength)); + } + if (destinationLength < 0) { + throw new ArgumentOutOfRangeException (nameof (destinationLength)); + } + + System.IO.Hashing.Crc64.Hash ( + new ReadOnlySpan (source, sourceLength), + new Span (destination, destinationLength)); + } +} diff --git a/src/Microsoft.Android.Build.Tasks/Tests/Microsoft.Android.Build.Tasks.Tests/Microsoft.Android.Build.Tasks.Tests.csproj b/src/Microsoft.Android.Build.Tasks/Tests/Microsoft.Android.Build.Tasks.Tests/Microsoft.Android.Build.Tasks.Tests.csproj index 8e92f0383b8..648f082d2c4 100644 --- a/src/Microsoft.Android.Build.Tasks/Tests/Microsoft.Android.Build.Tasks.Tests/Microsoft.Android.Build.Tasks.Tests.csproj +++ b/src/Microsoft.Android.Build.Tasks/Tests/Microsoft.Android.Build.Tasks.Tests/Microsoft.Android.Build.Tasks.Tests.csproj @@ -18,6 +18,7 @@ + diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Microsoft.Android.Sdk.TrimmableTypeMap.csproj b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Microsoft.Android.Sdk.TrimmableTypeMap.csproj index 222aba4ec74..6196aea88ed 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Microsoft.Android.Sdk.TrimmableTypeMap.csproj +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Microsoft.Android.Sdk.TrimmableTypeMap.csproj @@ -13,6 +13,7 @@ + diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/ScannerHashingHelper.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/ScannerHashingHelper.cs index f75c9ce4bba..7cdd638b021 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/ScannerHashingHelper.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/ScannerHashingHelper.cs @@ -1,16 +1,16 @@ +extern alias BaseTasks; + using System; using System.Buffers; using System.Buffers.Binary; using Java.Interop.Tools.JavaCallableWrappers; using Microsoft.Android.Build.Tasks; +using SystemCrc64 = BaseTasks::Microsoft.Android.Build.Tasks.SystemCrc64; namespace Microsoft.Android.Sdk.TrimmableTypeMap; internal static class ScannerHashingHelper { - const ulong Crc64Polynomial = 0x42F0E1EBA9EA3693; - static readonly ulong [] Crc64Lookup = CreateCrc64Lookup (); - internal static string ToLegacyCrc64 (string ns, string assemblyName) { int byteCount = GetNamespaceAssemblyUtf8ByteCount (ns, assemblyName); @@ -37,35 +37,19 @@ internal static string ToCrc64 (string ns, string assemblyName) : new byte [byteCount]; int bytesWritten = GetNamespaceAssemblyUtf8Bytes (ns, assemblyName, utf8Buffer.Slice (0, byteCount)); - ulong hashValue = BinaryPrimitives.ReverseEndianness (ComputeCrc64 (utf8Buffer.Slice (0, bytesWritten))); Span hash = stackalloc byte [8]; + ComputeCrc64 (utf8Buffer.Slice (0, bytesWritten), hash); + ulong hashValue = BinaryPrimitives.ReadUInt64LittleEndian (hash); BinaryPrimitives.WriteUInt64LittleEndian (hash, hashValue ^ (ulong) bytesWritten); return HexUtilities.ToHexString (hash, upperCase: false); } - static ulong ComputeCrc64 (ReadOnlySpan source) - { - ulong crc = 0; - foreach (byte value in source) { - int index = (byte) ((crc >> 56) ^ value); - crc = Crc64Lookup [index] ^ (crc << 8); - } - return crc; - } - - static ulong [] CreateCrc64Lookup () + static unsafe void ComputeCrc64 (ReadOnlySpan source, Span destination) { - var lookup = new ulong [256]; - for (int i = 0; i < lookup.Length; i++) { - ulong value = (ulong) i << 56; - for (int bit = 0; bit < 8; bit++) { - value = (value & 0x8000000000000000) != 0 - ? (value << 1) ^ Crc64Polynomial - : value << 1; - } - lookup [i] = value; + fixed (byte* sourcePointer = source) + fixed (byte* destinationPointer = destination) { + SystemCrc64.Hash (sourcePointer, source.Length, destinationPointer, destination.Length); } - return lookup; } static int GetNamespaceAssemblyUtf8ByteCount (string ns, string assemblyName) diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests/Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests.csproj b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests/Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests.csproj index 7e5b7d0bd14..975de21db45 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests/Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests.csproj +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests/Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests.csproj @@ -26,6 +26,7 @@ + @@ -13,19 +13,33 @@ Microsoft.Android.Tasks enable Nullable - $(MicrosoftAndroidSdkOutDir) + + $(MicrosoftAndroidSdkOutDir)net\ false true ..\..\product.snk - + false + + + + + + + diff --git a/src/Microsoft.Android.Build.Tasks/Tests/Microsoft.Android.Build.Tasks.Tests/Microsoft.Android.Build.Tasks.Tests.csproj b/src/Microsoft.Android.Build.Tasks/Tests/Microsoft.Android.Build.Tasks.Tests/Microsoft.Android.Build.Tasks.Tests.csproj index 648f082d2c4..8e92f0383b8 100644 --- a/src/Microsoft.Android.Build.Tasks/Tests/Microsoft.Android.Build.Tasks.Tests/Microsoft.Android.Build.Tasks.Tests.csproj +++ b/src/Microsoft.Android.Build.Tasks/Tests/Microsoft.Android.Build.Tasks.Tests/Microsoft.Android.Build.Tasks.Tests.csproj @@ -18,7 +18,6 @@ - diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Microsoft.Android.Sdk.TrimmableTypeMap.csproj b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Microsoft.Android.Sdk.TrimmableTypeMap.csproj index 6196aea88ed..f194f868819 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Microsoft.Android.Sdk.TrimmableTypeMap.csproj +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Microsoft.Android.Sdk.TrimmableTypeMap.csproj @@ -12,8 +12,8 @@ + - diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/ScannerHashingHelper.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/ScannerHashingHelper.cs index 7cdd638b021..4ac2731d306 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/ScannerHashingHelper.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/ScannerHashingHelper.cs @@ -1,11 +1,8 @@ -extern alias BaseTasks; - using System; using System.Buffers; using System.Buffers.Binary; using Java.Interop.Tools.JavaCallableWrappers; using Microsoft.Android.Build.Tasks; -using SystemCrc64 = BaseTasks::Microsoft.Android.Build.Tasks.SystemCrc64; namespace Microsoft.Android.Sdk.TrimmableTypeMap; @@ -38,20 +35,12 @@ internal static string ToCrc64 (string ns, string assemblyName) int bytesWritten = GetNamespaceAssemblyUtf8Bytes (ns, assemblyName, utf8Buffer.Slice (0, byteCount)); Span hash = stackalloc byte [8]; - ComputeCrc64 (utf8Buffer.Slice (0, bytesWritten), hash); + System.IO.Hashing.Crc64.Hash (utf8Buffer.Slice (0, bytesWritten), hash); ulong hashValue = BinaryPrimitives.ReadUInt64LittleEndian (hash); BinaryPrimitives.WriteUInt64LittleEndian (hash, hashValue ^ (ulong) bytesWritten); return HexUtilities.ToHexString (hash, upperCase: false); } - static unsafe void ComputeCrc64 (ReadOnlySpan source, Span destination) - { - fixed (byte* sourcePointer = source) - fixed (byte* destinationPointer = destination) { - SystemCrc64.Hash (sourcePointer, source.Length, destinationPointer, destination.Length); - } - } - static int GetNamespaceAssemblyUtf8ByteCount (string ns, string assemblyName) { return System.Text.Encoding.UTF8.GetByteCount (ns) + 1 + System.Text.Encoding.UTF8.GetByteCount (assemblyName); diff --git a/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.targets b/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.targets index ee2422a58b3..ac248037e63 100644 --- a/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.targets +++ b/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.targets @@ -5,7 +5,7 @@ Android <_XamarinAndroidBuildTasksAssembly>..\tools\Xamarin.Android.Build.Tasks.dll - <_MicrosoftAndroidBuildTasksAssembly>..\tools\Microsoft.Android.Build.Tasks.dll + <_MicrosoftAndroidBuildTasksAssembly>..\tools\net\Microsoft.Android.Build.Tasks.dll true true diff --git a/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Common.targets b/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Common.targets index 09cf0e73152..29c997c4a4a 100644 --- a/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Common.targets +++ b/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Common.targets @@ -44,8 +44,8 @@ Copyright (C) 2011-2012 Xamarin. All rights reserved. - - + + diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests/Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests.csproj b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests/Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests.csproj index 975de21db45..7e5b7d0bd14 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests/Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests.csproj +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests/Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests.csproj @@ -26,7 +26,6 @@ - - - Xamarin.Android.Tasks.Properties.Resources + + Xamarin.Android.Tasks.Properties.%(Filename) diff --git a/src/Microsoft.Android.Build.Tasks/Tests/Microsoft.Android.Build.Tasks.Tests/ResourcesTests.cs b/src/Microsoft.Android.Build.Tasks/Tests/Microsoft.Android.Build.Tasks.Tests/ResourcesTests.cs new file mode 100644 index 00000000000..4e4433494e5 --- /dev/null +++ b/src/Microsoft.Android.Build.Tasks/Tests/Microsoft.Android.Build.Tasks.Tests/ResourcesTests.cs @@ -0,0 +1,22 @@ +using System.Globalization; +using NUnit.Framework; + +namespace Xamarin.Android.Build.Tests; + +[TestFixture] +public class ResourcesTests +{ + [Test] + public void LocalizedResourcesAreAvailable () + { + var originalCulture = CultureInfo.CurrentUICulture; + try { + CultureInfo.CurrentUICulture = CultureInfo.GetCultureInfo ("de"); + + StringAssert.StartsWith ("Der Typ", Xamarin.Android.Tasks.Properties.Resources.XA4250); + StringAssert.StartsWith ("[ExportField] kann", Java.Interop.Localization.Resources.JavaCallableWrappers_XA4205); + } finally { + CultureInfo.CurrentUICulture = originalCulture; + } + } +} From 94c0e9baab3eaa01214df75b51355a2cd4530519 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Fri, 4 Sep 2026 13:22:33 +0200 Subject: [PATCH 7/9] [xabt] Assert localization without hardcoding translations Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../ResourcesTests.cs | 31 +++++++++++++------ 1 file changed, 21 insertions(+), 10 deletions(-) diff --git a/src/Microsoft.Android.Build.Tasks/Tests/Microsoft.Android.Build.Tasks.Tests/ResourcesTests.cs b/src/Microsoft.Android.Build.Tasks/Tests/Microsoft.Android.Build.Tasks.Tests/ResourcesTests.cs index 4e4433494e5..539215166e5 100644 --- a/src/Microsoft.Android.Build.Tasks/Tests/Microsoft.Android.Build.Tasks.Tests/ResourcesTests.cs +++ b/src/Microsoft.Android.Build.Tasks/Tests/Microsoft.Android.Build.Tasks.Tests/ResourcesTests.cs @@ -1,4 +1,5 @@ using System.Globalization; +using System.Resources; using NUnit.Framework; namespace Xamarin.Android.Build.Tests; @@ -6,17 +7,27 @@ namespace Xamarin.Android.Build.Tests; [TestFixture] public class ResourcesTests { - [Test] - public void LocalizedResourcesAreAvailable () + // The tasks in this assembly report XA#### diagnostics through resources linked from + // Xamarin.Android.Build.Tasks, and [Export] diagnostics through Java.Interop.Localization. + // Both must keep resolving their satellite assemblies after the move to this assembly. + // Assert that a translation exists and differs from English rather than asserting on the + // translated text itself, which the localization pipeline regenerates regularly. + [TestCase ("de")] + [TestCase ("ja")] + public void TaskDiagnosticsAreLocalized (string culture) { - var originalCulture = CultureInfo.CurrentUICulture; - try { - CultureInfo.CurrentUICulture = CultureInfo.GetCultureInfo ("de"); + AssertLocalized (Xamarin.Android.Tasks.Properties.Resources.ResourceManager, "XA4250", culture); + AssertLocalized (Java.Interop.Localization.Resources.ResourceManager, "JavaCallableWrappers_XA4205", culture); + } + + static void AssertLocalized (ResourceManager resourceManager, string name, string culture) + { + var neutral = resourceManager.GetString (name, CultureInfo.InvariantCulture); + Assert.IsNotNull (neutral, $"'{name}' should exist in the neutral resources."); - StringAssert.StartsWith ("Der Typ", Xamarin.Android.Tasks.Properties.Resources.XA4250); - StringAssert.StartsWith ("[ExportField] kann", Java.Interop.Localization.Resources.JavaCallableWrappers_XA4205); - } finally { - CultureInfo.CurrentUICulture = originalCulture; - } + var translated = resourceManager.GetString (name, CultureInfo.GetCultureInfo (culture)); + Assert.IsNotNull (translated, $"'{name}' should exist in the '{culture}' resources."); + Assert.AreNotEqual (neutral, translated, + $"'{name}' fell back to English, so the '{culture}' satellite assembly was not found."); } } From 25d10c5a7b0a532afa6a3929b7e7bcd54447a492 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Fri, 4 Sep 2026 13:46:05 +0200 Subject: [PATCH 8/9] [typemap] Validate custom view map entries Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../Tasks/GenerateTrimmableTypeMap.cs | 20 +++++++------- .../GenerateTrimmableTypeMapTests.cs | 26 +++++++++++++++++++ 2 files changed, 37 insertions(+), 9 deletions(-) diff --git a/src/Microsoft.Android.Build.Tasks/Tasks/GenerateTrimmableTypeMap.cs b/src/Microsoft.Android.Build.Tasks/Tasks/GenerateTrimmableTypeMap.cs index b62e6fe327a..40ab6bc7178 100644 --- a/src/Microsoft.Android.Build.Tasks/Tasks/GenerateTrimmableTypeMap.cs +++ b/src/Microsoft.Android.Build.Tasks/Tasks/GenerateTrimmableTypeMap.cs @@ -376,21 +376,23 @@ static bool IsFrameworkAssemblyItem (ITaskItem item) => item.GetMetadata ("NuGetPackageId").StartsWith ("Microsoft.NETCore.App.Runtime.", StringComparison.OrdinalIgnoreCase) || item.GetMetadata ("NuGetPackageId").StartsWith ("Microsoft.Android.Runtime.", StringComparison.OrdinalIgnoreCase); - static IReadOnlyCollection LoadCustomViewTypeNames (string mapFile) + internal static IReadOnlyCollection LoadCustomViewTypeNames (string mapFile) { - var map = new Dictionary> (); + var typeNames = new HashSet (StringComparer.Ordinal); if (!File.Exists (mapFile)) { - return map.Keys; + return typeNames; } foreach (var line in File.ReadLines (mapFile)) { - var items = line.Split (new char [] { ';' }, count: 2); - if (!map.TryGetValue (items [0], out var values)) { - values = new HashSet (); - map.Add (items [0], values); + if (line.IsNullOrWhiteSpace ()) { + continue; + } + int separator = line.IndexOf (';'); + if (separator <= 0 || separator == line.Length - 1) { + throw new InvalidDataException ($"Invalid custom view map entry '{line}' in '{mapFile}'."); } - values.Add (items [1]); + typeNames.Add (line.Substring (0, separator)); } - return map.Keys; + return typeNames; } void WriteGeneratedAssembliesListFile (IReadOnlyList assemblies) diff --git a/src/Microsoft.Android.Build.Tasks/Tests/Microsoft.Android.Build.Tasks.Tests/GenerateTrimmableTypeMapTests.cs b/src/Microsoft.Android.Build.Tasks/Tests/Microsoft.Android.Build.Tasks.Tests/GenerateTrimmableTypeMapTests.cs index 0e1bcef42a0..283dc2cf301 100644 --- a/src/Microsoft.Android.Build.Tasks/Tests/Microsoft.Android.Build.Tasks.Tests/GenerateTrimmableTypeMapTests.cs +++ b/src/Microsoft.Android.Build.Tasks/Tests/Microsoft.Android.Build.Tasks.Tests/GenerateTrimmableTypeMapTests.cs @@ -46,6 +46,32 @@ public void Execute_InvalidTargetFrameworkVersion_Fails () Assert.IsNotEmpty (errors, "Should have logged an error."); } + [Test] + public void LoadCustomViewTypeNames_ParsesKeysAndIgnoresBlankLines () + { + var directory = Path.Combine (Root, "temp", TestName); + var mapFile = Path.Combine (directory, "custom-view-map.txt"); + Directory.CreateDirectory (directory); + File.WriteAllText (mapFile, "Example.View;example.View\n\nExample.View;example.OtherView\nOther.View;other.View\n"); + + var typeNames = GenerateTrimmableTypeMap.LoadCustomViewTypeNames (mapFile); + + CollectionAssert.AreEquivalent (new [] { "Example.View", "Other.View" }, typeNames); + } + + [Test] + public void LoadCustomViewTypeNames_InvalidEntryThrows () + { + var directory = Path.Combine (Root, "temp", TestName); + var mapFile = Path.Combine (directory, "custom-view-map.txt"); + Directory.CreateDirectory (directory); + File.WriteAllText (mapFile, "invalid"); + + var exception = Assert.Throws (() => GenerateTrimmableTypeMap.LoadCustomViewTypeNames (mapFile)); + + StringAssert.Contains ("Invalid custom view map entry 'invalid'", exception?.Message); + } + [Test] public void Execute_WithMonoAndroid_ProducesOutputs () { From 0536798810c9dbdb957fb6a2ae3c0f7fe5645e3a Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Sat, 5 Sep 2026 00:36:42 +0200 Subject: [PATCH 9/9] [xabt] Reuse modern task assembly property Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../Xamarin.Android.Common.targets | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Common.targets b/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Common.targets index 29c997c4a4a..0116afdbe41 100644 --- a/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Common.targets +++ b/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Common.targets @@ -44,8 +44,8 @@ Copyright (C) 2011-2012 Xamarin. All rights reserved. - - + +