From 59e8c7eac3e13ebd6a2a33f097e2b89441ea3519 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Wed, 15 Jul 2026 20:23:11 +0200 Subject: [PATCH 01/11] [TrimmableTypeMap] Prototype: emit per-assembly typemaps without the root Toward pre-generating Mono.Android's typemap at SDK build time (issue #10792): add a generateRootAssembly flag to TrimmableTypeMapGenerator.Execute (and a GenerateRootAssembly property on the GenerateTrimmableTypeMap task, default true). When false, the per-assembly typemap DLLs (e.g. _Mono.Android.TypeMap) and JCW Java sources are emitted, but the root _Microsoft.Android.TypeMaps assembly (TypeMapAssemblyTarget attributes + TypeMapLoader.Initialize) is not: that root is emitted by the app build, which will reference the pre-generated per-assembly typemap alongside the app's own. Adds a unit test asserting the per-assembly typemap is produced and the root is omitted when generateRootAssembly is false. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 867395ec-6705-4ed6-8a68-e25b58d431fc --- .../TrimmableTypeMapGenerator.cs | 28 +++++++++++++------ .../Tasks/GenerateTrimmableTypeMap.cs | 10 ++++++- .../TrimmableTypeMapGeneratorTests.cs | 19 +++++++++++++ 3 files changed, 47 insertions(+), 10 deletions(-) diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/TrimmableTypeMapGenerator.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/TrimmableTypeMapGenerator.cs index fee0811ebc4..638de6b3226 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/TrimmableTypeMapGenerator.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/TrimmableTypeMapGenerator.cs @@ -35,7 +35,8 @@ public TrimmableTypeMapResult Execute ( XDocument? manifestTemplate = null, string? packageNamingPolicy = null, int maxArrayRank = 0, - bool generateTypeMapAssemblies = true) + bool generateTypeMapAssemblies = true, + bool generateRootAssembly = true) { _ = assemblies ?? throw new ArgumentNullException (nameof (assemblies)); _ = systemRuntimeVersion ?? throw new ArgumentNullException (nameof (systemRuntimeVersion)); @@ -56,7 +57,7 @@ public TrimmableTypeMapResult Execute ( PropagateCannotRegisterToDescendants (allPeers); var generatedAssemblies = generateTypeMapAssemblies - ? GenerateTypeMapAssemblies (allPeers, systemRuntimeVersion, useSharedTypemapUniverse, maxArrayRank) + ? GenerateTypeMapAssemblies (allPeers, systemRuntimeVersion, useSharedTypemapUniverse, maxArrayRank, generateRootAssembly) : []; var jcwPeers = allPeers.Where (ShouldGenerateJcw).ToList (); logger.LogGeneratingJcwFilesInfo (jcwPeers.Count, allPeers.Count); @@ -179,7 +180,8 @@ List GenerateTypeMapAssemblies ( List allPeers, Version systemRuntimeVersion, bool useSharedTypemapUniverse, - int maxArrayRank) + int maxArrayRank, + bool generateRootAssembly = true) { List<(string AssemblyName, List Peers)> peersByAssembly; @@ -213,12 +215,20 @@ List GenerateTypeMapAssemblies ( generatedAssemblies.Add (new GeneratedAssembly (typeMapAssemblyName, stream)); logger.LogGeneratedTypeMapAssemblyInfo (typeMapAssemblyName, peers.Count); } - var rootStream = new MemoryStream (); - var rootGenerator = new RootTypeMapAssemblyGenerator (systemRuntimeVersion); - rootGenerator.Generate (perAssemblyNames, useSharedTypemapUniverse, rootStream, maxArrayRank: maxArrayRank); - rootStream.Position = 0; - generatedAssemblies.Add (new GeneratedAssembly ("_Microsoft.Android.TypeMaps", rootStream)); - logger.LogGeneratedRootTypeMapInfo (perAssemblyNames.Count); + // The root assembly (_Microsoft.Android.TypeMaps) carries the + // [assembly: TypeMapAssemblyTarget] attributes and TypeMapLoader.Initialize() that + // bind the per-assembly typemaps into universes at runtime. When pre-generating a + // framework typemap (e.g. Mono.Android) at SDK build time, the root is intentionally + // skipped: it is emitted by the app build, which references the pre-generated per-assembly + // typemap alongside the app's own. + if (generateRootAssembly) { + var rootStream = new MemoryStream (); + var rootGenerator = new RootTypeMapAssemblyGenerator (systemRuntimeVersion); + rootGenerator.Generate (perAssemblyNames, useSharedTypemapUniverse, rootStream, maxArrayRank: maxArrayRank); + rootStream.Position = 0; + generatedAssemblies.Add (new GeneratedAssembly ("_Microsoft.Android.TypeMaps", rootStream)); + logger.LogGeneratedRootTypeMapInfo (perAssemblyNames.Count); + } logger.LogGeneratedTypeMapAssembliesInfo (generatedAssemblies.Count); return generatedAssemblies; } diff --git a/src/Xamarin.Android.Build.Tasks/Tasks/GenerateTrimmableTypeMap.cs b/src/Xamarin.Android.Build.Tasks/Tasks/GenerateTrimmableTypeMap.cs index b1dcd144167..2100a972763 100644 --- a/src/Xamarin.Android.Build.Tasks/Tasks/GenerateTrimmableTypeMap.cs +++ b/src/Xamarin.Android.Build.Tasks/Tasks/GenerateTrimmableTypeMap.cs @@ -113,6 +113,13 @@ public void LogJniAddNativeMethodRegistrationAttributeError (string managedTypeN public string? CheckedBuild { get; set; } public string? ApplicationJavaClass { get; set; } public bool GenerateTypeMapAssemblies { get; set; } = true; + + // When false, the per-assembly typemap DLLs (and JCWs) are emitted but the root + // _Microsoft.Android.TypeMaps assembly is not. Used for SDK-build-time pre-generation of + // framework typemaps (e.g. Mono.Android, issue #10792); the app build emits the root, which + // references the pre-generated per-assembly typemaps alongside the app's own. + public bool GenerateRootAssembly { get; set; } = true; + public bool CleanJavaSourceOutputDirectory { get; set; } [Output] @@ -215,7 +222,8 @@ public override bool RunTask () manifestTemplate: manifestTemplate, packageNamingPolicy: PackageNamingPolicy, maxArrayRank: MaxArrayRank, - generateTypeMapAssemblies: GenerateTypeMapAssemblies); + generateTypeMapAssemblies: GenerateTypeMapAssemblies, + generateRootAssembly: GenerateRootAssembly); if (GenerateTypeMapAssemblies) { GeneratedAssemblies = WriteAssembliesToDisk (result.GeneratedAssemblies, assemblyInputs.Select (i => i.Path).ToList ()); diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TrimmableTypeMapGeneratorTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TrimmableTypeMapGeneratorTests.cs index fb9829ca648..061e7507c9c 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TrimmableTypeMapGeneratorTests.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TrimmableTypeMapGeneratorTests.cs @@ -89,6 +89,25 @@ public void Execute_WithTestFixtures_ProducesOutputs () Assert.Contains (result.GeneratedAssemblies, a => a.Name == "_TestFixtures.TypeMap"); } + [Fact] + public void Execute_WithGenerateRootAssemblyFalse_EmitsPerAssemblyTypeMapButNoRoot () + { + // SDK-time pre-generation (issue #10792) emits only the per-assembly typemap (and JCWs) + // for a framework assembly; the root _Microsoft.Android.TypeMaps is emitted by the app + // build, which references the pre-generated per-assembly typemap alongside its own. + using var peReader = CreateTestFixturePEReader (); + var result = CreateGenerator ().Execute ( + [Input ("TestFixtures", peReader)], + new Version (11, 0), + new HashSet (), + generateRootAssembly: false); + + Assert.Contains (result.GeneratedAssemblies, a => a.Name == "_TestFixtures.TypeMap"); + Assert.DoesNotContain (result.GeneratedAssemblies, a => a.Name == "_Microsoft.Android.TypeMaps"); + // JCWs are still produced so they can be pre-compiled into a jar shipped in the SDK pack. + Assert.NotEmpty (result.GeneratedJavaSources); + } + [Fact] public void Execute_CollectsDeferredRegistrationTypes_ForAllApplicationAndInstrumentationSubtypes () { From 24be1cdd495b242aac9d8aa28407d47a395bdcb3 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Wed, 15 Jul 2026 22:52:32 +0200 Subject: [PATCH 02/11] [TrimmableTypeMap] Root: reference pre-generated framework typemaps under Java.Lang.Object MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Special-case pre-generated framework typemaps (e.g. _Mono.Android.TypeMap, built at SDK build time per issue #10792) in the root typemap assembly. Such typemaps always use Java.Lang.Object as their universe anchor, so a single artifact serves both app-build universe modes: - RootTypeMapAssemblyGenerator.Generate gains sharedFrameworkTypeMapNames. They are emitted as [assembly: TypeMapAssemblyTarget("name")] regardless of useSharedTypemapUniverse. * Merged (Release): they join the single Java.Lang.Object universe, so GetOrCreateExternalTypeMapping() merges them with the app's entries automatically — no Initialize change. * Aggregate (Debug): TypeMapLoader.Initialize adds the Java.Lang.Object universe as element [0] alongside the app's per-assembly __TypeMapAnchor universes. - Threaded through TrimmableTypeMapGenerator.Execute/GenerateTypeMapAssemblies. - Aggregate + array maps (maxArrayRank > 0) with a shared framework universe throws NotSupportedException for now (tracked follow-up). Adds unit tests asserting the framework typemap is referenced under Java.Lang.Object (scope Mono.Android) in both modes, the app typemap keeps its own anchor in aggregate mode, and both produce valid PE. 619 tests pass. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 867395ec-6705-4ed6-8a68-e25b58d431fc --- .../Generator/RootTypeMapAssemblyGenerator.cs | 58 ++++++++++++++++--- .../TrimmableTypeMapGenerator.cs | 10 ++-- .../RootTypeMapAssemblyGeneratorTests.cs | 56 +++++++++++++++++- 3 files changed, 109 insertions(+), 15 deletions(-) diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/RootTypeMapAssemblyGenerator.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/RootTypeMapAssemblyGenerator.cs index de295c8d4a9..f53f6a985aa 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/RootTypeMapAssemblyGenerator.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/RootTypeMapAssemblyGenerator.cs @@ -80,7 +80,16 @@ public RootTypeMapAssemblyGenerator (Version systemRuntimeVersion) /// sentinels. Must match the value passed to the per-assembly generators. 0 means /// no array sentinels were emitted; the loader passes null for array maps. /// - public void Generate (IReadOnlyList perAssemblyTypeMapNames, bool useSharedTypemapUniverse, Stream stream, string? assemblyName = null, string? moduleName = null, int maxArrayRank = 0) + /// + /// Names of pre-generated framework per-assembly typemaps (e.g. _Mono.Android.TypeMap, + /// produced at SDK build time, issue #10792) that always use Java.Lang.Object as their + /// universe anchor regardless of . They are emitted as + /// [assembly: TypeMapAssemblyTarget<Java.Lang.Object>("name")] so the runtime loads + /// them into the Java.Lang.Object universe: in shared mode they merge into the single + /// app universe; in aggregate mode the loader adds the Java.Lang.Object universe alongside + /// the app's per-assembly universes. + /// + public void Generate (IReadOnlyList perAssemblyTypeMapNames, bool useSharedTypemapUniverse, Stream stream, string? assemblyName = null, string? moduleName = null, int maxArrayRank = 0, IReadOnlyList? sharedFrameworkTypeMapNames = null) { if (perAssemblyTypeMapNames is null) { throw new ArgumentNullException (nameof (perAssemblyTypeMapNames)); @@ -125,6 +134,16 @@ public void Generate (IReadOnlyList perAssemblyTypeMapNames, bool useSha } EmitArrayAssemblyTargetAttributes (pe, perAssemblyTypeMapNames, maxArrayRank); + // Pre-generated framework typemaps (e.g. _Mono.Android.TypeMap) always live in the + // Java.Lang.Object universe, so reference them under that anchor regardless of mode. + bool hasSharedFrameworkUniverse = sharedFrameworkTypeMapNames is { Count: > 0 }; + if (hasSharedFrameworkUniverse) { + var javaLangObjectHandle = pe.Metadata.AddTypeReference (pe.MonoAndroidRef, + pe.Metadata.GetOrAddString ("Java.Lang"), + pe.Metadata.GetOrAddString ("Object")); + EmitSharedUniverseAssemblyTargetAttributes (pe, javaLangObjectHandle, sharedFrameworkTypeMapNames!); + } + // Emit [assembly: IgnoresAccessChecksTo("...")] so TypeMapLoader.Initialize() can access // internal types (TrimmableTypeMap and friends in Mono.Android, and private anchors // in each per-assembly typemap DLL when aggregate universes or array maps are used). @@ -135,7 +154,7 @@ public void Generate (IReadOnlyList perAssemblyTypeMapNames, bool useSha pe.EmitIgnoresAccessChecksToAttribute (accessTargets); // Emit TypeMapLoader class with Initialize() method - EmitTypeMapLoader (pe, anchorTypeHandle, perAssemblyTypeMapNames, useSharedTypemapUniverse, maxArrayRank, assemblyName); + EmitTypeMapLoader (pe, anchorTypeHandle, perAssemblyTypeMapNames, useSharedTypemapUniverse, maxArrayRank, assemblyName, hasSharedFrameworkUniverse); pe.WritePE (stream); } @@ -201,7 +220,7 @@ static void EmitAssemblyTargetAttribute (PEAssemblyBuilder pe, MemberReferenceHa pe.Metadata.AddCustomAttribute (EntityHandle.AssemblyDefinition, ctorRef, blobHandle); } - static void EmitTypeMapLoader (PEAssemblyBuilder pe, EntityHandle anchorTypeHandle, IReadOnlyList perAssemblyTypeMapNames, bool useSharedTypemapUniverse, int maxArrayRank, string assemblyName) + static void EmitTypeMapLoader (PEAssemblyBuilder pe, EntityHandle anchorTypeHandle, IReadOnlyList perAssemblyTypeMapNames, bool useSharedTypemapUniverse, int maxArrayRank, string assemblyName, bool hasSharedFrameworkUniverse = false) { var metadata = pe.Metadata; @@ -240,6 +259,9 @@ static void EmitTypeMapLoader (PEAssemblyBuilder pe, EntityHandle anchorTypeHand var externalDictArrayTypeSpec = MakeIReadOnlyDictArrayTypeSpec (pe, iReadOnlyDictOpenRef, systemTypeRef, keyIsString: true); if (useSharedTypemapUniverse) { + // Single Java.Lang.Object universe. Pre-generated framework typemaps (if any) also + // anchor on Java.Lang.Object, so GetOrCreateExternalTypeMapping() + // already merges them in via their TypeMapAssemblyTarget attributes — no extra universe. if (maxArrayRank > 0) { var initializeRef = AddInitializeSingleWithArraysRef (pe, trimmableTypeMapRef, iReadOnlyDictOpenRef, systemTypeRef); EmitInitializeWithSingleTypeMap (pe, anchorTypeHandle, getExternalMemberRef, getProxyMemberRef, @@ -251,13 +273,19 @@ static void EmitTypeMapLoader (PEAssemblyBuilder pe, EntityHandle anchorTypeHand } else { var proxyDictTypeSpec = MakeIReadOnlyDictTypeSpec (pe, iReadOnlyDictOpenRef, systemTypeRef, keyIsString: false); if (maxArrayRank > 0) { + if (hasSharedFrameworkUniverse) { + // Debug + array maps (NativeAOT) combined with a pre-generated shared framework + // universe is not yet wired: the array-map aggregation is built per-assembly and + // would need an extra Java.Lang.Object array-map slot. Tracked as follow-up. + throw new NotSupportedException ("Pre-generated framework typemaps are not yet supported together with aggregate array maps (maxArrayRank > 0)."); + } var initializeRef = AddInitializeAggregateWithArraysRef (pe, trimmableTypeMapRef, iReadOnlyDictOpenRef, systemTypeRef); EmitInitializeWithAggregateTypeMap (pe, perAssemblyTypeMapNames, getExternalMemberRef, getProxyMemberRef, initializeRef, externalDictTypeSpec, proxyDictTypeSpec, externalDictArrayTypeSpec, iReadOnlyDictOpenRef, systemTypeRef, maxArrayRank, assemblyName); } else { var initializeRef = AddInitializeAggregateNoArraysRef (pe, trimmableTypeMapRef, iReadOnlyDictOpenRef, systemTypeRef); EmitInitializeWithAggregateTypeMapNoArrays (pe, perAssemblyTypeMapNames, getExternalMemberRef, getProxyMemberRef, - initializeRef, externalDictTypeSpec, proxyDictTypeSpec, iReadOnlyDictOpenRef, systemTypeRef, assemblyName); + initializeRef, externalDictTypeSpec, proxyDictTypeSpec, iReadOnlyDictOpenRef, systemTypeRef, assemblyName, hasSharedFrameworkUniverse); } } } @@ -348,18 +376,30 @@ static void EmitInitializeWithAggregateTypeMapNoArrays (PEAssemblyBuilder pe, MemberReferenceHandle initializeRef, TypeSpecificationHandle externalDictTypeSpec, TypeSpecificationHandle proxyDictTypeSpec, TypeReferenceHandle iReadOnlyDictOpenRef, TypeReferenceHandle systemTypeRef, - string assemblyName) + string assemblyName, + bool hasSharedFrameworkUniverse = false) { - var count = perAssemblyTypeMapNames.Count; + // When a pre-generated framework typemap is present (e.g. _Mono.Android.TypeMap), it lives in + // the Java.Lang.Object universe. Emit it as universe [0] (via + // GetOrCreate*TypeMapping()), followed by the app's per-assembly universes. + var count = perAssemblyTypeMapNames.Count + (hasSharedFrameworkUniverse ? 1 : 0); var getExternalSpecs = new EntityHandle [count]; var getProxySpecs = new EntityHandle [count]; - for (int i = 0; i < count; i++) { + int offset = 0; + if (hasSharedFrameworkUniverse) { + var javaLangObjectRef = pe.Metadata.AddTypeReference (pe.MonoAndroidRef, + pe.Metadata.GetOrAddString ("Java.Lang"), pe.Metadata.GetOrAddString ("Object")); + getExternalSpecs [0] = MakeGenericMethodSpec (pe, getExternalMemberRef, javaLangObjectRef); + getProxySpecs [0] = MakeGenericMethodSpec (pe, getProxyMemberRef, javaLangObjectRef); + offset = 1; + } + for (int i = 0; i < perAssemblyTypeMapNames.Count; i++) { var asmRef = pe.FindOrAddAssemblyRef (perAssemblyTypeMapNames [i]); var perAsmAnchorRef = pe.Metadata.AddTypeReference (asmRef, default, pe.Metadata.GetOrAddString ("__TypeMapAnchor")); - getExternalSpecs [i] = MakeGenericMethodSpec (pe, getExternalMemberRef, perAsmAnchorRef); - getProxySpecs [i] = MakeGenericMethodSpec (pe, getProxyMemberRef, perAsmAnchorRef); + getExternalSpecs [offset + i] = MakeGenericMethodSpec (pe, getExternalMemberRef, perAsmAnchorRef); + getProxySpecs [offset + i] = MakeGenericMethodSpec (pe, getProxyMemberRef, perAsmAnchorRef); } pe.EmitBody ("Initialize", diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/TrimmableTypeMapGenerator.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/TrimmableTypeMapGenerator.cs index 638de6b3226..ae0b917d0a6 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/TrimmableTypeMapGenerator.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/TrimmableTypeMapGenerator.cs @@ -36,7 +36,8 @@ public TrimmableTypeMapResult Execute ( string? packageNamingPolicy = null, int maxArrayRank = 0, bool generateTypeMapAssemblies = true, - bool generateRootAssembly = true) + bool generateRootAssembly = true, + IReadOnlyList? sharedFrameworkTypeMapNames = null) { _ = assemblies ?? throw new ArgumentNullException (nameof (assemblies)); _ = systemRuntimeVersion ?? throw new ArgumentNullException (nameof (systemRuntimeVersion)); @@ -57,7 +58,7 @@ public TrimmableTypeMapResult Execute ( PropagateCannotRegisterToDescendants (allPeers); var generatedAssemblies = generateTypeMapAssemblies - ? GenerateTypeMapAssemblies (allPeers, systemRuntimeVersion, useSharedTypemapUniverse, maxArrayRank, generateRootAssembly) + ? GenerateTypeMapAssemblies (allPeers, systemRuntimeVersion, useSharedTypemapUniverse, maxArrayRank, generateRootAssembly, sharedFrameworkTypeMapNames) : []; var jcwPeers = allPeers.Where (ShouldGenerateJcw).ToList (); logger.LogGeneratingJcwFilesInfo (jcwPeers.Count, allPeers.Count); @@ -181,7 +182,8 @@ List GenerateTypeMapAssemblies ( Version systemRuntimeVersion, bool useSharedTypemapUniverse, int maxArrayRank, - bool generateRootAssembly = true) + bool generateRootAssembly = true, + IReadOnlyList? sharedFrameworkTypeMapNames = null) { List<(string AssemblyName, List Peers)> peersByAssembly; @@ -224,7 +226,7 @@ List GenerateTypeMapAssemblies ( if (generateRootAssembly) { var rootStream = new MemoryStream (); var rootGenerator = new RootTypeMapAssemblyGenerator (systemRuntimeVersion); - rootGenerator.Generate (perAssemblyNames, useSharedTypemapUniverse, rootStream, maxArrayRank: maxArrayRank); + rootGenerator.Generate (perAssemblyNames, useSharedTypemapUniverse, rootStream, maxArrayRank: maxArrayRank, sharedFrameworkTypeMapNames: sharedFrameworkTypeMapNames); rootStream.Position = 0; generatedAssemblies.Add (new GeneratedAssembly ("_Microsoft.Android.TypeMaps", rootStream)); logger.LogGeneratedRootTypeMapInfo (perAssemblyNames.Count); diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/RootTypeMapAssemblyGeneratorTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/RootTypeMapAssemblyGeneratorTests.cs index 2ad242d096a..b0eda710196 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/RootTypeMapAssemblyGeneratorTests.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/RootTypeMapAssemblyGeneratorTests.cs @@ -12,11 +12,11 @@ namespace Microsoft.Android.Sdk.TrimmableTypeMap.Tests; public class RootTypeMapAssemblyGeneratorTests : FixtureTestBase { - static MemoryStream GenerateRootAssembly (IReadOnlyList perAssemblyNames, bool useSharedTypemapUniverse = false, string? assemblyName = null, int maxArrayRank = 0) + static MemoryStream GenerateRootAssembly (IReadOnlyList perAssemblyNames, bool useSharedTypemapUniverse = false, string? assemblyName = null, int maxArrayRank = 0, IReadOnlyList? sharedFrameworkTypeMapNames = null) { var stream = new MemoryStream (); var generator = new RootTypeMapAssemblyGenerator (new Version (11, 0, 0, 0)); - generator.Generate (perAssemblyNames, useSharedTypemapUniverse, stream, assemblyName, maxArrayRank: maxArrayRank); + generator.Generate (perAssemblyNames, useSharedTypemapUniverse, stream, assemblyName, maxArrayRank: maxArrayRank, sharedFrameworkTypeMapNames: sharedFrameworkTypeMapNames); stream.Position = 0; return stream; } @@ -180,6 +180,58 @@ public void Generate_MergedMode_TargetAttributesUseSharedAnchor () }, targetAttributes); } + // Issue #10792: a pre-generated framework typemap (e.g. _Mono.Android.TypeMap, built at SDK + // build time) always uses Java.Lang.Object as its universe anchor. The root must reference it + // under that anchor (resolution scope "Mono.Android") regardless of the app's universe mode. + + [Fact] + public void Generate_MergedMode_SharedFrameworkTypeMap_ReferencedUnderJavaLangObject () + { + using var stream = GenerateRootAssembly ( + [ "_App.TypeMap" ], + useSharedTypemapUniverse: true, + sharedFrameworkTypeMapNames: [ "_Mono.Android.TypeMap" ]); + using var pe = new PEReader (stream); + var reader = pe.GetMetadataReader (); + + var targetAttributes = GetTypeMapAssemblyTargetAttributeTargets (reader); + + // Both the app typemap (shared mode) and the framework typemap anchor on Java.Lang.Object. + Assert.Contains (("_App.TypeMap", "Mono.Android"), targetAttributes); + Assert.Contains (("_Mono.Android.TypeMap", "Mono.Android"), targetAttributes); + } + + [Fact] + public void Generate_AggregateMode_SharedFrameworkTypeMap_AppUsesOwnAnchorFrameworkUsesJavaLangObject () + { + using var stream = GenerateRootAssembly ( + [ "_App.TypeMap" ], + useSharedTypemapUniverse: false, + sharedFrameworkTypeMapNames: [ "_Mono.Android.TypeMap" ]); + using var pe = new PEReader (stream); + var reader = pe.GetMetadataReader (); + + var targetAttributes = GetTypeMapAssemblyTargetAttributeTargets (reader); + + // App typemap keeps its own per-assembly __TypeMapAnchor universe; the pre-generated + // framework typemap is referenced under Java.Lang.Object (scope "Mono.Android"). + Assert.Contains (("_App.TypeMap", "_App.TypeMap"), targetAttributes); + Assert.Contains (("_Mono.Android.TypeMap", "Mono.Android"), targetAttributes); + } + + [Theory] + [InlineData (true)] + [InlineData (false)] + public void Generate_WithSharedFrameworkTypeMap_ProducesValidPE (bool useSharedTypemapUniverse) + { + using var stream = GenerateRootAssembly ( + [ "_App.TypeMap" ], + useSharedTypemapUniverse, + sharedFrameworkTypeMapNames: [ "_Mono.Android.TypeMap" ]); + using var pe = new PEReader (stream); + Assert.True (pe.HasMetadata); + } + [Theory] [InlineData (true)] [InlineData (false)] From 403823754e5e0a3c3fe8e65e1f159184bb9949f1 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Wed, 15 Jul 2026 23:08:47 +0200 Subject: [PATCH 03/11] [TrimmableTypeMap] Pre-generate Mono.Android typemap at SDK build time Milestone: generate _Mono.Android.TypeMap.dll (issue #10792) once during the SDK/pack build instead of rescanning Mono.Android (~8000 types) on every app build. - GenerateTrimmableTypeMap task: add ForceSharedTypemapUniverse. When true the typemap always uses the Java.Lang.Object universe anchor regardless of Debug, so the single pre-generated artifact is consumable by both app-build universe modes. - build-tools/create-packs/Microsoft.Android.Sdk.TrimmableTypeMap.targets: new _GeneratePreBuiltMonoAndroidTypeMap target scans the built Mono.Android.dll (framework assemblies supplied for reference only) with ForceSharedTypemapUniverse=true and GenerateRootAssembly=false, emitting _Mono.Android.TypeMap.dll + JCW Java sources; and _AddPreBuiltMonoAndroidTypeMapToPackage ships the DLL in the SDK pack under data/prebuilt-typemap. Imported by Microsoft.Android.Sdk.proj. Verified locally: the target produces _Mono.Android.TypeMap.dll (5.4 MB) with no __TypeMapAnchor (i.e. anchored on Java.Lang.Object), 320 JCW .java files, and no root _Microsoft.Android.TypeMaps assembly. JCW jar compilation + app-build consumption follow in later milestones. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 867395ec-6705-4ed6-8a68-e25b58d431fc --- ...osoft.Android.Sdk.TrimmableTypeMap.targets | 54 +++++++++++++++++++ .../create-packs/Microsoft.Android.Sdk.proj | 2 + .../Tasks/GenerateTrimmableTypeMap.cs | 7 ++- 3 files changed, 62 insertions(+), 1 deletion(-) create mode 100644 build-tools/create-packs/Microsoft.Android.Sdk.TrimmableTypeMap.targets diff --git a/build-tools/create-packs/Microsoft.Android.Sdk.TrimmableTypeMap.targets b/build-tools/create-packs/Microsoft.Android.Sdk.TrimmableTypeMap.targets new file mode 100644 index 00000000000..0889edf3941 --- /dev/null +++ b/build-tools/create-packs/Microsoft.Android.Sdk.TrimmableTypeMap.targets @@ -0,0 +1,54 @@ + + + + + + + <_PreBuiltTypeMapDir>$(IntermediateOutputPath)prebuilt-typemap\ + <_PreBuiltTypeMapOutputDir>$(_PreBuiltTypeMapDir)typemap\ + <_PreBuiltTypeMapJavaDir>$(_PreBuiltTypeMapDir)java\ + <_PreBuiltTypeMapAssembly>$(_PreBuiltTypeMapOutputDir)_Mono.Android.TypeMap.dll + + + + + <_PreBuiltTypeMapInput Include="$(_MonoAndroidNETDefaultOutDir)Mono.Android.dll" /> + + <_PreBuiltTypeMapFramework Include="$(_MonoAndroidNETDefaultOutDir)*.dll" + Exclude="$(_MonoAndroidNETDefaultOutDir)Mono.Android.dll" /> + + + + + + + + + + + + diff --git a/build-tools/create-packs/Microsoft.Android.Sdk.proj b/build-tools/create-packs/Microsoft.Android.Sdk.proj index de28a81047f..71b45305b7b 100644 --- a/build-tools/create-packs/Microsoft.Android.Sdk.proj +++ b/build-tools/create-packs/Microsoft.Android.Sdk.proj @@ -12,6 +12,8 @@ core workload SDK packs imported by WorkloadManifest.targets. + + $(DotNetStableTargetFramework) diff --git a/src/Xamarin.Android.Build.Tasks/Tasks/GenerateTrimmableTypeMap.cs b/src/Xamarin.Android.Build.Tasks/Tasks/GenerateTrimmableTypeMap.cs index 2100a972763..87194f911f2 100644 --- a/src/Xamarin.Android.Build.Tasks/Tasks/GenerateTrimmableTypeMap.cs +++ b/src/Xamarin.Android.Build.Tasks/Tasks/GenerateTrimmableTypeMap.cs @@ -120,6 +120,11 @@ public void LogJniAddNativeMethodRegistrationAttributeError (string managedTypeN // references the pre-generated per-assembly typemaps alongside the app's own. public bool GenerateRootAssembly { get; set; } = true; + // When true, forces the shared (Java.Lang.Object) typemap universe regardless of Debug. + // Set for SDK-build-time pre-generation of framework typemaps (issue #10792) so they always + // anchor on Java.Lang.Object and can be consumed by both app-build universe modes. + public bool ForceSharedTypemapUniverse { get; set; } + public bool CleanJavaSourceOutputDirectory { get; set; } [Output] @@ -217,7 +222,7 @@ public override bool RunTask () assemblies, systemRuntimeVersion, frameworkAssemblyNames, - useSharedTypemapUniverse: !Debug, + useSharedTypemapUniverse: ForceSharedTypemapUniverse || !Debug, manifestConfig: manifestConfig, manifestTemplate: manifestTemplate, packageNamingPolicy: PackageNamingPolicy, From 0204e158b5dac5a02241bab77d7d7442076620fd Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Wed, 15 Jul 2026 23:17:59 +0200 Subject: [PATCH 04/11] [TrimmableTypeMap] Emit pre-built Mono.Android typemap into the SDK pack layout ConfigureLocalWorkload (used by 'make all' and -t:InstallMaui) does not run the NuGet GetFilesToPackage flow, so the FilesToPackage hook alone did not place the pre-generated typemap into the local pack. Write _Mono.Android.TypeMap.dll directly into the pack layout at $(MicrosoftAndroidSdkPackDir)data\prebuilt-typemap\ and run the generation BeforeTargets=ConfigureLocalWorkload as well as _GenerateXASdkContent, so the artifact is present for both local workload testing and the NuGet pack. Verified: after ConfigureLocalWorkload, _Mono.Android.TypeMap.dll (5.4 MB) is present under bin/.../packs/Microsoft.Android.Sdk.Darwin//data/prebuilt-typemap/. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 867395ec-6705-4ed6-8a68-e25b58d431fc --- .../Microsoft.Android.Sdk.TrimmableTypeMap.targets | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/build-tools/create-packs/Microsoft.Android.Sdk.TrimmableTypeMap.targets b/build-tools/create-packs/Microsoft.Android.Sdk.TrimmableTypeMap.targets index 0889edf3941..579f6a1ba6c 100644 --- a/build-tools/create-packs/Microsoft.Android.Sdk.TrimmableTypeMap.targets +++ b/build-tools/create-packs/Microsoft.Android.Sdk.TrimmableTypeMap.targets @@ -14,14 +14,14 @@ - <_PreBuiltTypeMapDir>$(IntermediateOutputPath)prebuilt-typemap\ - <_PreBuiltTypeMapOutputDir>$(_PreBuiltTypeMapDir)typemap\ - <_PreBuiltTypeMapJavaDir>$(_PreBuiltTypeMapDir)java\ + <_PreBuiltTypeMapPackSubDir>data\prebuilt-typemap + <_PreBuiltTypeMapOutputDir>$(MicrosoftAndroidSdkPackDir)$(_PreBuiltTypeMapPackSubDir)\ + <_PreBuiltTypeMapJavaDir>$(_PreBuiltTypeMapOutputDir)java\ <_PreBuiltTypeMapAssembly>$(_PreBuiltTypeMapOutputDir)_Mono.Android.TypeMap.dll @@ -47,7 +47,7 @@ BeforeTargets="_GenerateXASdkContent" DependsOnTargets="_GeneratePreBuiltMonoAndroidTypeMap"> - + From 2ecb273cca790693400469dc22387263c40fe7aa Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Wed, 15 Jul 2026 23:25:18 +0200 Subject: [PATCH 05/11] [TrimmableTypeMap] Pre-compile Mono.Android JCWs into mono.android-typemaps.jar Compile the pre-generated Java Callable Wrappers (issue #10792) into a jar shipped in the SDK pack under data/prebuilt-typemap, so the app build can dex them directly instead of re-generating and re-compiling the ~320 Mono.Android JCWs. - _CompilePreBuiltMonoAndroidTypeMapJcwJar javac-compiles the generated JCW .java into mono.android-typemaps.jar. Classpath mirrors GenerateJavaCallableWrappers: android.jar + mono.android.jar + java_runtime.jar. - Ship the jar alongside _Mono.Android.TypeMap.dll (FilesToPackage + local pack layout). Verified: mono.android-typemaps.jar (403 entries) is produced under bin/.../packs/Microsoft.Android.Sdk.Darwin//data/prebuilt-typemap/. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 867395ec-6705-4ed6-8a68-e25b58d431fc --- ...osoft.Android.Sdk.TrimmableTypeMap.targets | 30 ++++++++++++++++++- 1 file changed, 29 insertions(+), 1 deletion(-) diff --git a/build-tools/create-packs/Microsoft.Android.Sdk.TrimmableTypeMap.targets b/build-tools/create-packs/Microsoft.Android.Sdk.TrimmableTypeMap.targets index 579f6a1ba6c..86ccc203856 100644 --- a/build-tools/create-packs/Microsoft.Android.Sdk.TrimmableTypeMap.targets +++ b/build-tools/create-packs/Microsoft.Android.Sdk.TrimmableTypeMap.targets @@ -43,11 +43,39 @@ GenerateRootAssembly="false" /> + + <_PreBuiltTypeMapJcwJar>$(_PreBuiltTypeMapOutputDir)mono.android-typemaps.jar + <_PreBuiltTypeMapJcwClassesDir>$(_PreBuiltTypeMapOutputDir)jcw-classes\ + <_PreBuiltTypeMapAndroidJar>$(AndroidSdkDirectory)\platforms\android-$(AndroidPlatformId)\android.jar + <_PreBuiltTypeMapMonoAndroidJar>$(_MonoAndroidNETDefaultOutDir)mono.android.jar + <_PreBuiltTypeMapRuntimeJar>$(MicrosoftAndroidSdkOutDir)java_runtime.jar + + + + + + <_PreBuiltTypeMapJavaFile Include="$(_PreBuiltTypeMapJavaDir)**\*.java" /> + + + + + + + + + DependsOnTargets="_GeneratePreBuiltMonoAndroidTypeMap;_CompilePreBuiltMonoAndroidTypeMapJcwJar"> + From cbd1c05d6c13825ae67dd6040efe923e5b686068 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Wed, 15 Jul 2026 23:31:14 +0200 Subject: [PATCH 06/11] [TrimmableTypeMap] Support reference-only assemblies + framework typemap refs in the task Adds the generator/scanner/task plumbing the app build needs to consume a pre-generated Mono.Android typemap (issue #10792): - AssemblyInput gains ScanForPeers. When false the assembly is indexed for base-type resolution but not scanned for peer emission; JavaPeerScanner skips ScanAssembly for it. - GenerateTrimmableTypeMap task gains PreGeneratedTypeMapAssemblies: those assemblies are marked ScanForPeers=false, and their _.TypeMap names are passed to the generator as sharedFrameworkTypeMapNames so the generated root references them under Java.Lang.Object. Adds a unit test asserting a reference-only assembly emits no peers/typemap. 620 tests pass. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 867395ec-6705-4ed6-8a68-e25b58d431fc --- .../AssemblyInput.cs | 7 +++++- .../Scanner/JavaPeerScanner.cs | 9 +++++++ .../Tasks/GenerateTrimmableTypeMap.cs | 25 ++++++++++++++++--- .../TrimmableTypeMapGeneratorTests.cs | 17 +++++++++++++ 4 files changed, 53 insertions(+), 5 deletions(-) diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/AssemblyInput.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/AssemblyInput.cs index d742d120d8c..c924746abf6 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/AssemblyInput.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/AssemblyInput.cs @@ -2,4 +2,9 @@ namespace Microsoft.Android.Sdk.TrimmableTypeMap; -public readonly record struct AssemblyInput (string Name, string Path, PEReader Reader); +/// +/// When true (default) the assembly is scanned and its Java peers are emitted into a typemap. +/// When false the assembly is only indexed for base-type resolution (e.g. Mono.Android on the app +/// build, whose typemap is pre-generated at SDK build time, issue #10792) and no peers are emitted. +/// +public readonly record struct AssemblyInput (string Name, string Path, PEReader Reader, bool ScanForPeers = true); diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs index f239f509f54..e88c234e2b4 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs @@ -173,9 +173,15 @@ public List Scan (IReadOnlyList<(string Name, PEReader Reader)> as public List Scan (IEnumerable assemblies) { + // Reference-only assemblies (ScanForPeers == false) are indexed so base-type resolution + // works, but are not scanned for peer emission (their typemap is pre-generated elsewhere). + var referenceOnlyAssemblyNames = new HashSet (StringComparer.Ordinal); foreach (var assembly in assemblies) { var index = AssemblyIndex.Create (assembly.Reader, assembly.Name, assembly.Path); assemblyCache [index.AssemblyName] = index; + if (!assembly.ScanForPeers) { + referenceOnlyAssemblyNames.Add (index.AssemblyName); + } } // Key by (managedTypeName, assemblyName) to avoid collisions when two assemblies @@ -183,6 +189,9 @@ public List Scan (IEnumerable assemblies) // Java.Interop and Mono.Android). var resultsByQualifiedName = new Dictionary<(string ManagedName, string AssemblyName), JavaPeerInfo> (); foreach (var index in assemblyCache.Values) { + if (referenceOnlyAssemblyNames.Contains (index.AssemblyName)) { + continue; + } ScanAssembly (index, resultsByQualifiedName); } ForceUnconditionalCrossReferences (resultsByQualifiedName, assemblyCache); diff --git a/src/Xamarin.Android.Build.Tasks/Tasks/GenerateTrimmableTypeMap.cs b/src/Xamarin.Android.Build.Tasks/Tasks/GenerateTrimmableTypeMap.cs index 87194f911f2..119721eb4e9 100644 --- a/src/Xamarin.Android.Build.Tasks/Tasks/GenerateTrimmableTypeMap.cs +++ b/src/Xamarin.Android.Build.Tasks/Tasks/GenerateTrimmableTypeMap.cs @@ -125,6 +125,12 @@ public void LogJniAddNativeMethodRegistrationAttributeError (string managedTypeN // anchor on Java.Lang.Object and can be consumed by both app-build universe modes. public bool ForceSharedTypemapUniverse { get; set; } + // Framework assemblies whose typemap is pre-generated at SDK build time (issue #10792), e.g. + // Mono.Android. On the app build these are indexed for base-type resolution but NOT scanned for + // peers; instead their pre-generated per-assembly typemap (_.TypeMap) is referenced by the + // generated root assembly under the Java.Lang.Object universe. + public ITaskItem [] PreGeneratedTypeMapAssemblies { get; set; } = []; + public bool CleanJavaSourceOutputDirectory { get; set; } [Output] @@ -142,11 +148,21 @@ public override bool RunTask () var frameworkAssemblyPaths = new HashSet ( ResolvedFrameworkAssemblies.Select (i => Path.GetFullPath (i.ItemSpec)), StringComparer.OrdinalIgnoreCase); + // Assemblies whose typemap is pre-generated: index them for resolution but don't scan for + // peers, and reference their pre-generated _.TypeMap from the root under Java.Lang.Object. + var preGeneratedAssemblyPaths = new HashSet ( + PreGeneratedTypeMapAssemblies.Select (i => Path.GetFullPath (i.ItemSpec)), + StringComparer.OrdinalIgnoreCase); + var sharedFrameworkTypeMapNames = PreGeneratedTypeMapAssemblies + .Select (i => $"_{Path.GetFileNameWithoutExtension (i.ItemSpec)}.TypeMap") + .Distinct (StringComparer.Ordinal) + .ToList (); var assemblyInputs = ResolvedAssemblies .GroupBy (i => Path.GetFullPath (i.ItemSpec), StringComparer.OrdinalIgnoreCase) .Select (g => ( Path: g.Key, - IsFrameworkAssembly: frameworkAssemblyPaths.Contains (g.Key) || g.Any (IsFrameworkAssemblyItem))) + IsFrameworkAssembly: frameworkAssemblyPaths.Contains (g.Key) || g.Any (IsFrameworkAssemblyItem), + ScanForPeers: !preGeneratedAssemblyPaths.Contains (g.Key))) .ToList (); var frameworkAssemblyNames = new HashSet (DefaultFrameworkAssemblyNames, StringComparer.OrdinalIgnoreCase); foreach (var assemblyName in FrameworkAssemblyNames) { @@ -181,12 +197,12 @@ public override bool RunTask () var assemblies = new List (); TrimmableTypeMapResult? result = null; try { - foreach (var (path, isFrameworkAssembly) in assemblyInputs) { + foreach (var (path, isFrameworkAssembly, scanForPeers) in assemblyInputs) { var peReader = new PEReader (File.OpenRead (path)); peReaders.Add (peReader); var mdReader = peReader.GetMetadataReader (); var assemblyName = mdReader.GetString (mdReader.GetAssemblyDefinition ().Name); - assemblies.Add (new AssemblyInput (assemblyName, path, peReader)); + assemblies.Add (new AssemblyInput (assemblyName, path, peReader, scanForPeers)); if (isFrameworkAssembly) { frameworkAssemblyNames.Add (assemblyName); } @@ -228,7 +244,8 @@ public override bool RunTask () packageNamingPolicy: PackageNamingPolicy, maxArrayRank: MaxArrayRank, generateTypeMapAssemblies: GenerateTypeMapAssemblies, - generateRootAssembly: GenerateRootAssembly); + generateRootAssembly: GenerateRootAssembly, + sharedFrameworkTypeMapNames: sharedFrameworkTypeMapNames); if (GenerateTypeMapAssemblies) { GeneratedAssemblies = WriteAssembliesToDisk (result.GeneratedAssemblies, assemblyInputs.Select (i => i.Path).ToList ()); diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TrimmableTypeMapGeneratorTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TrimmableTypeMapGeneratorTests.cs index 061e7507c9c..424b05cda09 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TrimmableTypeMapGeneratorTests.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TrimmableTypeMapGeneratorTests.cs @@ -108,6 +108,23 @@ public void Execute_WithGenerateRootAssemblyFalse_EmitsPerAssemblyTypeMapButNoRo Assert.NotEmpty (result.GeneratedJavaSources); } + [Fact] + public void Execute_ReferenceOnlyAssembly_EmitsNoPeers () + { + // A reference-only assembly (ScanForPeers: false) is indexed for base-type resolution but + // its peers are not emitted — used on the app build for Mono.Android, whose typemap is + // pre-generated at SDK build time (issue #10792). + using var peReader = CreateTestFixturePEReader (); + var result = CreateGenerator ().Execute ( + [new AssemblyInput ("TestFixtures", "", peReader, ScanForPeers: false)], + new Version (11, 0), + new HashSet ()); + + Assert.Empty (result.GeneratedAssemblies); + Assert.Empty (result.GeneratedJavaSources); + Assert.DoesNotContain (result.GeneratedAssemblies, a => a.Name == "_TestFixtures.TypeMap"); + } + [Fact] public void Execute_CollectsDeferredRegistrationTypes_ForAllApplicationAndInstrumentationSubtypes () { From 0ac7bc050d31d2404bd0c08c03641e086e7f8773 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Wed, 15 Jul 2026 23:43:05 +0200 Subject: [PATCH 07/11] [TrimmableTypeMap] App build: consume pre-generated Mono.Android typemap Wire the app build to use the SDK-shipped Mono.Android typemap (issue #10792) instead of rescanning Mono.Android's ~8000 types on every build: - Microsoft.Android.Sdk.TypeMap.Trimmable.targets: when the pre-generated typemap exists in the pack (data/prebuilt-typemap, gated by $(_AndroidUsePreGeneratedMonoAndroidTypeMap)), pass Mono.Android to GenerateTrimmableTypeMap as a PreGeneratedTypeMapAssembly (indexed for resolution, not rescanned); add the pre-built _Mono.Android.TypeMap.dll to the generated typemap set so it is linked/published and referenced by the root under Java.Lang.Object; and add mono.android-typemaps.jar to @(AndroidJavaLibrary) for dexing. - create-packs targets: emit generated JCW .java / .class to obj intermediate so only the _Mono.Android.TypeMap.dll and jar are packaged. Verified (Release NativeAOT, _AndroidTypeMapImplementation=trimmable): the app build no longer generates _Mono.Android.TypeMap (only _Java.Interop/_Microsoft.Android.Runtime.NativeAOT/_NativeAOT + root), and the root references the pre-built _Mono.Android.TypeMap. KNOWN GAP (next milestone): R8 shrinks the pre-built Mono.Android JCWs from classes.dex because their ACW entries are no longer in the app acw-map (Mono.Android excluded from scan), so no keep rules are generated. The pre-generated typemap's ACW keep rules must be shipped and fed to R8 before on-device runs will work. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 867395ec-6705-4ed6-8a68-e25b58d431fc --- ...osoft.Android.Sdk.TrimmableTypeMap.targets | 11 ++++--- ...soft.Android.Sdk.TypeMap.Trimmable.targets | 31 +++++++++++++++++++ 2 files changed, 38 insertions(+), 4 deletions(-) diff --git a/build-tools/create-packs/Microsoft.Android.Sdk.TrimmableTypeMap.targets b/build-tools/create-packs/Microsoft.Android.Sdk.TrimmableTypeMap.targets index 86ccc203856..1af472a32f0 100644 --- a/build-tools/create-packs/Microsoft.Android.Sdk.TrimmableTypeMap.targets +++ b/build-tools/create-packs/Microsoft.Android.Sdk.TrimmableTypeMap.targets @@ -16,7 +16,9 @@ <_PreBuiltTypeMapPackSubDir>data\prebuilt-typemap <_PreBuiltTypeMapOutputDir>$(MicrosoftAndroidSdkPackDir)$(_PreBuiltTypeMapPackSubDir)\ - <_PreBuiltTypeMapJavaDir>$(_PreBuiltTypeMapOutputDir)java\ + + <_PreBuiltTypeMapIntDir>$([MSBuild]::NormalizeDirectory('$(MSBuildProjectDirectory)', 'obj', 'prebuilt-typemap')) + <_PreBuiltTypeMapJavaDir>$(_PreBuiltTypeMapIntDir)java\ <_PreBuiltTypeMapAssembly>$(_PreBuiltTypeMapOutputDir)_Mono.Android.TypeMap.dll @@ -45,7 +47,8 @@ <_PreBuiltTypeMapJcwJar>$(_PreBuiltTypeMapOutputDir)mono.android-typemaps.jar - <_PreBuiltTypeMapJcwClassesDir>$(_PreBuiltTypeMapOutputDir)jcw-classes\ + <_PreBuiltTypeMapJcwClassesDir>$(_PreBuiltTypeMapIntDir)jcw-classes\ + <_PreBuiltTypeMapJcwSourceList>$(_PreBuiltTypeMapIntDir)jcw-sources.txt <_PreBuiltTypeMapAndroidJar>$(AndroidSdkDirectory)\platforms\android-$(AndroidPlatformId)\android.jar <_PreBuiltTypeMapMonoAndroidJar>$(_MonoAndroidNETDefaultOutDir)mono.android.jar <_PreBuiltTypeMapRuntimeJar>$(MicrosoftAndroidSdkOutDir)java_runtime.jar @@ -65,8 +68,8 @@ - - + + 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 b66b2e0208d..67d292cd84c 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 @@ -61,6 +61,18 @@ + + + <_PreGeneratedTypeMapDir>$([MSBuild]::NormalizeDirectory('$(MSBuildThisFileDirectory)..\data\prebuilt-typemap')) + <_PreGeneratedMonoAndroidTypeMapAssembly>$(_PreGeneratedTypeMapDir)_Mono.Android.TypeMap.dll + <_PreGeneratedMonoAndroidTypeMapJar>$(_PreGeneratedTypeMapDir)mono.android-typemaps.jar + <_AndroidUsePreGeneratedMonoAndroidTypeMap Condition=" '$(_AndroidUsePreGeneratedMonoAndroidTypeMap)' == '' and Exists('$(_PreGeneratedMonoAndroidTypeMapAssembly)') ">true + <_AndroidUsePreGeneratedMonoAndroidTypeMap Condition=" '$(_AndroidUsePreGeneratedMonoAndroidTypeMap)' == '' ">false + + @@ -127,12 +139,21 @@ <_MergedManifestDocuments Condition=" '$(AndroidManifestMerger)' == 'legacy' " Include="@(ExtractedManifestDocuments)" /> + + + <_PreGeneratedTypeMapAssemblyInput Condition=" '$(_AndroidUsePreGeneratedMonoAndroidTypeMap)' == 'true' " + Include="@(_TypeMapInputAssemblies)" KeepDuplicates="false" /> + <_PreGeneratedTypeMapAssemblyInput Remove="@(_PreGeneratedTypeMapAssemblyInput)" + Condition=" '%(Filename)' != 'Mono.Android' " /> + + + <_GeneratedTypeMapAssembliesFromList Include="$(_PreGeneratedMonoAndroidTypeMapAssembly)" /> + + + + + + + + + + + From 7b28317728ef5314fa2e63d73f74b8b91e5d3c81 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Thu, 16 Jul 2026 00:15:57 +0200 Subject: [PATCH 09/11] [TrimmableTypeMap] Pre-generate Mono.Android + Java.Interop together; fix array rank On-device (Release NativeAOT, trimmable) the app crashed at startup with BadImageFormatException in TypeMapLazyDictionary.CreateExternalTypeMap. Two causes, both fixed: 1. Duplicate java/lang/Object in the merged Java.Lang.Object universe: the pre-built Mono.Android typemap mapped it to Java.Lang.Object while the app-scanned _Java.Interop.TypeMap mapped it to Java.Interop.JavaObject. MergeCrossAssemblyAliases coordinates these only within a single scan. Fix: pre-generate Mono.Android AND Java.Interop together (aliases coordinated) and exclude both from the app scan; ship/reference all pre-built *.TypeMap.dll. 2. Array-rank mismatch: the pre-built typemap used MaxArrayRank=0 while the app root/runtime used the AOT default (3), so array entries landed in the main universe. Fix: generate the pre-built typemap with MaxArrayRank=3, and have the root reference the framework typemaps' __ArrayMapRank{N} universes too. Verified on a physical arm64 device (API 36): the Release/single-universe trimmable NativeAOT app launches to MainActivity with no BadImageFormatException / crash. 620 unit tests still pass. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 867395ec-6705-4ed6-8a68-e25b58d431fc --- ...crosoft.Android.Sdk.TrimmableTypeMap.targets | 15 +++++++++++---- .../Generator/RootTypeMapAssemblyGenerator.cs | 3 +++ ...rosoft.Android.Sdk.TypeMap.Trimmable.targets | 17 ++++++++++------- 3 files changed, 24 insertions(+), 11 deletions(-) diff --git a/build-tools/create-packs/Microsoft.Android.Sdk.TrimmableTypeMap.targets b/build-tools/create-packs/Microsoft.Android.Sdk.TrimmableTypeMap.targets index f969ef23d0c..ee26528e565 100644 --- a/build-tools/create-packs/Microsoft.Android.Sdk.TrimmableTypeMap.targets +++ b/build-tools/create-packs/Microsoft.Android.Sdk.TrimmableTypeMap.targets @@ -25,14 +25,19 @@ + <_PreBuiltTypeMapInput Include="$(_MonoAndroidNETDefaultOutDir)Mono.Android.dll" /> - <_PreBuiltTypeMapFramework Include="$(_MonoAndroidNETDefaultOutDir)*.dll" - Exclude="$(_MonoAndroidNETDefaultOutDir)Mono.Android.dll" /> + Exclude="@(_PreBuiltTypeMapInput)" /> @@ -79,7 +85,8 @@ BeforeTargets="_GenerateXASdkContent" DependsOnTargets="_GeneratePreBuiltMonoAndroidTypeMap;_CompilePreBuiltMonoAndroidTypeMapJcwJar"> - + <_PreBuiltTypeMapAllAssemblies Include="$(_PreBuiltTypeMapOutputDir)*.TypeMap.dll" /> + diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/RootTypeMapAssemblyGenerator.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/RootTypeMapAssemblyGenerator.cs index f53f6a985aa..374de770c6b 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/RootTypeMapAssemblyGenerator.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/RootTypeMapAssemblyGenerator.cs @@ -142,6 +142,9 @@ public void Generate (IReadOnlyList perAssemblyTypeMapNames, bool useSha pe.Metadata.GetOrAddString ("Java.Lang"), pe.Metadata.GetOrAddString ("Object")); EmitSharedUniverseAssemblyTargetAttributes (pe, javaLangObjectHandle, sharedFrameworkTypeMapNames!); + // The pre-generated framework typemaps also emit __ArrayMapRank{N} array universes; the + // app-build root must reference those so framework array types resolve at runtime. + EmitArrayAssemblyTargetAttributes (pe, sharedFrameworkTypeMapNames!, maxArrayRank); } // Emit [assembly: IgnoresAccessChecksTo("...")] so TypeMapLoader.Initialize() can access 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 e078760b2f6..c15556c7638 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 @@ -141,13 +141,15 @@ merger path; manifestmerger.jar merges these downstream in the _ManifestMerger target. --> <_MergedManifestDocuments Condition=" '$(AndroidManifestMerger)' == 'legacy' " Include="@(ExtractedManifestDocuments)" /> - + <_PreGeneratedTypeMapAssemblyInput Condition=" '$(_AndroidUsePreGeneratedMonoAndroidTypeMap)' == 'true' " Include="@(_TypeMapInputAssemblies)" KeepDuplicates="false" /> <_PreGeneratedTypeMapAssemblyInput Remove="@(_PreGeneratedTypeMapAssemblyInput)" - Condition=" '%(Filename)' != 'Mono.Android' " /> + Condition=" '%(Filename)' != 'Mono.Android' and '%(Filename)' != 'Java.Interop' " /> - + - <_GeneratedTypeMapAssembliesFromList Include="$(_PreGeneratedMonoAndroidTypeMapAssembly)" /> + <_GeneratedTypeMapAssembliesFromList Include="$(_PreGeneratedTypeMapDir)*.TypeMap.dll" /> From 5e349c732aa31aeadec20e30120220c3a33996ec Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Thu, 16 Jul 2026 00:22:28 +0200 Subject: [PATCH 10/11] [TrimmableTypeMap] Fall back to normal scan for Debug NativeAOT (aggregate + array maps) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The aggregate-universe + array-maps (maxArrayRank > 0) combination — i.e. Debug NativeAOT — is not yet wired for pre-generated framework typemaps and previously threw NotSupportedException, which would break those builds. Gracefully disable the pre-generation for that combination so the build falls back to scanning Mono.Android normally (correct, just without the speedup) instead of failing. Verified: Debug NativeAOT trimmable now builds (Mono.Android scanned in-app), while Release NativeAOT (single universe) and CoreCLR Debug (aggregate universe) continue to use the pre-generated typemap and are device-verified. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 867395ec-6705-4ed6-8a68-e25b58d431fc --- .../targets/Microsoft.Android.Sdk.TypeMap.Trimmable.targets | 4 ++++ 1 file changed, 4 insertions(+) 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 c15556c7638..e892027bf38 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 @@ -72,6 +72,10 @@ <_PreGeneratedMonoAndroidTypeMapAcwMap>$(_PreGeneratedTypeMapDir)mono.android-acw-map.txt <_AndroidUsePreGeneratedMonoAndroidTypeMap Condition=" '$(_AndroidUsePreGeneratedMonoAndroidTypeMap)' == '' and Exists('$(_PreGeneratedMonoAndroidTypeMapAssembly)') ">true <_AndroidUsePreGeneratedMonoAndroidTypeMap Condition=" '$(_AndroidUsePreGeneratedMonoAndroidTypeMap)' == '' ">false + + <_AndroidUsePreGeneratedMonoAndroidTypeMap Condition=" '$(_AndroidUsePreGeneratedMonoAndroidTypeMap)' == 'true' and '$(AndroidIncludeDebugSymbols)' == 'true' and '$(_AndroidTrimmableTypeMapMaxArrayRank)' != '0' ">false From f31eea5ccf0be0374c37eb87928088c734dc7f2c Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Thu, 16 Jul 2026 00:39:11 +0200 Subject: [PATCH 11/11] Address code review feedback on pre-generated Mono.Android typemap - Track both pre-built typemaps in _GeneratePreBuiltMonoAndroidTypeMap Outputs (_Mono.Android.TypeMap.dll + _Java.Interop.TypeMap.dll) so the incremental state is not broken if the Java.Interop typemap is deleted. - Replace the hardcoded MaxArrayRank="3" with a documented _PreBuiltTypeMapMaxArrayRank property explaining it must match the app build's effective rank (BadImageFormatException otherwise). - Reference the tracking issue (#12128) from the arrays-aggregate NotSupportedException and the Debug-NativeAOT fallback comment. - Stop mutating the app's acw-map.txt in place. Instead build a separate deterministic acw-map.prebuilt-merged.txt (app map + pre-built framework map, Overwrite=true) and feed that to the NativeAOT R8 keep-rule generator, so the app's acw-map.txt stays byte-stable and its own incremental checks keep working (no double-append). Verified: 620 TrimmableTypeMap unit tests pass; Release NativeAOT (trimmable, R8) sample keeps the pre-built JCWs (classes.dex ~255KB, 310 mono.android keep rules) and launches on device without crashing, while acw-map.txt is no longer mutated. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 867395ec-6705-4ed6-8a68-e25b58d431fc --- ...osoft.Android.Sdk.TrimmableTypeMap.targets | 14 ++++++-- .../Generator/RootTypeMapAssemblyGenerator.cs | 6 ++-- ...id.Sdk.TypeMap.Trimmable.NativeAOT.targets | 32 +++++++++++++++++-- ...soft.Android.Sdk.TypeMap.Trimmable.targets | 23 ++++++------- 4 files changed, 55 insertions(+), 20 deletions(-) diff --git a/build-tools/create-packs/Microsoft.Android.Sdk.TrimmableTypeMap.targets b/build-tools/create-packs/Microsoft.Android.Sdk.TrimmableTypeMap.targets index ee26528e565..2571729ab8b 100644 --- a/build-tools/create-packs/Microsoft.Android.Sdk.TrimmableTypeMap.targets +++ b/build-tools/create-packs/Microsoft.Android.Sdk.TrimmableTypeMap.targets @@ -20,13 +20,23 @@ <_PreBuiltTypeMapIntDir>$([MSBuild]::NormalizeDirectory('$(MSBuildProjectDirectory)', 'obj', 'prebuilt-typemap')) <_PreBuiltTypeMapJavaDir>$(_PreBuiltTypeMapIntDir)java\ <_PreBuiltTypeMapAssembly>$(_PreBuiltTypeMapOutputDir)_Mono.Android.TypeMap.dll + <_PreBuiltTypeMapJavaInteropAssembly>$(_PreBuiltTypeMapOutputDir)_Java.Interop.TypeMap.dll <_PreBuiltTypeMapAcwMap>$(_PreBuiltTypeMapOutputDir)mono.android-acw-map.txt + + <_PreBuiltTypeMapMaxArrayRank>3 + Outputs="$(_PreBuiltTypeMapAssembly);$(_PreBuiltTypeMapJavaInteropAssembly)"> + + + + + + + + + + + + + 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 e892027bf38..46169402a2e 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 @@ -74,8 +74,17 @@ <_AndroidUsePreGeneratedMonoAndroidTypeMap Condition=" '$(_AndroidUsePreGeneratedMonoAndroidTypeMap)' == '' ">false + build still succeeds (without the pre-generation speedup) instead of failing. Tracked by + https://github.com/dotnet/android/issues/12128. --> <_AndroidUsePreGeneratedMonoAndroidTypeMap Condition=" '$(_AndroidUsePreGeneratedMonoAndroidTypeMap)' == 'true' and '$(AndroidIncludeDebugSymbols)' == 'true' and '$(_AndroidTrimmableTypeMapMaxArrayRank)' != '0' ">false + + <_TrimmableTypeMapKeepRulesAcwMapFile>$(IntermediateOutputPath)acw-map.txt + <_TrimmableTypeMapKeepRulesAcwMapFile Condition=" '$(_AndroidUsePreGeneratedMonoAndroidTypeMap)' == 'true' ">$(IntermediateOutputPath)acw-map.prebuilt-merged.txt @@ -191,18 +200,6 @@ - - - - - -