diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/README.md b/src/Microsoft.Android.Sdk.TrimmableTypeMap/README.md
index 941d583da37..89905d108c9 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,66 +85,91 @@ 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
The set of generated assemblies and JCWs is data-dependent, so a build that
*skips* `_GenerateTrimmableTypeMap` never executes the `ItemGroup` that registers
those files in `@(FileWrites)`. `_RecordTrimmableTypeMapFileWrites` re-reads the
-generated outputs from `typemap-assemblies.txt` (and globs the JCWs) and
-re-emits them — plus the stamp — into `@(FileWrites)` *before* MSBuild's
-`IncrementalClean`, so the outputs are not seen as orphaned and deleted between
-incremental builds.
-
-### 5. The generator does not run in design-time builds, and runs once
+generated outputs from `typemap-assemblies.txt`, `java-files.txt`, and
+`linked-java-files.txt`, then re-emits them — plus the stamps — into
+`@(FileWrites)` *before* MSBuild's `IncrementalClean`. This avoids recursively
+globbing both Java trees on every no-op build. When upgrading an existing
+`obj` directory that predates either Java list, the record target temporarily
+falls back to the corresponding directory glob so the old outputs survive
+`IncrementalClean` long enough for generation to backfill the list.
+
+### 5. CoreCLR skips unchanged shrunk-assembly copies
+
+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.
+
+`*.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
+skipped target cannot point `_ShrunkAssemblies` back into the shared runtime
+pack and reintroduce cross-build races.
+
+### 6. The generator does not run in design-time builds, and runs once
`_GenerateTrimmableTypeMap` is gated on `'$(DesignTimeBuild)' != 'true'`: in a
design-time build, project references may resolve to target paths that are not
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..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
@@ -14,9 +14,9 @@
+ DependsOnTargets="_ReadGeneratedTrimmableTypeMapAssemblies">
-
+
%(Filename)%(Extension)
true
@@ -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->'%(FullPath)')" 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
<_TrimmableTypeMapOutputStamp>$(_TypeMapOutputDirectory)_GenerateTrimmableTypeMap.stamp
+ <_TrimmableRemoveRegisterFlag>$(_AndroidStampDirectory)_RemoveRegisterAttribute.stamp
+ <_TrimmableRemoveRegisterTarget Condition=" '$(_AndroidRuntime)' == 'CoreCLR' ">_RemoveRegisterAttributeCoreClr
+ <_TrimmableRemoveRegisterTarget Condition=" '$(_AndroidRuntime)' == 'NativeAOT' ">_RemoveRegisterAttributeNativeAot
@@ -183,10 +200,11 @@
+
-
-
+
+
@@ -230,19 +248,33 @@
+
+
+
+
+
+
- <_TypeMapJavaFilesForFileWrites Include="$(_TypeMapJavaOutputDirectory)/**/*.java" />
-
- <_TypeMapPostTrimJavaFilesForFileWrites Include="$(_PostTrimTypeMapJavaOutputDirectory)/**/*.java" />
+
+ <_TypeMapJavaFilesForFileWrites
+ Include="$(_TypeMapJavaOutputDirectory)/**/*.java"
+ Condition="!Exists('$(_TypeMapJavaFilesList)')" />
+ <_TypeMapPostTrimJavaFilesForFileWrites
+ Include="$(_PostTrimTypeMapJavaOutputDirectory)/**/*.java"
+ Condition="!Exists('$(_PostTrimTypeMapJavaFilesList)')" />
+
+
@@ -327,10 +359,48 @@
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.
-->
-
+
+ <_MissingTrimmableShrunkAssembly Remove="@(_MissingTrimmableShrunkAssembly)" />
+ <_MissingTrimmableShrunkAssembly
+ Include="@(_ShrunkAssemblies)"
+ Condition=" '$(_AndroidRuntime)' == 'CoreCLR' and !Exists('%(_ShrunkAssemblies.Identity)') " />
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
<_ShrunkAssemblies Remove="@(_ShrunkAssemblies)" />
<_ShrunkAssemblies Include="@(_ResolvedAssemblies->'$(MonoAndroidIntermediateAssemblyDir)shrunk\%(DestinationSubPath)')" />
@@ -342,28 +412,24 @@
-