Skip to content

Commit 174e481

Browse files
[TrimmableTypeMap] Improve trimmable typemap build incrementality (#11764)
## Summary Makes the **trimmable** typemap build pipeline (`_AndroidTypeMapImplementation=trimmable`, used by CoreCLR and NativeAOT) fully incremental, skipped in design‑time builds, and self‑consistent on every rebuild. Extracted as a focused, well‑tested slice from the larger #11617, and implemented on top of `main`'s current generator/task API rather than the runtime rewrite in that PR. The bulk of this description is a deep dive into **how incrementality is achieved**, because the pipeline has several non‑obvious moving parts (two generator passes, content‑based writes, dynamic outputs vs. `IncrementalClean`, and a post‑link Java regeneration step). --- ## Background: what the trimmable typemap generator produces The legacy typemap implementations (`llvm-ir`, `managed`) embed the managed ↔ Java type mapping into native binaries. The **trimmable** implementation instead generates, from a scan of the app's assemblies: - a set of small managed **TypeMap assemblies** — one `_<Assembly>.TypeMap.dll` per input assembly plus a `_Microsoft.Android.TypeMaps.dll` root — that are trimmer‑friendly (unused entries are removed by the IL linker), and - the **Java Callable Wrapper** (`*.java`, "JCW") sources that Java needs to call back into managed code, plus `acw-map.txt` and a merged `AndroidManifest.xml`. This is driven by the `GenerateTrimmableTypeMap` MSBuild task. The active runtime is selected by `$(_AndroidRuntime)` (`CoreCLR` or `NativeAOT`); runtime‑specific targets extend the shared pipeline (`…TypeMap.Trimmable.CoreCLR.targets`, `…TypeMap.Trimmable.NativeAOT.targets`). --- ## The pipeline (target graph) ### Shared / Debug CoreCLR & NativeAOT (no trimming) ``` CoreCompile └─► _GenerateTrimmableTypeMap (AfterTargets="CoreCompile") • scans @(ReferencePath) + framework/SDK assemblies + the app .dll • writes typemap/_*.TypeMap.dll + _Microsoft.Android.TypeMaps.dll • writes typemap/java/**/*.java (JCWs), acw-map.txt, merged AndroidManifest.xml • prunes stale JCWs, then touches typemap/_GenerateTrimmableTypeMap.stamp … _ReadGeneratedTrimmableTypeMapAssemblies (reads typemap-assemblies.txt → items) _PrepareTrimmableNativeConfigAssemblies (feeds _GeneratePackageManagerJava) _PrepareTrimmableTypeMapAssemblies (feeds packaging / assembly store) _GenerateJavaStubs (copies JCWs → android/src, manifest, acw-map) └─► _CompileJava ─► _CompileToDalvik ─► packaging ``` `_GenerateJavaStubs` **overrides** the legacy target of the same name from `BuildOrder.targets`; on the trimmable path the JCWs already exist, so it only copies them into `$(IntermediateOutputPath)android/src` and wires up the manifest/`acw-map.txt`/native config. ### Release CoreCLR (`PublishTrimmed=true`) — two generator passes ``` CoreCompile ─► _GenerateTrimmableTypeMap (pre-trim: full assembly set) … ILLink (trim) ─► CrossGen/R2R └─► _ComputePostTrimTrimmableTypeMapInputs (collect existing linked .dll) └─► _GeneratePostTrimTrimmableTypeMapJavaSources (post-trim) • regenerates JCWs from the *linked* assemblies only • writes typemap/linked-java/**/*.java (GenerateTypeMapAssemblies=false) • touches stamp/_GeneratePostTrimTrimmableTypeMapJavaSources.stamp _GenerateJavaStubs (copies linked-java → android/src) ``` The post‑trim pass exists because, after trimming, the set of types that still need JCWs is a **subset** of the pre‑trim set. `_GenerateJavaStubs` sources its JCWs from `$(_TypeMapJavaStubsSourceDirectory)`, which is `typemap/linked-java` for CoreCLR + `PublishTrimmed` and `typemap/java` otherwise. NativeAOT (`PublishTrimmed=true` as well) does **not** use the post‑trim Java pass — it feeds the pre‑trim JCWs to ILC and its own native steps — so for NativeAOT the sources stay `typemap/java`. --- ## How incrementality is achieved (deep dive) The pipeline follows the repo's [MSBuild best practices](https://github.com/dotnet/android/blob/main/Documentation/guides/MSBuildBestPractices.md): every expensive target declares `Inputs`/`Outputs`, re‑emits its dynamic `FileWrites`, and uses stamp files where a real output can't serve as a reliable timestamp sentinel. There are six principles at work. ### 1. Stamp files are the incremental sentinels — because the real outputs are content‑addressed `GenerateTrimmableTypeMap` writes every output with `Files.CopyIfStreamChanged` (and the per‑assembly `WriteAssembliesToDisk` additionally compares timestamps): **an output whose content did not change keeps its old timestamp**. That's exactly what you want to avoid churning downstream Java/native compilation — but it makes those files unusable as MSBuild `Outputs`, because their timestamps can be *older* than the `Inputs` even on a successful run, which makes MSBuild consider the target perpetually out‑of‑date and re‑run it every build. So `_GenerateTrimmableTypeMap` declares a **dedicated stamp** as its sole output and touches only that stamp: ```xml Inputs="@(ReferencePath);@(PrivateSdkAssemblies);@(FrameworkAssemblies);$(IntermediateOutputPath)$(TargetFileName);$(_AndroidManifestAbs);$(_AndroidBuildPropertiesCache)" Outputs="$(_TrimmableTypeMapOutputStamp)" … <Touch Files="$(_TrimmableTypeMapOutputStamp)" AlwaysCreate="true" /> ``` The stamp is always touched on a run and never touched on a skip, so the target is correctly skipped iff none of its inputs changed. The generated DLLs and `typemap-assemblies.txt` are **not** touched and are **not** declared as outputs — they are content‑addressed, and no other target consumes them as timestamp `Inputs` (verified), so touching them would be pure churn. *(This is the `@jonathanpeppers` review point.)* > The `Inputs` deliberately include `@(PrivateSdkAssemblies)` and `@(FrameworkAssemblies)`: they can contribute managed ↔ Java mappings, so a change in them must re‑run generation. `_GenerateJavaStubs` uses the analogous idea, but its sentinel must reflect **whichever** pass produced the JCWs it copies: ```xml Inputs="$(_TrimmableJavaSourceStamp);@(_EnvironmentFiles)" ``` where `_TrimmableJavaSourceStamp` resolves to the **post‑trim** stamp for CoreCLR + `PublishTrimmed`, and the **pre‑trim generator** stamp otherwise. This is what lets `_GenerateJavaStubs` react to post‑trim JCW regeneration while staying incremental on no‑op builds. *(This is the `@Copilot` review point; the non‑trim default was previously the TypeMap DLL, whose timestamp is unreliable for the reason above.)* ### 2. `Inputs` must reference files that actually exist MSBuild treats a **non‑existent `Inputs` file as "out of date"** and runs the target. The post‑trim pass originally declared `Inputs="@(ResolvedFileToPublish)"`, which also lists non‑assembly publish outputs — e.g. `…/bin/Release/<rid>/UnnamedProject.runtimeconfig.json` — whose paths do not exist when the target runs. That single phantom input made `_GeneratePostTrimTrimmableTypeMapJavaSources` run on **every** build, wiping and regenerating `linked-java` each time. The fix factors input collection into `_ComputePostTrimTrimmableTypeMapInputs`, which keeps only `.dll` items that exist on disk: ```xml <_PostTrimTrimmableTypeMapInputAssemblies Include="@(ResolvedFileToPublish)" Condition=" '%(Extension)' == '.dll' and Exists('%(FullPath)') and (…RID filter…) " /> ``` so the post‑trim pass is now genuinely incremental and its stamp is a trustworthy sentinel for `_GenerateJavaStubs`. ### 3. Dynamic outputs must be re‑published to `@(FileWrites)` on skipped builds The set of generated assemblies and JCWs is data‑dependent (it comes from scanning), so the `ItemGroup`s that register those files into `@(FileWrites)` live **inside** the generating targets — and therefore don't execute on a no‑op build where those targets are skipped. If nothing re‑declares them, MSBuild's `IncrementalClean` sees the previously‑tracked files as orphaned and **deletes them** before packaging reads its inputs. `_RecordTrimmableTypeMapFileWrites` runs unconditionally (gated only on the assemblies‑list file existing) and re‑emits the previous dynamic outputs back into `@(FileWrites)` before `IncrementalClean`: - the generated TypeMap assemblies (read back from `typemap-assemblies.txt`), - the pre‑trim JCWs (`typemap/java`) **and** their `android/src` copies, - the post‑trim JCWs (`typemap/linked-java`) **and** their `android/src` copies, plus the post‑trim stamp, and - the merged manifest, `acw-map.txt`, `ApplicationRegistration.java`, and the generator stamp. The post‑trim entries are new in this PR: once `_GeneratePostTrimTrimmableTypeMapJavaSources` became skippable (principle 2), its `linked-java` outputs would otherwise be deleted by `IncrementalClean` on the builds where it's skipped. The globs are empty for configurations that have no post‑trim pass, so this is a no‑op there. > Two more targets — `_PrepareTrimmableNativeConfigAssemblies` and `_PrepareTrimmableTypeMapAssemblies` — exist for the same "skipped target leaves item groups empty" reason: they re‑populate the TypeMap‑assembly item groups (used by packaging, the assembly store, and native config) from the on‑disk list via `BeforeTargets`, so incremental builds that skip generation still package correctly. ### 4. Content‑based writes + explicit stale pruning keep `android/src` exact Because generation is content‑addressed (`CopyIfStreamChanged`) and the `android/src` copy uses `SkipUnchangedFiles="true"`, an unchanged JCW never updates a timestamp and never triggers Java recompilation. The flip side is removal: when a JCW disappears between builds — a type deleted from source, or **trimmed away** on the `PublishTrimmed` path — its `android/src` copy must not linger, or it would be compiled and packaged. Both generator passes report the JCWs they no longer produce as `DeletedJavaFiles`; 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`: ```xml <Delete Files="@(_DeletedCopiedJavaFiles)" /> <Delete Files="$(_AndroidCompileJavaStampFile)" Condition=" '@(_DeletedCopiedJavaFiles->Count())' != '0' " /> ``` The two passes compute the deleted set differently because of how each manages its output directory: - **Pre‑trim** (`_GenerateTrimmableTypeMap`, writing `typemap/java`): the task scans the output dir and deletes any `*.java` the current pass didn't produce. - **Post‑trim** (`_GeneratePostTrimTrimmableTypeMapJavaSources`, writing `typemap/linked-java` with `CleanJavaSourceOutputDirectory=true`): the dir 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 like `ApplicationRegistration.java`. The net invariant is two‑directional: **`android/src` contains exactly the JCWs the active pass produces** (`typemap/linked-java` for `PublishTrimmed`, `typemap/java` otherwise) — no missing files (copied by `_GenerateJavaStubs`) and no stale files (pruned via `DeletedJavaFiles`). Exercised directly by the `…KeepsAndroidSrcConsistentWithLinkedJava` and `…DeletesStaleAndroidSrcWhenLinkedJavaShrinks` tests. ### 5. The generator is skipped in design‑time builds and runs exactly once `_GenerateTrimmableTypeMap` is gated on `'$(DesignTimeBuild)' != 'true'`: in a design‑time build, project references may resolve to target paths that aren't produced when `SkipCompilerExecution=true`, and the output isn't needed for IDE information. Combined with the existing `'$(_OuterIntermediateOutputPath)' == ''` guard (which skips inner per‑RID builds), the generator runs exactly once per outer build. ### 6. Two‑pass consistency under trimming For CoreCLR + `PublishTrimmed`, correctness depends on the pre‑trim and post‑trim sentinels composing cleanly: - **No‑op rebuild** — both passes are skipped (their inputs are unchanged); `_GenerateJavaStubs` keys off the stable post‑trim stamp and is also skipped. *(Newly true; previously the post‑trim pass ran every build.)* - **Managed source change** — the pre‑trim generator re‑runs (its inputs changed) and re‑linking changes the linked assemblies, so the post‑trim pass re‑runs too; `_GenerateJavaStubs` re‑copies. - **Trim‑only change** (e.g. a change that alters the linked output without touching the pre‑trim inputs) — the linked `.dll` change re‑runs the post‑trim pass, which touches the post‑trim stamp, so `_GenerateJavaStubs` re‑runs even though the pre‑trim generator did not. This is precisely the staleness the `@Copilot` review flagged, and is why `_GenerateJavaStubs` must key off `_TrimmableJavaSourceStamp` rather than the generator stamp alone. --- ## What changed in this PR | Area | Change | | ---- | ------ | | `…TypeMap.Trimmable.targets` | Stamp sentinel for `_GenerateTrimmableTypeMap` (sole `Output`, sole `Touch`); expanded `Inputs` (`PrivateSdkAssemblies`/`FrameworkAssemblies`); design‑time‑build skip; `SkipUnchangedFiles` JCW copy; stale‑JCW deletion wiring; `_GenerateJavaStubs` keyed off `_TrimmableJavaSourceStamp`; post‑trim outputs re‑emitted in `_RecordTrimmableTypeMapFileWrites`. | | `…TypeMap.Trimmable.CoreCLR.targets` | New `_ComputePostTrimTrimmableTypeMapInputs` (existing‑`.dll` inputs) so `_GeneratePostTrimTrimmableTypeMapJavaSources` is incremental; post‑trim pass now mirrors `DeletedJavaFiles` into `android/src` + busts the Java compile stamp. | | `Tasks/GenerateTrimmableTypeMap.cs` | New `DeletedJavaFiles` output + `DeleteStaleJavaSources()`; in the `CleanJavaSourceOutputDirectory` (post‑trim) case it snapshots the prior JCW set before the wipe and reports `previous − regenerated`. | | `Microsoft.Android.Sdk.TrimmableTypeMap/README.md` | Documents the pipeline and its incrementality design. | | `TrimmableTypeMapBuildTests.cs` | Tests for stale‑JCW pruning (both directions), updated‑JCW copying, the `android/src` ↔ `linked-java` invariant, post‑trim no‑op incrementality, and stale `android/src` removal when `linked-java` shrinks. | ### Review feedback addressed - **@Copilot** (×2): `_GenerateJavaStubs` now keys off `$(_TrimmableJavaSourceStamp)` (post‑trim stamp for `PublishTrimmed`, generator stamp otherwise), and the non‑trim default of `_TrimmableJavaSourceStamp` was changed from the TypeMap DLL to the generator stamp. The underlying reason the bare generator stamp was insufficient — a non‑incremental post‑trim pass — is fixed too. - **@jonathanpeppers**: `_GenerateTrimmableTypeMap` declares only the stamp as its `Outputs` and touches only the stamp; the DLLs/list are content‑addressed and not consumed as timestamp inputs elsewhere. --- ## Scope notes (and a little beyond) - All changes are gated on `_AndroidTypeMapImplementation == 'trimmable'`; non‑trimmable (`llvm-ir`/`managed`) builds are unaffected. - Unlike #11617, this PR **keeps** `main`'s CoreCLR post‑trim `linked-java` machinery intact and instead makes it incremental. - **Beyond this PR**, the post‑trim pass still regenerates `linked-java` wholesale (`CleanJavaSourceOutputDirectory=true`) when it runs; it now also reports the JCWs it dropped so `android/src` stays exact. A further optimization would replace the wholesale clean with content‑based writes + in‑place stale pruning (as the pre‑trim pass does), letting unchanged `linked-java` JCWs keep stable timestamps across a post‑trim run. Left as a follow‑up. --- ## Testing Verified locally against a Debug local SDK. **Host (`Xamarin.Android.Build.Tests`, full `TrimmableTypeMapBuildTests`): 24 passed, 2 skipped, 0 failed**, including: - `…IncrementalBuild` (Debug CoreCLR, Release CoreCLR, Release NativeAOT) - `…DeletesStaleGeneratedJavaSourcesAndCopies`, `…CopiesUpdatedGeneratedJavaSources` - `…PublishTrimmed_KeepsAndroidSrcConsistentWithLinkedJava` (the `android/src` ↔ `linked-java` invariant) - `…PublishTrimmed_DeletesStaleAndroidSrcWhenLinkedJavaShrinks` (a JCW dropped from `linked-java` is removed from `android/src`) - `…PublishTrimmed_PostTrimJavaGenerationIsIncremental` (no‑op rebuild skips post‑trim and `_GenerateJavaStubs`) - `…Succeeds`, `…ArrayRankChangeRegeneratesTypeMap`, `…DoesNotHitCopyIfChangedMismatch`, R2R/multi‑RID packaging tests **Emulator (`MSBuildDeviceIntegration`, API 36 / arm64):** `DotNetRun` deploy+run for trimmable CoreCLR (Debug + Release) and NativeAOT (Release), and `TrimmableTypeMapInheritedVirtualOverrideUsesCorrectUco`.
1 parent 222aecd commit 174e481

5 files changed

Lines changed: 545 additions & 18 deletions

File tree

Lines changed: 165 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,165 @@
1+
# Trimmable typemap build pipeline
2+
3+
This document describes how the **trimmable** typemap implementation
4+
(`_AndroidTypeMapImplementation=trimmable`) is produced during an Android app
5+
build, and how the MSBuild targets are kept incremental. It is aimed at
6+
contributors working on the targets in
7+
`src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.TypeMap.Trimmable*.targets`
8+
and the `GenerateTrimmableTypeMap` MSBuild task.
9+
10+
## Background
11+
12+
The legacy typemap implementations (`llvm-ir`, `managed`) embed the
13+
managed&nbsp;&nbsp;Java type mapping into native binaries. The **trimmable**
14+
implementation instead generates a set of small managed *TypeMap assemblies*
15+
(one per input assembly, plus a `_Microsoft.Android.TypeMaps` root) and the Java
16+
Callable Wrapper (JCW) `*.java` sources from the same scan. This keeps the
17+
mapping trimmer-friendly: unused entries are removed by the IL linker.
18+
19+
The work happens in the `GenerateTrimmableTypeMap` task, invoked from the
20+
`_GenerateTrimmableTypeMap` target. The runtime is selected by `_AndroidRuntime`
21+
(`CoreCLR` or `NativeAOT`); the runtime-specific imports
22+
(`*.Trimmable.CoreCLR.targets`, `*.Trimmable.NativeAOT.targets`) extend the
23+
shared pipeline.
24+
25+
## Target pipeline (CoreCLR, non-trimmed Debug build)
26+
27+
```
28+
CoreCompile
29+
└─► _GenerateTrimmableTypeMap (AfterTargets="CoreCompile")
30+
• scans @(ReferencePath) + framework/SDK assemblies + the app .dll
31+
• writes typemap/_*.TypeMap.dll + _Microsoft.Android.TypeMaps.dll
32+
• writes typemap/java/**/*.java (JCWs) and acw-map.txt
33+
• writes the merged AndroidManifest.xml
34+
• touches typemap/_GenerateTrimmableTypeMap.stamp
35+
...
36+
_ReadGeneratedTrimmableTypeMapAssemblies (reads typemap-assemblies.txt)
37+
_PrepareTrimmableNativeConfigAssemblies (feeds _GeneratePackageManagerJava)
38+
_PrepareTrimmableTypeMapAssemblies (feeds packaging / assembly store)
39+
_CollectTrimmableTypeMapJavaFiles (globs the JCW *.java)
40+
_GenerateJavaStubs (copies JCWs into android/src, manifest, acw-map)
41+
└─► _CompileJava ─► _CompileToDalvik ─► packaging
42+
```
43+
44+
`_GenerateJavaStubs` **overrides** the legacy target of the same name from
45+
`BuildOrder.targets`; in the trimmable path the JCWs already exist, so this
46+
target only copies them into `$(IntermediateOutputPath)android/src` and wires up
47+
the manifest, `acw-map.txt`, and native config.
48+
49+
For `CoreCLR` + `PublishTrimmed=true`, a second pass
50+
(`_GeneratePostTrimTrimmableTypeMapJavaSources`, in the CoreCLR targets)
51+
regenerates the JCWs from the **linked** assemblies into a `linked-java`
52+
directory, which then becomes the source for `_GenerateJavaStubs`.
53+
54+
## Incrementality design
55+
56+
The pipeline follows the repository's
57+
[MSBuild best practices](../../Documentation/guides/MSBuildBestPractices.md):
58+
every expensive target declares `Inputs`/`Outputs`, re-emits its dynamic
59+
`FileWrites`, and uses stamp files where a real output cannot serve as a
60+
reliable timestamp sentinel.
61+
62+
### 1. A stamp file is the generator's incremental sentinel
63+
64+
`_GenerateTrimmableTypeMap` declares:
65+
66+
```xml
67+
Inputs="@(ReferencePath);@(PrivateSdkAssemblies);@(FrameworkAssemblies);$(IntermediateOutputPath)$(TargetFileName);$(_AndroidManifestAbs);$(_AndroidBuildPropertiesCache)"
68+
Outputs="$(_TypeMapOutputDirectory)$(_TypeMapAssemblyName).dll;$(_TypeMapAssembliesListFile);$(_TrimmableTypeMapOutputStamp)"
69+
```
70+
71+
The generated TypeMap DLLs are written with `Files.CopyIfStreamChanged`, so an
72+
assembly whose **content** is unchanged keeps its old timestamp. If those DLLs
73+
were the only `Outputs`, MSBuild would consider the target perpetually
74+
out-of-date (its inputs are always newer than the untouched outputs) and re-run
75+
it on every build. To avoid this, the target unconditionally `Touch`es a
76+
dedicated stamp:
77+
78+
```xml
79+
<Touch Files="@(_GeneratedTypeMapAssemblies);$(_TypeMapAssembliesListFile);$(_TrimmableTypeMapOutputStamp)" AlwaysCreate="true" />
80+
```
81+
82+
so the stamp is always newer than the inputs after a run, and the target is
83+
correctly **skipped** when none of the inputs changed.
84+
85+
> All assemblies that can contribute managed&nbsp;&nbsp;Java mappings must be
86+
> inputs — including `@(PrivateSdkAssemblies)` and `@(FrameworkAssemblies)`
87+
> otherwise a change in one of them would not trigger regeneration.
88+
89+
### 2. `_GenerateJavaStubs` keys off the stamp
90+
91+
```xml
92+
Inputs="$(_TrimmableTypeMapOutputStamp);@(_EnvironmentFiles)"
93+
Outputs="$(_AndroidStampDirectory)_GenerateJavaStubs.stamp"
94+
```
95+
96+
The stamp captures "the generator ran because its inputs changed" and is left
97+
stable when the generator is skipped, so the JCW copy into `android/src` only
98+
re-runs when something relevant actually changed. The copy uses
99+
`SkipUnchangedFiles="true"` so unchanged JCWs do not churn downstream Java
100+
compilation. For `CoreCLR` + `PublishTrimmed`, the JCWs are sourced from the
101+
`linked-java` directory produced by `_GeneratePostTrimTrimmableTypeMapJavaSources`,
102+
which is itself incremental; the stamp remains the sentinel so a no-op build
103+
still skips `_GenerateJavaStubs`.
104+
105+
### 3. Stale generated Java sources are pruned (both passes)
106+
107+
When a managed type is removed — or trimmed away on the `PublishTrimmed` path —
108+
its JCW must not linger in `android/src`, where it would otherwise be compiled
109+
and packaged. Both generator passes report the JCWs they no longer produce as
110+
`DeletedJavaFiles` (with `RelativePath` metadata), and the owning target mirrors
111+
each deletion into the `android/src` copy and, if anything was deleted, deletes
112+
`$(_AndroidCompileJavaStampFile)` so `_CompileJava` re-runs and drops the stale
113+
`.class` outputs:
114+
115+
```xml
116+
<Delete Files="@(_DeletedCopiedJavaFiles)" />
117+
<Delete Files="$(_AndroidCompileJavaStampFile)" Condition=" '@(_DeletedCopiedJavaFiles->Count())' != '0' " />
118+
```
119+
120+
The two passes compute the deleted set differently because of how each manages
121+
its output directory:
122+
123+
- **Pre-trim** (`_GenerateTrimmableTypeMap`, writing `typemap/java`): the task
124+
scans the output directory and deletes any `*.java` the current pass did not
125+
produce.
126+
- **Post-trim** (`_GeneratePostTrimTrimmableTypeMapJavaSources`, writing
127+
`typemap/linked-java` with `CleanJavaSourceOutputDirectory=true`): the
128+
directory is wiped before regeneration, so the task snapshots the previous
129+
`*.java` set *before* the wipe and reports `previous − regenerated`. This keeps
130+
the deletion precise — only files the generator itself previously produced are
131+
ever removed from `android/src`, never unrelated sources such as
132+
`ApplicationRegistration.java`.
133+
134+
The invariant is two-directional: **`android/src` contains exactly the JCWs the
135+
active pass produces** — no missing files (copied via `_GenerateJavaStubs`) and
136+
no stale files (pruned via `DeletedJavaFiles`).
137+
138+
### 4. Dynamic `FileWrites` are re-emitted on no-op builds
139+
140+
The set of generated assemblies and JCWs is data-dependent, so a build that
141+
*skips* `_GenerateTrimmableTypeMap` never executes the `ItemGroup` that registers
142+
those files in `@(FileWrites)`. `_RecordTrimmableTypeMapFileWrites` re-reads the
143+
generated outputs from `typemap-assemblies.txt` (and globs the JCWs) and
144+
re-emits them — plus the stamp — into `@(FileWrites)` *before* MSBuild's
145+
`IncrementalClean`, so the outputs are not seen as orphaned and deleted between
146+
incremental builds.
147+
148+
### 5. The generator does not run in design-time builds, and runs once
149+
150+
`_GenerateTrimmableTypeMap` is gated on `'$(DesignTimeBuild)' != 'true'`: in a
151+
design-time build, project references may resolve to target paths that are not
152+
produced when `SkipCompilerExecution=true`, and the generator output is not
153+
needed to provide IDE information. Combined with the
154+
`'$(_OuterIntermediateOutputPath)' == ''` guard (which skips inner per-RID
155+
builds), the generator runs exactly once per outer build.
156+
157+
## Files
158+
159+
| File | Role |
160+
| ---- | ---- |
161+
| `Microsoft.Android.Sdk.TypeMap.Trimmable.targets` | Shared pipeline: generation, Java stubs, packaging hookup, incremental `FileWrites`. |
162+
| `Microsoft.Android.Sdk.TypeMap.Trimmable.CoreCLR.targets` | CoreCLR specifics, incl. the post-trim `linked-java` regeneration. |
163+
| `Microsoft.Android.Sdk.TypeMap.Trimmable.NativeAOT.targets` | NativeAOT specifics (ILC inputs, proguard). |
164+
| `Tasks/GenerateTrimmableTypeMap.cs` | The MSBuild task front-end for the generator. |
165+
| `Microsoft.Android.Sdk.TrimmableTypeMap/**` | The generator/scanner library invoked by the task. |

src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.TypeMap.Trimmable.CoreCLR.targets

Lines changed: 32 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -48,16 +48,28 @@
4848
OutputFile="$(_ProguardProjectConfiguration)" />
4949
</Target>
5050

51+
<Target Name="_ComputePostTrimTrimmableTypeMapInputs"
52+
Condition=" '$(_AndroidTypeMapImplementation)' == 'trimmable' and '$(PublishTrimmed)' == 'true' and '$(_ComputeFilesToPublishForRuntimeIdentifiers)' != 'true' "
53+
AfterTargets="_ResolveAssemblies">
54+
<ItemGroup>
55+
<_PostTrimTrimmableTypeMapInputAssemblies Remove="@(_PostTrimTrimmableTypeMapInputAssemblies)" />
56+
<!-- Only the linked .dll assemblies that already exist on disk are valid inputs.
57+
@(ResolvedFileToPublish) also contains non-assembly publish outputs (e.g.
58+
runtimeconfig.json) whose paths do not exist when this target runs; declaring a
59+
non-existent file as an Input would make MSBuild consider the target perpetually
60+
out-of-date and run it on every build. -->
61+
<_PostTrimTrimmableTypeMapInputAssemblies Include="@(ResolvedFileToPublish)"
62+
Condition=" '%(Extension)' == '.dll' and Exists('%(FullPath)') and ('%(RuntimeIdentifier)' == '' or '$(_PostTrimTypeMapFirstRuntimeIdentifier)' == '' or '%(RuntimeIdentifier)' == '$(_PostTrimTypeMapFirstRuntimeIdentifier)') " />
63+
</ItemGroup>
64+
</Target>
65+
5166
<Target Name="_GeneratePostTrimTrimmableTypeMapJavaSources"
5267
Condition=" '$(_AndroidTypeMapImplementation)' == 'trimmable' and '$(PublishTrimmed)' == 'true' and '$(_ComputeFilesToPublishForRuntimeIdentifiers)' != 'true' "
68+
DependsOnTargets="_ComputePostTrimTrimmableTypeMapInputs"
5369
AfterTargets="_ResolveAssemblies"
5470
BeforeTargets="_GenerateJavaStubs;_CompileJava;_CompileToDalvik"
55-
Inputs="@(ResolvedFileToPublish)"
71+
Inputs="@(_PostTrimTrimmableTypeMapInputAssemblies)"
5672
Outputs="$(_PostTrimTrimmableTypeMapJavaStamp)">
57-
<ItemGroup>
58-
<_PostTrimTrimmableTypeMapInputAssemblies Include="@(ResolvedFileToPublish)"
59-
Condition=" '%(Extension)' == '.dll' and ('%(RuntimeIdentifier)' == '' or '$(_PostTrimTypeMapFirstRuntimeIdentifier)' == '' or '%(RuntimeIdentifier)' == '$(_PostTrimTypeMapFirstRuntimeIdentifier)') " />
60-
</ItemGroup>
6173

6274
<GenerateTrimmableTypeMap
6375
ResolvedAssemblies="@(_PostTrimTrimmableTypeMapInputAssemblies)"
@@ -73,18 +85,33 @@
7385
AcwMapOutputFile="$(IntermediateOutputPath)acw-map.txt"
7486
ApplicationRegistrationOutputFile="$(IntermediateOutputPath)android/src/net/dot/android/ApplicationRegistration.java">
7587
<Output TaskParameter="GeneratedJavaFiles" ItemName="_PostTrimGeneratedJavaFiles" />
88+
<Output TaskParameter="DeletedJavaFiles" ItemName="_PostTrimDeletedJavaFiles" />
7689
</GenerateTrimmableTypeMap>
7790

91+
<!-- Mirror any JCWs the post-trim pass no longer produces (e.g. trimmed-away types) into
92+
the android/src copies so a stale .java (and stale .class) is not left behind. Busting
93+
the Java compile stamp forces _CompileJava to drop the corresponding class output. -->
94+
<ItemGroup>
95+
<_PostTrimDeletedCopiedJavaFiles Remove="@(_PostTrimDeletedCopiedJavaFiles)" />
96+
<_PostTrimDeletedCopiedJavaFiles Include="@(_PostTrimDeletedJavaFiles->'$(IntermediateOutputPath)android/src/%(RelativePath)')" />
97+
</ItemGroup>
98+
<Delete Files="@(_PostTrimDeletedCopiedJavaFiles)" />
99+
<Delete Files="$(_AndroidCompileJavaStampFile)" Condition=" '@(_PostTrimDeletedCopiedJavaFiles->Count())' != '0' " />
100+
78101
<MakeDir Directories="$([System.IO.Path]::GetDirectoryName('$(_PostTrimTrimmableTypeMapJavaStamp)'))" />
79102
<Touch Files="$(_PostTrimTrimmableTypeMapJavaStamp)" AlwaysCreate="true" />
80103

81104
<ItemGroup>
105+
<FileWrites Remove="@(_PostTrimDeletedJavaFiles)" />
106+
<FileWrites Remove="@(_PostTrimDeletedCopiedJavaFiles)" />
82107
<FileWrites Include="@(_PostTrimGeneratedJavaFiles)" />
83108
<FileWrites Include="$(IntermediateOutputPath)acw-map.txt" />
84109
<FileWrites Include="$(IntermediateOutputPath)android/src/net/dot/android/ApplicationRegistration.java" />
85110
<FileWrites Include="$(_PostTrimTrimmableTypeMapJavaStamp)" />
86111
<_PostTrimTrimmableTypeMapInputAssemblies Remove="@(_PostTrimTrimmableTypeMapInputAssemblies)" />
87112
<_PostTrimGeneratedJavaFiles Remove="@(_PostTrimGeneratedJavaFiles)" />
113+
<_PostTrimDeletedJavaFiles Remove="@(_PostTrimDeletedJavaFiles)" />
114+
<_PostTrimDeletedCopiedJavaFiles Remove="@(_PostTrimDeletedCopiedJavaFiles)" />
88115
</ItemGroup>
89116
</Target>
90117
<!-- Add linked TypeMap DLLs to the normal publish assembly pipeline. The SDK R2R

0 commit comments

Comments
 (0)