From 8978bb9f89bceb3aa0f17e6a3a76ead0358cd678 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Thu, 16 Apr 2026 07:47:39 +0200 Subject: [PATCH 1/6] [TrimmableTypeMap] Default instrumentation targetPackage to app package name The legacy ManifestDocument automatically sets targetPackage to the app's PackageName when [Instrumentation] doesn't specify it. Without this, the generated manifest has without android:targetPackage, causing INSTALL_PARSE_FAILED_MANIFEST_MALFORMED on the device. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> (cherry picked from commit 70deb3089a2ce2b8e36f08d7286486a229a84fb1) --- .../Generator/ComponentElementBuilder.cs | 7 ++++++- .../Generator/ManifestGenerator.cs | 2 +- .../Generator/ManifestGeneratorTests.cs | 20 +++++++++++++++++++ 3 files changed, 27 insertions(+), 2 deletions(-) diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ComponentElementBuilder.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ComponentElementBuilder.cs index e382556c748..5d4957fc212 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ComponentElementBuilder.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ComponentElementBuilder.cs @@ -165,7 +165,7 @@ internal static void UpdateApplicationElement (XElement app, JavaPeerInfo peer) PropertyMapper.ApplyMappings (app, component.Properties, PropertyMapper.ApplicationElementMappings); } - internal static void AddInstrumentation (XElement manifest, JavaPeerInfo peer) + internal static void AddInstrumentation (XElement manifest, JavaPeerInfo peer, string packageName) { string jniName = JniSignatureHelper.JniNameToJavaName (peer.JavaName); var element = new XElement ("instrumentation", @@ -177,6 +177,11 @@ internal static void AddInstrumentation (XElement manifest, JavaPeerInfo peer) } PropertyMapper.ApplyMappings (element, component.Properties, PropertyMapper.InstrumentationMappings); + // Default targetPackage to the app package name, matching legacy ManifestDocument behavior + if (element.Attribute (AndroidNs + "targetPackage") is null) { + element.SetAttributeValue (AndroidNs + "targetPackage", packageName); + } + manifest.Add (element); } } diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ManifestGenerator.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ManifestGenerator.cs index 5b2d6204eb5..9f64a305023 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ManifestGenerator.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ManifestGenerator.cs @@ -73,7 +73,7 @@ class ManifestGenerator } if (peer.ComponentAttribute.Kind == ComponentKind.Instrumentation) { - ComponentElementBuilder.AddInstrumentation (manifest, peer); + ComponentElementBuilder.AddInstrumentation (manifest, peer, PackageName); continue; } diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/ManifestGeneratorTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/ManifestGeneratorTests.cs index 07929a9d5a4..e09608c974f 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/ManifestGeneratorTests.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/ManifestGeneratorTests.cs @@ -253,6 +253,26 @@ public void Instrumentation_GoesToManifest () Assert.Null (appInstrumentation); } + [Fact] + public void Instrumentation_DefaultsTargetPackage () + { + var gen = CreateDefaultGenerator (); + var peer = CreatePeer ("com/example/app/MyInstrumentation", new ComponentInfo { + Kind = ComponentKind.Instrumentation, + Properties = new Dictionary { + ["Label"] = "My Test", + }, + }); + + var doc = GenerateAndLoad (gen, [peer]); + + var instrumentation = doc.Root?.Element ("instrumentation"); + Assert.NotNull (instrumentation); + + // targetPackage should default to the app's PackageName + Assert.Equal ("com.example.app", (string?)instrumentation?.Attribute (AndroidNs + "targetPackage")); + } + [Fact] public void RuntimeProvider_Added () { From a29c17fee5c994374343e6d480fd28077e9d9cd0 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Thu, 16 Apr 2026 09:23:25 +0200 Subject: [PATCH 2/6] [TrimmableTypeMap] Fix stale manifest causing ClassNotFoundException on mode switch The CoreCLRTrimmable CI test was crashing with: ClassNotFoundException: Didn't find class "android.apptests.App" Root cause: when switching from the default (LLVM IR) typemap build to the trimmable path in the same intermediate directory (sequential CI test runs), _GenerateTrimmableTypeMap was incorrectly skipped because its output DLL existed from a prior run. The stale LLVM IR manifest (which uses compat JNI names like "android.apptests.App") remained while the JCW was generated with the CRC-based name ("crc64.../App"), causing a name mismatch at runtime. Fixes: 1. Add a sentinel file (.trimmable) written when _GenerateTrimmableTypeMap runs. A new target _CleanStaleNonTrimmableState deletes the stale typemap DLL when the sentinel is missing, forcing regeneration. 2. Pass extraBuildArgs to the Clean step in apk-instrumentation.yaml so the Clean imports the same targets as the build and properly cleans trimmable-specific files. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> (cherry picked from commit f719fb18bfec3dc0c332c8f3c4fce885d0fa3362) --- .../yaml-templates/apk-instrumentation.yaml | 2 +- ...soft.Android.Sdk.TypeMap.Trimmable.targets | 20 +++++++++++++++++++ 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/build-tools/automation/yaml-templates/apk-instrumentation.yaml b/build-tools/automation/yaml-templates/apk-instrumentation.yaml index 81468c6c681..3ab581056f5 100644 --- a/build-tools/automation/yaml-templates/apk-instrumentation.yaml +++ b/build-tools/automation/yaml-templates/apk-instrumentation.yaml @@ -47,7 +47,7 @@ steps: configuration: ${{ parameters.buildConfiguration }} xaSourcePath: ${{ parameters.xaSourcePath }} project: ${{ parameters.project }} - arguments: -t:Clean -c ${{ parameters.configuration }} --no-restore + arguments: -t:Clean -c ${{ parameters.configuration }} --no-restore ${{ parameters.extraBuildArgs }} displayName: Clean ${{ parameters.testName }} condition: ${{ parameters.condition }} continueOnError: false 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 d38aad44fb9..8e4c98f1337 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 @@ -33,6 +33,20 @@ + + + + + + <_TypeMapBaseOutputDir Condition=" '$(_OuterIntermediateOutputPath)' != '' ">$(_OuterIntermediateOutputPath) + <_TypeMapBaseOutputDir Condition=" '$(_TypeMapBaseOutputDir)' == '' ">$(IntermediateOutputPath) <_TypeMapBaseOutputDir>$(_TypeMapBaseOutputDir.Replace('\','/')) <_TypeMapOutputDirectory>$(_TypeMapBaseOutputDir)typemap/ <_TypeMapJavaOutputDirectory>$(_TypeMapBaseOutputDir)typemap/java From 2baf64ec0d7492d7acbef62984d51e792c1b821e Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Thu, 16 Apr 2026 14:52:46 +0200 Subject: [PATCH 4/6] [TrimmableTypeMap] Rewrite compat JNI names in manifest template to CRC names Manifest templates may hardcode compat JNI names (e.g., android.apptests.App) but the trimmable JCW generator uses CRC-based names (e.g., crc64.../App). The compat name rewrite must happen before collecting existingTypes so the duplicate check works correctly and we don't end up with both versions. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> (cherry picked from commit 1f221296530ae240ec10140f8371ceea79307089) --- .../Generator/ManifestGenerator.cs | 38 ++++++++++++++ .../Generator/ManifestGeneratorTests.cs | 51 +++++++++++++++++++ 2 files changed, 89 insertions(+) diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ManifestGenerator.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ManifestGenerator.cs index 9f64a305023..447ff0df569 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ManifestGenerator.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ManifestGenerator.cs @@ -52,6 +52,10 @@ class ManifestGenerator EnsureManifestAttributes (manifest); var app = EnsureApplicationElement (manifest); + // Rewrite compat JNI names in the template to CRC names BEFORE collecting + // existing types, so the duplicate check works correctly. + RewriteCompatNames (manifest, allPeers); + // Apply assembly-level [Application] properties if (assemblyInfo.ApplicationProperties is not null) { AssemblyLevelElementBuilder.ApplyApplicationProperties (app, assemblyInfo.ApplicationProperties, allPeers, Warn); @@ -130,6 +134,40 @@ XDocument CreateDefaultManifest () new XAttribute ("package", PackageName))); } + /// + /// Manifest templates may use compat JNI names (e.g., "android.apptests.App") + /// but the trimmable path generates JCWs with CRC-based names (e.g., "crc64.../App"). + /// This method rewrites any compat name references to the actual JCW name so the + /// Android runtime can find the class. + /// + void RewriteCompatNames (XElement manifest, IReadOnlyList allPeers) + { + // Build mapping: compat Java name → CRC Java name + var compatToCrc = new Dictionary (StringComparer.Ordinal); + foreach (var peer in allPeers) { + string javaName = JniSignatureHelper.JniNameToJavaName (peer.JavaName); + string compatName = JniSignatureHelper.JniNameToJavaName (peer.CompatJniName); + if (javaName != compatName) { + compatToCrc [compatName] = javaName; + } + } + + if (compatToCrc.Count == 0) { + return; + } + + // Rewrite android:name attributes throughout the manifest + foreach (var element in manifest.DescendantsAndSelf ()) { + var nameAttr = element.Attribute (AttName); + if (nameAttr is null) { + continue; + } + if (compatToCrc.TryGetValue (nameAttr.Value, out var crcName)) { + nameAttr.Value = crcName; + } + } + } + void EnsureManifestAttributes (XElement manifest) { manifest.SetAttributeValue (XNamespace.Xmlns + "android", AndroidNs.NamespaceName); diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/ManifestGeneratorTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/ManifestGeneratorTests.cs index e09608c974f..4668c293b83 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/ManifestGeneratorTests.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/ManifestGeneratorTests.cs @@ -273,6 +273,57 @@ public void Instrumentation_DefaultsTargetPackage () Assert.Equal ("com.example.app", (string?)instrumentation?.Attribute (AndroidNs + "targetPackage")); } + [Fact] + public void CompatNames_RewrittenToCrc () + { + var gen = CreateDefaultGenerator (); + + // Template uses compat names + var template = ParseTemplate (""" + + + + + + """); + + // Peer has CRC JavaName but compat CompatJniName + var appPeer = new JavaPeerInfo { + JavaName = "crc64abc123/MyApp", + CompatJniName = "com/example/app/MyApp", + ManagedTypeName = "Com.Example.App.MyApp", + ManagedTypeNamespace = "Com.Example.App", + ManagedTypeShortName = "MyApp", + AssemblyName = "TestApp", + ComponentAttribute = new ComponentInfo { + Kind = ComponentKind.Application, + Properties = new Dictionary (), + }, + }; + var activityPeer = new JavaPeerInfo { + JavaName = "crc64def456/MainActivity", + CompatJniName = "com/example/app/MainActivity", + ManagedTypeName = "Com.Example.App.MainActivity", + ManagedTypeNamespace = "Com.Example.App", + ManagedTypeShortName = "MainActivity", + AssemblyName = "TestApp", + ComponentAttribute = new ComponentInfo { + Kind = ComponentKind.Activity, + Properties = new Dictionary (), + }, + }; + + var doc = GenerateAndLoad (gen, [appPeer, activityPeer], template: template); + + var app = doc.Root?.Element ("application"); + Assert.NotNull (app); + Assert.Equal ("crc64abc123.MyApp", (string?)app?.Attribute (AttName)); + + var activity = app?.Element ("activity"); + Assert.NotNull (activity); + Assert.Equal ("crc64def456.MainActivity", (string?)activity?.Attribute (AttName)); + } + [Fact] public void RuntimeProvider_Added () { From 01fc77a8a5a2f4ac147122fdd9b515da5dcf1a4a Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Fri, 17 Apr 2026 13:40:14 +0200 Subject: [PATCH 5/6] [TrimmableTypeMap] Address review feedback on manifest fixes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Resolve relative `android:name` forms (".Type", bare "Type") before the compat→CRC rewrite in ManifestGenerator. The previous implementation only matched fully-qualified dot-names and would silently leave relative/ unqualified names pointing at the compat Java name, also breaking the duplicate-detection check (it would emit a second CRC-named component next to the original relative entry). Extract TrimmableTypeMapGenerator's existing ResolveManifestClassName into a shared ManifestNameResolver helper. - Drop the `_CleanStaleNonTrimmableState` target and the `.trimmable` sentinel file in Microsoft.Android.Sdk.TypeMap.Trimmable.targets. The sentinel-based cleanup was narrow (only deleted the typemap DLL), asymmetric (only covered llvm-ir → trimmable), and a one-off pattern. Replace with the idiomatic fix: add `_AndroidTypeMapImplementation` to `_PropertyCacheItems` in Xamarin.Android.Common.targets. This hooks typemap-mode switches into the existing `_CleanIntermediateIfNeeded` → `_CleanMonoAndroidIntermediateDir` mechanism used by ~30 other build-shape properties (AOT mode, link mode, package format, etc.), so all stale intermediate artifacts (typemap DLL, JCWs, manifest, acw-map) are cleaned on every switch in both directions, and pre-existing `obj/` directories from a prior release are handled too. - Add ManifestGeneratorTests cases for the ".Type" relative form, the bare "Type" unqualified form, and a regression guarding that duplicate-detection still works when a template references a peer by its relative name. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Generator/ManifestGenerator.cs | 13 +- .../Generator/ManifestNameResolver.cs | 20 +++ .../TrimmableTypeMapGenerator.cs | 15 +- ...soft.Android.Sdk.TypeMap.Trimmable.targets | 20 --- .../Xamarin.Android.Common.targets | 1 + .../Generator/ManifestGeneratorTests.cs | 142 ++++++++++++++++++ 6 files changed, 174 insertions(+), 37 deletions(-) create mode 100644 src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ManifestNameResolver.cs diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ManifestGenerator.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ManifestGenerator.cs index 447ff0df569..87bf502a659 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ManifestGenerator.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ManifestGenerator.cs @@ -142,7 +142,7 @@ XDocument CreateDefaultManifest () /// void RewriteCompatNames (XElement manifest, IReadOnlyList allPeers) { - // Build mapping: compat Java name → CRC Java name + // Build mapping: fully-qualified compat Java name → CRC Java name var compatToCrc = new Dictionary (StringComparer.Ordinal); foreach (var peer in allPeers) { string javaName = JniSignatureHelper.JniNameToJavaName (peer.JavaName); @@ -156,13 +156,20 @@ void RewriteCompatNames (XElement manifest, IReadOnlyList allPeers return; } - // Rewrite android:name attributes throughout the manifest + // Rewrite android:name attributes throughout the manifest. Android allows + // android:name to be specified as: + // - fully qualified ("com.example.app.MainActivity") + // - relative to the manifest package, starting with '.' (".MainActivity") + // - bare, with no '.' at all ("MainActivity"), also relative to the package + // Resolve to the fully-qualified form before the lookup, then write the CRC + // name back so duplicate detection later in the pipeline works correctly. foreach (var element in manifest.DescendantsAndSelf ()) { var nameAttr = element.Attribute (AttName); if (nameAttr is null) { continue; } - if (compatToCrc.TryGetValue (nameAttr.Value, out var crcName)) { + var resolved = ManifestNameResolver.Resolve (nameAttr.Value, PackageName); + if (compatToCrc.TryGetValue (resolved, out var crcName)) { nameAttr.Value = crcName; } } diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ManifestNameResolver.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ManifestNameResolver.cs new file mode 100644 index 00000000000..d44f750075f --- /dev/null +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ManifestNameResolver.cs @@ -0,0 +1,20 @@ +using System; + +namespace Microsoft.Android.Sdk.TrimmableTypeMap; + +static class ManifestNameResolver +{ + /// + /// Resolves an android:name value to a fully-qualified class name. + /// Names starting with '.' are relative to the package. Names with no '.' at all + /// are also treated as relative (Android tooling convention). + /// + public static string Resolve (string name, string packageName) + { + return name switch { + _ when name.StartsWith (".", StringComparison.Ordinal) => packageName + name, + _ when name.IndexOf ('.') < 0 && !packageName.IsNullOrEmpty () => packageName + "." + name, + _ => name, + }; + } +} diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/TrimmableTypeMapGenerator.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/TrimmableTypeMapGenerator.cs index 78ae91c7cfb..5dc065f59b7 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/TrimmableTypeMapGenerator.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/TrimmableTypeMapGenerator.cs @@ -165,7 +165,7 @@ internal void RootManifestReferencedTypes (List allPeers, XDocumen case "provider": var name = (string?) element.Attribute (attName); if (name is not null) { - var resolvedName = ResolveManifestClassName (name, packageName); + var resolvedName = ManifestNameResolver.Resolve (name, packageName); componentNames.Add (resolvedName); if (element.Name.LocalName is "application" or "instrumentation") { @@ -309,17 +309,4 @@ static void AddJniLookupNames (Dictionary> peersByDot } } - /// - /// Resolves an android:name value to a fully-qualified class name. - /// Names starting with '.' are relative to the package. Names with no '.' at all - /// are also treated as relative (Android tooling convention). - /// - static string ResolveManifestClassName (string name, string packageName) - { - return name switch { - _ when name.StartsWith (".", StringComparison.Ordinal) => packageName + name, - _ when name.IndexOf ('.') < 0 && !packageName.IsNullOrEmpty () => packageName + "." + name, - _ => name, - }; - } } 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 d1e6e0f0f18..f84d03d1edd 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 @@ -37,20 +37,6 @@ - - - - -