From 1c944a3df3b8df3f4c777723b1bbf5b668f04f0f Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Fri, 24 Jul 2026 16:53:05 +0200 Subject: [PATCH 1/4] [TrimmableTypeMap] Improve incremental build performance Preserve unchanged post-trim Java outputs, prevent pre-trim generation from clobbering linked metadata, and make the legacy post-trimming pipeline incremental. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: a301fa68-0e6c-4280-b2f2-bc3d4c9ae3de --- .../README.md | 77 +++++++-------- ...crosoft.Android.Sdk.TypeMap.LlvmIr.targets | 18 +++- ...roid.Sdk.TypeMap.Trimmable.CoreCLR.targets | 61 +++++++++--- ...soft.Android.Sdk.TypeMap.Trimmable.targets | 26 +++-- .../Tasks/GenerateTrimmableTypeMap.cs | 27 +++--- .../IncrementalBuildTest.cs | 6 ++ .../Tasks/GenerateTrimmableTypeMapTests.cs | 44 ++++++++- .../TrimmableTypeMapBuildTests.cs | 94 ++++++++++++++++++- 8 files changed, 270 insertions(+), 83 deletions(-) diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/README.md b/src/Microsoft.Android.Sdk.TrimmableTypeMap/README.md index 941d583da37..760b37f33f7 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/README.md +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/README.md @@ -36,15 +36,14 @@ CoreCompile _ReadGeneratedTrimmableTypeMapAssemblies (reads typemap-assemblies.txt) _PrepareTrimmableNativeConfigAssemblies (feeds _GeneratePackageManagerJava) _PrepareTrimmableTypeMapAssemblies (feeds packaging / assembly store) -_CollectTrimmableTypeMapJavaFiles (globs the JCW *.java) -_GenerateJavaStubs (copies JCWs into android/src, manifest, acw-map) +_GenerateJavaStubs (prepares manifest and native config) └─► _CompileJava ─► _CompileToDalvik ─► packaging ``` `_GenerateJavaStubs` **overrides** the legacy target of the same name from -`BuildOrder.targets`; in the trimmable path the JCWs already exist, so this -target only copies them into `$(IntermediateOutputPath)android/src` and wires up -the manifest, `acw-map.txt`, and native config. +`BuildOrder.targets`; in the trimmable path the JCWs already exist and are +compiled in place from the generator output directory. This target wires up +the manifest and native config. For `CoreCLR` + `PublishTrimmed=true`, a second pass (`_GeneratePostTrimTrimmableTypeMapJavaSources`, in the CoreCLR targets) @@ -64,8 +63,8 @@ reliable timestamp sentinel. `_GenerateTrimmableTypeMap` declares: ```xml -Inputs="@(ReferencePath);@(PrivateSdkAssemblies);@(FrameworkAssemblies);$(IntermediateOutputPath)$(TargetFileName);$(_AndroidManifestAbs);$(_AndroidBuildPropertiesCache)" -Outputs="$(_TypeMapOutputDirectory)$(_TypeMapAssemblyName).dll;$(_TypeMapAssembliesListFile);$(_TrimmableTypeMapOutputStamp)" +Inputs="@(ReferencePath);@(PrivateSdkAssemblies);@(FrameworkAssemblies);...;$(_AndroidManifestAbs);$(_AndroidBuildPropertiesCache)" +Outputs="$(_TrimmableTypeMapOutputStamp)" ``` The generated TypeMap DLLs are written with `Files.CopyIfStreamChanged`, so an @@ -76,7 +75,7 @@ it on every build. To avoid this, the target unconditionally `Touch`es a dedicated stamp: ```xml - + ``` so the stamp is always newer than the inputs after a run, and the target is @@ -86,54 +85,56 @@ correctly **skipped** when none of the inputs changed. > inputs — including `@(PrivateSdkAssemblies)` and `@(FrameworkAssemblies)` — > otherwise a change in one of them would not trigger regeneration. -### 2. `_GenerateJavaStubs` keys off the stamp +### 2. `_GenerateJavaStubs` keys off both producer stamps ```xml -Inputs="$(_TrimmableTypeMapOutputStamp);@(_EnvironmentFiles)" +Inputs="$(_TrimmableTypeMapOutputStamp);$(_TrimmableJavaSourceStamp);@(_EnvironmentFiles)" Outputs="$(_AndroidStampDirectory)_GenerateJavaStubs.stamp" ``` -The stamp captures "the generator ran because its inputs changed" and is left -stable when the generator is skipped, so the JCW copy into `android/src` only -re-runs when something relevant actually changed. The copy uses -`SkipUnchangedFiles="true"` so unchanged JCWs do not churn downstream Java -compilation. For `CoreCLR` + `PublishTrimmed`, the JCWs are sourced from the -`linked-java` directory produced by `_GeneratePostTrimTrimmableTypeMapJavaSources`, -which is itself incremental; the stamp remains the sentinel so a no-op build -still skips `_GenerateJavaStubs`. +The pre-trim stamp covers the generated manifest and other shared outputs. For +`CoreCLR` + `PublishTrimmed`, `_TrimmableJavaSourceStamp` is the post-trim stamp +and covers the linked JCW set. Both remain stable when their producer is +skipped, so a no-op build still skips `_GenerateJavaStubs` while a manifest-only +change is not lost just because the linked assemblies stayed unchanged. + +The pre-trim CoreCLR pass deliberately does not write the final `acw-map.txt` +or `ApplicationRegistration.java` in trimmed builds. Those files are owned by +the post-trim pass; sharing their paths allowed a manifest-only pre-trim run to +overwrite the linked versions while the post-trim target remained up-to-date. ### 3. Stale generated Java sources are pruned (both passes) When a managed type is removed — or trimmed away on the `PublishTrimmed` path — -its JCW must not linger in `android/src`, where it would otherwise be compiled -and packaged. Both generator passes report the JCWs they no longer produce as -`DeletedJavaFiles` (with `RelativePath` metadata), and the owning target mirrors -each deletion into the `android/src` copy and, if anything was deleted, deletes -`$(_AndroidCompileJavaStampFile)` so `_CompileJava` re-runs and drops the stale -`.class` outputs: +its JCW must not linger in the generator directory, where it would otherwise +be compiled and packaged. Both generator passes report the JCWs they no longer +produce as `DeletedJavaFiles` (with `RelativePath` metadata). If anything was +deleted, the owning target deletes `$(_AndroidCompileJavaStampFile)` so +`_CompileJava` re-runs and drops the stale `.class` outputs: ```xml - - + ``` -The two passes compute the deleted set differently because of how each manages -its output directory: +Both passes update their output directories in place: - **Pre-trim** (`_GenerateTrimmableTypeMap`, writing `typemap/java`): the task scans the output directory and deletes any `*.java` the current pass did not produce. - **Post-trim** (`_GeneratePostTrimTrimmableTypeMapJavaSources`, writing - `typemap/linked-java` with `CleanJavaSourceOutputDirectory=true`): the - directory is wiped before regeneration, so the task snapshots the previous - `*.java` set *before* the wipe and reports `previous − regenerated`. This keeps - the deletion precise — only files the generator itself previously produced are - ever removed from `android/src`, never unrelated sources such as - `ApplicationRegistration.java`. - -The invariant is two-directional: **`android/src` contains exactly the JCWs the -active pass produces** — no missing files (copied via `_GenerateJavaStubs`) and -no stale files (pruned via `DeletedJavaFiles`). + `typemap/linked-java`): the task updates files by content and prunes stale + `*.java` files in place. Unchanged JCWs keep their timestamps, so a managed + method-body change does not force `javac` and D8 to rebuild identical Java + output. + +The invariant is two-directional: the active generator directory contains +exactly the JCWs that should be compiled — no missing files and no stale files. +The post-trim pass persists its expected Java paths in +`typemap/linked-java-files.txt`. Before using the post-trim stamp, an always-run +validation target compares that list with the files on disk and invalidates the +stamp if a source is missing or unexpected, or if the ACW map or application +registration source is missing. If generation logs an error, stale pruning is +suppressed so a partial result cannot delete the last known-good output set. ### 4. Dynamic `FileWrites` are re-emitted on no-op builds diff --git a/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.TypeMap.LlvmIr.targets b/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.TypeMap.LlvmIr.targets index 52ef1811dbd..9ec518a3571 100644 --- a/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.TypeMap.LlvmIr.targets +++ b/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.TypeMap.LlvmIr.targets @@ -282,17 +282,30 @@ Runs in the inner build after ILLink but before ReadyToRun/crossgen2 compilation, so R2R images are generated from the already-modified assemblies. --> - + <_PostTrimmingAssembly Remove="@(_PostTrimmingAssembly)" /> <_PostTrimmingAssembly Include="@(ResolvedFileToPublish)" Condition=" '%(Extension)' == '.dll' and '%(ResolvedFileToPublish.PostprocessAssembly)' == 'true' " /> + + + + + + + + @@ -304,6 +317,7 @@ <_PropertyCacheItems Include="TypeMapKind=$(_TypeMapKind)" /> + <_PropertyCacheItems Include="Deterministic=$(Deterministic)" /> diff --git a/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.TypeMap.Trimmable.CoreCLR.targets b/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.TypeMap.Trimmable.CoreCLR.targets index 37bb53c617e..a111690181a 100644 --- a/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.TypeMap.Trimmable.CoreCLR.targets +++ b/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.TypeMap.Trimmable.CoreCLR.targets @@ -65,12 +65,45 @@ + + + + <_ExpectedPostTrimJavaFile Remove="@(_ExpectedPostTrimJavaFile)" /> + <_ActualPostTrimJavaFile Remove="@(_ActualPostTrimJavaFile)" /> + <_MissingPostTrimJavaFile Remove="@(_MissingPostTrimJavaFile)" /> + <_UnexpectedPostTrimJavaFile Remove="@(_UnexpectedPostTrimJavaFile)" /> + + + + + + <_ActualPostTrimJavaFile Include="$(_PostTrimTypeMapJavaOutputDirectory)/**/*.java" /> + <_MissingPostTrimJavaFile Include="@(_ExpectedPostTrimJavaFile)" Condition="!Exists('%(_ExpectedPostTrimJavaFile.Identity)')" /> + <_UnexpectedPostTrimJavaFile Include="@(_ActualPostTrimJavaFile)" Exclude="@(_ExpectedPostTrimJavaFile)" /> + + + + <_ExpectedPostTrimJavaFile Remove="@(_ExpectedPostTrimJavaFile)" /> + <_ActualPostTrimJavaFile Remove="@(_ActualPostTrimJavaFile)" /> + <_MissingPostTrimJavaFile Remove="@(_MissingPostTrimJavaFile)" /> + <_UnexpectedPostTrimJavaFile Remove="@(_UnexpectedPostTrimJavaFile)" /> + + + - - - <_PostTrimDeletedCopiedJavaFiles Remove="@(_PostTrimDeletedCopiedJavaFiles)" /> - <_PostTrimDeletedCopiedJavaFiles Include="@(_PostTrimDeletedJavaFiles->'$(IntermediateOutputPath)android/src/%(RelativePath)')" /> - - - + + + + - + <_PostTrimTrimmableTypeMapInputAssemblies Remove="@(_PostTrimTrimmableTypeMapInputAssemblies)" /> <_PostTrimGeneratedJavaFiles Remove="@(_PostTrimGeneratedJavaFiles)" /> <_PostTrimDeletedJavaFiles Remove="@(_PostTrimDeletedJavaFiles)" /> - <_PostTrimDeletedCopiedJavaFiles Remove="@(_PostTrimDeletedCopiedJavaFiles)" /> + <_PreTrimTypeMapAcwMapOutputFile Condition=" '$(_AndroidRuntime)' != 'CoreCLR' or '$(PublishTrimmed)' != 'true' ">$(IntermediateOutputPath)acw-map.txt + <_PreTrimTypeMapApplicationRegistrationOutputFile Condition=" '$(_AndroidRuntime)' != 'CoreCLR' or '$(PublishTrimmed)' != 'true' ">$(IntermediateOutputPath)android/src/net/dot/android/ApplicationRegistration.java + DependsOnTargets="_ReadGeneratedTrimmableTypeMapAssemblies"> - + %(Filename)%(Extension) true 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 f25461c202f..0ea23453ad4 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 @@ -24,6 +24,7 @@ <_TypeMapOutputDirectory>$(_TypeMapBaseOutputDir)typemap/ <_TypeMapJavaOutputDirectory>$(_TypeMapBaseOutputDir)typemap/java <_TypeMapAssembliesListFile>$(_TypeMapOutputDirectory)typemap-assemblies.txt + <_TypeMapJavaFilesList>$(_TypeMapOutputDirectory)java-files.txt <_PostTrimTypeMapJavaBaseOutputDir Condition=" '$(_OuterIntermediateOutputPath)' != '' ">$(IntermediateOutputPath) <_PostTrimTypeMapJavaBaseOutputDir Condition=" '$(_PostTrimTypeMapJavaBaseOutputDir)' == '' ">$(_TypeMapBaseOutputDir) <_PostTrimTypeMapJavaOutputDirectory>$(_PostTrimTypeMapJavaBaseOutputDir)typemap/linked-java @@ -44,6 +45,9 @@ Outputs without making the target run on every build. The stamp is always touched, so the target is correctly skipped when none of its Inputs changed. --> <_TrimmableTypeMapOutputStamp>$(_TypeMapOutputDirectory)_GenerateTrimmableTypeMap.stamp + <_TrimmableRemoveRegisterFlag>$(_AndroidStampDirectory)_RemoveRegisterAttribute.stamp + <_TrimmableRemoveRegisterTarget Condition=" '$(_AndroidRuntime)' == 'CoreCLR' ">_RemoveRegisterAttributeCoreClr + <_TrimmableRemoveRegisterTarget Condition=" '$(_AndroidRuntime)' == 'NativeAOT' ">_RemoveRegisterAttributeNativeAot @@ -189,6 +199,7 @@ + @@ -236,13 +247,25 @@ + + + + + + - <_TypeMapJavaFilesForFileWrites Include="$(_TypeMapJavaOutputDirectory)/**/*.java" /> - - <_TypeMapPostTrimJavaFilesForFileWrites Include="$(_PostTrimTypeMapJavaOutputDirectory)/**/*.java" /> + + <_TypeMapJavaFilesForFileWrites + Include="$(_TypeMapJavaOutputDirectory)/**/*.java" + Condition="!Exists('$(_TypeMapJavaFilesList)')" /> + <_TypeMapPostTrimJavaFilesForFileWrites + Include="$(_PostTrimTypeMapJavaOutputDirectory)/**/*.java" + Condition="!Exists('$(_PostTrimTypeMapJavaFilesList)')" /> @@ -250,6 +273,7 @@ + @@ -335,9 +359,53 @@ CoreCLR (and any other trimmable runtime) keeps the base behavior because it still packages the managed assemblies and therefore needs the shrunk copies in place. --> - + + <_TrimmableResolvedAssemblyConfigInput Remove="@(_TrimmableResolvedAssemblyConfigInput)" /> + <_TrimmableResolvedAssemblyConfigInput Include="@(_ResolvedAssemblies->'%(Identity).config')" /> + <_TrimmableResolvedAssemblyConfigInput Remove="@(_TrimmableResolvedAssemblyConfigInput)" Condition="!Exists('%(_TrimmableResolvedAssemblyConfigInput.Identity)')" /> + <_MissingTrimmableShrunkAssembly Remove="@(_MissingTrimmableShrunkAssembly)" /> + <_MissingTrimmableShrunkAssembly + Include="@(_ShrunkAssemblies)" + Condition=" '$(_AndroidRuntime)' == 'CoreCLR' and !Exists('%(_ShrunkAssemblies.Identity)') " /> + + + + + <_TrimmableRemoveRegisterAlwaysRunInput + Condition=" '@(_TrimmableResolvedAssemblyConfigInput->Count())' != '0' ">$(IntermediateOutputPath)__always_copy_trimmable_assembly_configs__ + + + + + + + + + + + + + + + + + <_ShrunkAssemblies Remove="@(_ShrunkAssemblies)" /> <_ShrunkAssemblies Include="@(_ResolvedAssemblies->'$(MonoAndroidIntermediateAssemblyDir)shrunk\%(DestinationSubPath)')" /> diff --git a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/TrimmableTypeMapBuildTests.cs b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/TrimmableTypeMapBuildTests.cs index dc9667345df..6fe86c60675 100644 --- a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/TrimmableTypeMapBuildTests.cs +++ b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/TrimmableTypeMapBuildTests.cs @@ -61,11 +61,79 @@ public void Build_WithTrimmableTypeMap_IncrementalBuild ([Values] bool isRelease Assert.IsTrue ( builder.Output.IsTargetSkipped ("_GenerateJavaStubs"), "_GenerateJavaStubs should be skipped on incremental build."); + if (isRelease && runtime == AndroidRuntime.CoreCLR) { + builder.Output.AssertTargetIsSkipped ("_RemoveRegisterAttributeCoreClr"); + } + if (isRelease && runtime == AndroidRuntime.NativeAOT) { + builder.Output.AssertTargetIsNotSkipped ("_RemoveRegisterAttributeNativeAot"); + } foreach (var typemapDll in typemapDlls) { FileAssert.Exists (typemapDll, $"No-op builds should preserve generated typemap assembly {typemapDll} when _GenerateTrimmableTypeMap is skipped."); } } + [Test] + public void Build_WithTrimmableTypeMap_MissingJavaListPreservesGeneratedJava () + { + if (IgnoreUnsupportedConfiguration (AndroidRuntime.CoreCLR, release: false)) { + return; + } + + var proj = new XamarinAndroidApplicationProject (); + proj.SetRuntime (AndroidRuntime.CoreCLR); + proj.SetProperty ("_AndroidTypeMapImplementation", "trimmable"); + + using var builder = CreateApkBuilder (); + Assert.IsTrue (builder.Build (proj), "First build should have succeeded."); + + var typemapDirectory = builder.Output.GetIntermediaryPath ("typemap"); + var javaDirectory = Path.Combine (typemapDirectory, "java"); + var javaFiles = Directory.GetFiles (javaDirectory, "*.java", SearchOption.AllDirectories); + var javaFilesList = Path.Combine (typemapDirectory, "java-files.txt"); + Assert.IsNotEmpty (javaFiles, "First build should have generated pre-trim Java sources."); + FileAssert.Exists (javaFilesList, "First build should have persisted the pre-trim Java file list."); + File.Delete (javaFilesList); + + Assert.IsTrue (builder.Build (proj, doNotCleanupOnUpdate: true, saveProject: false), "No-op build should have succeeded."); + builder.Output.AssertTargetIsSkipped ("_GenerateTrimmableTypeMap"); + foreach (var javaFile in javaFiles) { + FileAssert.Exists (javaFile, $"IncrementalClean should preserve {javaFile} when upgrading an obj directory without java-files.txt."); + } + } + + [Test] + public void Build_WithTrimmableTypeMap_PublishTrimmed_MissingLinkedJavaListRegenerates () + { + if (IgnoreUnsupportedConfiguration (AndroidRuntime.CoreCLR, release: true)) { + return; + } + + var proj = new XamarinAndroidApplicationProject { + IsRelease = true, + }; + proj.SetRuntime (AndroidRuntime.CoreCLR); + proj.SetProperty ("_AndroidTypeMapImplementation", "trimmable"); + + using var builder = CreateApkBuilder (); + Assert.IsTrue (builder.Build (proj), "First build should have succeeded."); + + var typemapDirectory = builder.Output.GetIntermediaryPath ("typemap"); + var linkedJavaDirectory = Path.Combine (typemapDirectory, "linked-java"); + var linkedJavaFiles = Directory.GetFiles (linkedJavaDirectory, "*.java", SearchOption.AllDirectories); + var linkedJavaFilesList = Path.Combine (typemapDirectory, "linked-java-files.txt"); + Assert.IsNotEmpty (linkedJavaFiles, "First build should have generated post-trim Java sources."); + FileAssert.Exists (linkedJavaFilesList, "First build should have persisted the post-trim Java file list."); + File.Delete (linkedJavaFilesList); + + Assert.IsTrue (builder.Build (proj, doNotCleanupOnUpdate: true, saveProject: false), "Migration rebuild should have succeeded."); + builder.Output.AssertTargetIsNotSkipped ("_GeneratePostTrimTrimmableTypeMapJavaSources"); + builder.Output.AssertTargetIsSkipped ("_CompileJava"); + FileAssert.Exists (linkedJavaFilesList, "Post-trim generation should recreate linked-java-files.txt."); + foreach (var javaFile in linkedJavaFiles) { + FileAssert.Exists (javaFile, $"IncrementalClean should preserve {javaFile} until post-trim generation recreates its list."); + } + } + [Test] public void Build_WithTrimmableTypeMap_KeepsNativeAotRuntimeHostAcws () { @@ -346,6 +414,7 @@ public void Build_WithTrimmableTypeMap_PublishTrimmed_IncrementalChangesAvoidUnn Assert.IsTrue (builder.Build (proj, doNotCleanupOnUpdate: true, saveProject: false), "Managed-only rebuild should have succeeded."); builder.Output.AssertTargetIsNotSkipped ("_GeneratePostTrimTrimmableTypeMapJavaSources"); + builder.Output.AssertTargetIsNotSkipped ("_RemoveRegisterAttributeCoreClr"); builder.Output.AssertTargetIsSkipped ("_CompileJava"); builder.Output.AssertTargetIsSkipped ("_CompileToDalvik"); @@ -377,6 +446,7 @@ public void Build_WithTrimmableTypeMap_PublishTrimmed_IncrementalChangesAvoidUnn builder.Output.AssertTargetIsNotSkipped ("_GenerateTrimmableTypeMap"); builder.Output.AssertTargetIsSkipped ("_GeneratePostTrimTrimmableTypeMapJavaSources"); builder.Output.AssertTargetIsNotSkipped ("_GenerateJavaStubs"); + builder.Output.AssertTargetIsSkipped ("_RemoveRegisterAttributeCoreClr"); builder.Output.AssertTargetIsSkipped ("_CompileJava"); builder.Output.AssertTargetIsSkipped ("_CompileToDalvik"); Assert.IsTrue (postTrimAcwMap.SequenceEqual (ComputeFileHash (acwMap)), "The pre-trim pass should not overwrite the linked acw-map.txt."); From 2f8b328a5971578ba237e1f5b134589d75de65a9 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Fri, 24 Jul 2026 22:36:13 +0200 Subject: [PATCH 3/4] [TrimmableTypeMap] Normalize persisted Java paths Persist and compare fully qualified Java paths and normalize the post-trim output base directory for reliable Windows incrementality. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: a301fa68-0e6c-4280-b2f2-bc3d4c9ae3de --- .../Microsoft.Android.Sdk.TypeMap.Trimmable.CoreCLR.targets | 4 ++-- .../targets/Microsoft.Android.Sdk.TypeMap.Trimmable.targets | 3 ++- .../TrimmableTypeMapBuildTests.cs | 6 ++++++ 3 files changed, 10 insertions(+), 3 deletions(-) diff --git a/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.TypeMap.Trimmable.CoreCLR.targets b/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.TypeMap.Trimmable.CoreCLR.targets index 52cfaca0d5b..5054346d8b5 100644 --- a/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.TypeMap.Trimmable.CoreCLR.targets +++ b/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.TypeMap.Trimmable.CoreCLR.targets @@ -85,7 +85,7 @@ <_ActualPostTrimJavaFile Include="$(_PostTrimTypeMapJavaOutputDirectory)/**/*.java" /> <_MissingPostTrimJavaFile Include="@(_ExpectedPostTrimJavaFile)" Condition="!Exists('%(_ExpectedPostTrimJavaFile.Identity)')" /> - <_UnexpectedPostTrimJavaFile Include="@(_ActualPostTrimJavaFile)" Exclude="@(_ExpectedPostTrimJavaFile)" /> + <_UnexpectedPostTrimJavaFile Include="@(_ActualPostTrimJavaFile->'%(FullPath)')" Exclude="@(_ExpectedPostTrimJavaFile)" /> 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 0ea23453ad4..a6c550148a2 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 @@ -27,6 +27,7 @@ <_TypeMapJavaFilesList>$(_TypeMapOutputDirectory)java-files.txt <_PostTrimTypeMapJavaBaseOutputDir Condition=" '$(_OuterIntermediateOutputPath)' != '' ">$(IntermediateOutputPath) <_PostTrimTypeMapJavaBaseOutputDir Condition=" '$(_PostTrimTypeMapJavaBaseOutputDir)' == '' ">$(_TypeMapBaseOutputDir) + <_PostTrimTypeMapJavaBaseOutputDir>$(_PostTrimTypeMapJavaBaseOutputDir.Replace('\','/')) <_PostTrimTypeMapJavaOutputDirectory>$(_PostTrimTypeMapJavaBaseOutputDir)typemap/linked-java <_PostTrimTypeMapJavaFilesList>$(_PostTrimTypeMapJavaBaseOutputDir)typemap/linked-java-files.txt <_PostTrimTypeMapFirstRuntimeIdentifier Condition=" '$(RuntimeIdentifiers)' != '' ">$([System.String]::Copy('$(RuntimeIdentifiers)').Split(';')[0]) @@ -179,7 +180,7 @@ diff --git a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/TrimmableTypeMapBuildTests.cs b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/TrimmableTypeMapBuildTests.cs index 6fe86c60675..d47da49d497 100644 --- a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/TrimmableTypeMapBuildTests.cs +++ b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/TrimmableTypeMapBuildTests.cs @@ -92,6 +92,9 @@ public void Build_WithTrimmableTypeMap_MissingJavaListPreservesGeneratedJava () var javaFilesList = Path.Combine (typemapDirectory, "java-files.txt"); Assert.IsNotEmpty (javaFiles, "First build should have generated pre-trim Java sources."); FileAssert.Exists (javaFilesList, "First build should have persisted the pre-trim Java file list."); + foreach (var path in File.ReadAllLines (javaFilesList)) { + Assert.IsTrue (Path.IsPathFullyQualified (path), $"The persisted Java path should be fully qualified: {path}"); + } File.Delete (javaFilesList); Assert.IsTrue (builder.Build (proj, doNotCleanupOnUpdate: true, saveProject: false), "No-op build should have succeeded."); @@ -123,6 +126,9 @@ public void Build_WithTrimmableTypeMap_PublishTrimmed_MissingLinkedJavaListRegen var linkedJavaFilesList = Path.Combine (typemapDirectory, "linked-java-files.txt"); Assert.IsNotEmpty (linkedJavaFiles, "First build should have generated post-trim Java sources."); FileAssert.Exists (linkedJavaFilesList, "First build should have persisted the post-trim Java file list."); + foreach (var path in File.ReadAllLines (linkedJavaFilesList)) { + Assert.IsTrue (Path.IsPathFullyQualified (path), $"The persisted linked Java path should be fully qualified: {path}"); + } File.Delete (linkedJavaFilesList); Assert.IsTrue (builder.Build (proj, doNotCleanupOnUpdate: true, saveProject: false), "Migration rebuild should have succeeded."); From 74d654de3ad3ca3ecc140fcfad9125d824a9fddd Mon Sep 17 00:00:00 2001 From: Jonathan Peppers Date: Mon, 27 Jul 2026 16:18:39 -0500 Subject: [PATCH 4/4] [TrimmableTypeMap] Don't copy *.dll.config next to shrunk assemblies `_RemoveRegisterAttributeCoreClr` had to opt projects containing `*.dll.config` files out of incremental builds, because the source and destination config files can't be modeled as a reliable one-to-one transform (the sources are optional, the destinations are not). That escape hatch protected a copy that has no observable effect. Assembly configuration files are not supported by .NET for Android at all -- see `Documentation/guides/OneDotNet.md` -- and while the assembly store still carries a `config_data` blob, that field is never assigned from the store data nor handed to the runtime. Its only use in `assembly-store.cc` / `embedded-assemblies.cc` is a `log_debug` message that always prints `nullptr`. There is no `mono_register_config_for_assembly` call anywhere in the tree. Drop the config copy from the trimmable path so `_RemoveRegisterAttributeCoreClr` is a clean one-to-one incremental transform for every project, and remove the always-run input that existed only to disable it. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 2eda38c8-0207-46a4-8e47-1ba2535800ab --- .../README.md | 11 ++++++--- ...soft.Android.Sdk.TypeMap.Trimmable.targets | 23 ++++++------------- 2 files changed, 15 insertions(+), 19 deletions(-) diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/README.md b/src/Microsoft.Android.Sdk.TrimmableTypeMap/README.md index 9417fa83cd1..89905d108c9 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/README.md +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/README.md @@ -155,9 +155,14 @@ CoreCLR packages managed assemblies, so `_RemoveRegisterAttributeCoreClr` copies the resolved assembly set into the shrunk/package locations. The target uses the resolved assemblies, assembly-set hash, build properties, and project imports as inputs and a stamp as its output. Missing destination assemblies -invalidate the stamp. Projects with `*.dll.config` files retain the previous -always-run behavior until those files can be modeled as a reliable one-to-one -transform. +invalidate the stamp. + +`*.dll.config` files are deliberately *not* copied alongside the shrunk +assemblies. .NET for Android does not support assembly configuration files (see +[OneDotNet.md](../../Documentation/guides/OneDotNet.md)): the `config_data` +field carried by the assembly store is never handed to the runtime — it only +appears in a `log_debug` message — so copying the files added an unavoidable +always-run step for no observable behavior. NativeAOT keeps a separate always-run `_RemoveRegisterAttributeNativeAot` target. Its project-local destination remap must execute on every build so a 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 a6c550148a2..834f4295e57 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 @@ -359,14 +359,17 @@ CoreCLR (and any other trimmable runtime) keeps the base behavior because it still packages the managed assemblies and therefore needs the shrunk copies in place. + + Unlike the base target, `*.dll.config` files are not copied next to the shrunk + assemblies. Assembly configuration files are not supported by .NET for Android + (see Documentation/guides/OneDotNet.md); the assembly store's `config_data` is + never handed to the runtime, so the copy only forced this target to run on every + build. --> - <_TrimmableResolvedAssemblyConfigInput Remove="@(_TrimmableResolvedAssemblyConfigInput)" /> - <_TrimmableResolvedAssemblyConfigInput Include="@(_ResolvedAssemblies->'%(Identity).config')" /> - <_TrimmableResolvedAssemblyConfigInput Remove="@(_TrimmableResolvedAssemblyConfigInput)" Condition="!Exists('%(_TrimmableResolvedAssemblyConfigInput.Identity)')" /> <_MissingTrimmableShrunkAssembly Remove="@(_MissingTrimmableShrunkAssembly)" /> <_MissingTrimmableShrunkAssembly Include="@(_ShrunkAssemblies)" @@ -375,12 +378,6 @@ - - - <_TrimmableRemoveRegisterAlwaysRunInput - Condition=" '@(_TrimmableResolvedAssemblyConfigInput->Count())' != '0' ">$(IntermediateOutputPath)__always_copy_trimmable_assembly_configs__ - - @@ -418,9 +412,6 @@ -