Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
112 changes: 68 additions & 44 deletions src/Microsoft.Android.Sdk.TrimmableTypeMap/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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
Expand All @@ -76,7 +75,7 @@ it on every build. To avoid this, the target unconditionally `Touch`es a
dedicated stamp:

```xml
<Touch Files="@(_GeneratedTypeMapAssemblies);$(_TypeMapAssembliesListFile);$(_TrimmableTypeMapOutputStamp)" AlwaysCreate="true" />
<Touch Files="$(_TrimmableTypeMapOutputStamp)" AlwaysCreate="true" />
```

so the stamp is always newer than the inputs after a run, and the target is
Expand All @@ -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
<Delete Files="@(_DeletedCopiedJavaFiles)" />
<Delete Files="$(_AndroidCompileJavaStampFile)" Condition=" '@(_DeletedCopiedJavaFiles->Count())' != '0' " />
<Delete Files="$(_AndroidCompileJavaStampFile)" Condition=" '@(_DeletedJavaFiles->Count())' != '0' " />
```

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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.
-->
<Target Name="_PostTrimmingPipeline"
AfterTargets="ILLink"
<Target Name="_CollectPostTrimmingAssemblies"
Condition=" '$(PublishTrimmed)' == 'true' ">
<ItemGroup>
<_PostTrimmingAssembly Remove="@(_PostTrimmingAssembly)" />
<_PostTrimmingAssembly Include="@(ResolvedFileToPublish)" Condition=" '%(Extension)' == '.dll' and '%(ResolvedFileToPublish.PostprocessAssembly)' == 'true' " />
</ItemGroup>
</Target>

<Target Name="_PostTrimmingPipeline"
AfterTargets="ILLink"
DependsOnTargets="_CollectPostTrimmingAssemblies"
Condition=" '$(PublishTrimmed)' == 'true' "
Inputs="@(_PostTrimmingAssembly);$(_AndroidBuildPropertiesCache);@(_AndroidMSBuildAllProjects)"
Outputs="$(_AndroidStampDirectory)_PostTrimmingPipeline.stamp">
<PostTrimmingPipeline
Assemblies="@(_PostTrimmingAssembly)"
AddKeepAlives="$(AndroidAddKeepAlives)"
AndroidLinkResources="$(AndroidLinkResources)"
Deterministic="$(Deterministic)" />
<MakeDir Directories="$(_AndroidStampDirectory)" />
<Touch Files="$(_AndroidStampDirectory)_PostTrimmingPipeline.stamp" AlwaysCreate="true" />
<ItemGroup>
<FileWrites Include="$(_AndroidStampDirectory)_PostTrimmingPipeline.stamp" />
</ItemGroup>
</Target>

<!-- Inject _TypeMapKind into the property cache -->
Expand All @@ -304,6 +317,7 @@
</PropertyGroup>
<ItemGroup>
<_PropertyCacheItems Include="TypeMapKind=$(_TypeMapKind)" />
<_PropertyCacheItems Include="Deterministic=$(Deterministic)" />
</ItemGroup>
</Target>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,9 @@
<!-- Add TypeMap DLLs to ILLink input. ILLink natively understands TypeMapAttribute<T>. -->
<Target Name="_AddTrimmableTypeMapToLinker"
BeforeTargets="PrepareForILLink;_RunILLink"
DependsOnTargets="_GenerateTrimmableTypeMap">
DependsOnTargets="_ReadGeneratedTrimmableTypeMapAssemblies">
<ItemGroup>
<ManagedAssemblyToLink Include="$(_TypeMapOutputDirectory)*.dll">
<ManagedAssemblyToLink Include="@(_GeneratedTypeMapAssembliesFromList)">
<DestinationSubPath>%(Filename)%(Extension)</DestinationSubPath>
<IsTrimmable>true</IsTrimmable>
</ManagedAssemblyToLink>
Expand Down Expand Up @@ -65,12 +65,45 @@
</ItemGroup>
</Target>

<!-- The stamp is the timestamp sentinel because generated files use content-based writes and
can legitimately be older than their inputs. If IncrementalClean or a manual cleanup removes
a real output but leaves the stamp, invalidate the stamp so the producer restores the complete
post-trim output set instead of letting _GenerateJavaStubs create an empty acw-map placeholder. -->
<Target Name="_InvalidatePostTrimTrimmableTypeMapStampIfOutputsMissing"
Condition=" '$(_AndroidTypeMapImplementation)' == 'trimmable' and '$(PublishTrimmed)' == 'true' and '$(_ComputeFilesToPublishForRuntimeIdentifiers)' != 'true' ">
<ItemGroup>
<_ExpectedPostTrimJavaFile Remove="@(_ExpectedPostTrimJavaFile)" />
<_ActualPostTrimJavaFile Remove="@(_ActualPostTrimJavaFile)" />
<_MissingPostTrimJavaFile Remove="@(_MissingPostTrimJavaFile)" />
<_UnexpectedPostTrimJavaFile Remove="@(_UnexpectedPostTrimJavaFile)" />
</ItemGroup>
<ReadLinesFromFile
File="$(_PostTrimTypeMapJavaFilesList)"
Condition="Exists('$(_PostTrimTypeMapJavaFilesList)')">
<Output TaskParameter="Lines" ItemName="_ExpectedPostTrimJavaFile" />
</ReadLinesFromFile>
<ItemGroup>
<_ActualPostTrimJavaFile Include="$(_PostTrimTypeMapJavaOutputDirectory)/**/*.java" />
Comment thread
jonathanpeppers marked this conversation as resolved.
<_MissingPostTrimJavaFile Include="@(_ExpectedPostTrimJavaFile)" Condition="!Exists('%(_ExpectedPostTrimJavaFile.Identity)')" />
<_UnexpectedPostTrimJavaFile Include="@(_ActualPostTrimJavaFile->'%(FullPath)')" Exclude="@(_ExpectedPostTrimJavaFile)" />
</ItemGroup>
<Delete
Files="$(_PostTrimTrimmableTypeMapJavaStamp)"
Condition="Exists('$(_PostTrimTrimmableTypeMapJavaStamp)') and (!Exists('$(_PostTrimTypeMapJavaOutputDirectory)') or !Exists('$(_PostTrimTypeMapJavaFilesList)') or '@(_MissingPostTrimJavaFile->Count())' != '0' or '@(_UnexpectedPostTrimJavaFile->Count())' != '0' or !Exists('$(IntermediateOutputPath)acw-map.txt') or !Exists('$(IntermediateOutputPath)android/src/net/dot/android/ApplicationRegistration.java'))" />
<ItemGroup>
<_ExpectedPostTrimJavaFile Remove="@(_ExpectedPostTrimJavaFile)" />
<_ActualPostTrimJavaFile Remove="@(_ActualPostTrimJavaFile)" />
<_MissingPostTrimJavaFile Remove="@(_MissingPostTrimJavaFile)" />
<_UnexpectedPostTrimJavaFile Remove="@(_UnexpectedPostTrimJavaFile)" />
</ItemGroup>
</Target>

<Target Name="_GeneratePostTrimTrimmableTypeMapJavaSources"
Condition=" '$(_AndroidTypeMapImplementation)' == 'trimmable' and '$(PublishTrimmed)' == 'true' and '$(_ComputeFilesToPublishForRuntimeIdentifiers)' != 'true' "
DependsOnTargets="_ComputePostTrimTrimmableTypeMapInputs"
DependsOnTargets="_ComputePostTrimTrimmableTypeMapInputs;_InvalidatePostTrimTrimmableTypeMapStampIfOutputsMissing"
AfterTargets="_ResolveAssemblies"
BeforeTargets="_GenerateJavaStubs;_CompileJava;_CompileToDalvik"
Inputs="@(_PostTrimTrimmableTypeMapInputAssemblies)"
Inputs="@(_PostTrimTrimmableTypeMapInputAssemblies);$(_ResolvedUserAssembliesHashFile);$(_AndroidBuildPropertiesCache);@(_AndroidMSBuildAllProjects)"
Outputs="$(_PostTrimTrimmableTypeMapJavaStamp)">

<GenerateTrimmableTypeMap
Expand All @@ -82,37 +115,37 @@
TargetFrameworkVersion="$(TargetFrameworkVersion)"
PackageNamingPolicy="$(_TrimmableTypeMapPackageNamingPolicy)"
GenerateTypeMapAssemblies="false"
CleanJavaSourceOutputDirectory="true"
CleanJavaSourceOutputDirectory="false"
AcwMapOutputFile="$(IntermediateOutputPath)acw-map.txt"
ApplicationRegistrationOutputFile="$(IntermediateOutputPath)android/src/net/dot/android/ApplicationRegistration.java">
<Output TaskParameter="GeneratedJavaFiles" ItemName="_PostTrimGeneratedJavaFiles" />
<Output TaskParameter="DeletedJavaFiles" ItemName="_PostTrimDeletedJavaFiles" />
</GenerateTrimmableTypeMap>

<!-- Mirror any JCWs the post-trim pass no longer produces (e.g. trimmed-away types) into
the android/src copies so a stale .java (and stale .class) is not left behind. Busting
the Java compile stamp forces _CompileJava to drop the corresponding class output. -->
<ItemGroup>
<_PostTrimDeletedCopiedJavaFiles Remove="@(_PostTrimDeletedCopiedJavaFiles)" />
<_PostTrimDeletedCopiedJavaFiles Include="@(_PostTrimDeletedJavaFiles->'$(IntermediateOutputPath)android/src/%(RelativePath)')" />
</ItemGroup>
<Delete Files="@(_PostTrimDeletedCopiedJavaFiles)" />
<Delete Files="$(_AndroidCompileJavaStampFile)" Condition=" '@(_PostTrimDeletedCopiedJavaFiles->Count())' != '0' " />
<WriteLinesToFile
File="$(_PostTrimTypeMapJavaFilesList)"
Lines="@(_PostTrimGeneratedJavaFiles->'%(FullPath)')"
Overwrite="true"
WriteOnlyWhenDifferent="true" />

<!-- The task updates linked-java in place, preserving timestamps for unchanged JCWs, and
reports any sources it pruned (e.g. trimmed-away types). Busting the Java compile stamp
for removals forces _CompileJava to drop the corresponding class output. -->
<Delete Files="$(_AndroidCompileJavaStampFile)" Condition=" '@(_PostTrimDeletedJavaFiles->Count())' != '0' " />

<MakeDir Directories="$([System.IO.Path]::GetDirectoryName('$(_PostTrimTrimmableTypeMapJavaStamp)'))" />
<Touch Files="$(_PostTrimTrimmableTypeMapJavaStamp)" AlwaysCreate="true" />

<ItemGroup>
<FileWrites Remove="@(_PostTrimDeletedJavaFiles)" />
<FileWrites Remove="@(_PostTrimDeletedCopiedJavaFiles)" />
<FileWrites Include="@(_PostTrimGeneratedJavaFiles)" />
<FileWrites Include="$(_PostTrimTypeMapJavaFilesList)" />
<FileWrites Include="$(IntermediateOutputPath)acw-map.txt" />
<FileWrites Include="$(IntermediateOutputPath)android/src/net/dot/android/ApplicationRegistration.java" />
<FileWrites Include="$(_PostTrimTrimmableTypeMapJavaStamp)" />
<_PostTrimTrimmableTypeMapInputAssemblies Remove="@(_PostTrimTrimmableTypeMapInputAssemblies)" />
<_PostTrimGeneratedJavaFiles Remove="@(_PostTrimGeneratedJavaFiles)" />
<_PostTrimDeletedJavaFiles Remove="@(_PostTrimDeletedJavaFiles)" />
<_PostTrimDeletedCopiedJavaFiles Remove="@(_PostTrimDeletedCopiedJavaFiles)" />
</ItemGroup>
</Target>
<!-- After ILLink trims per-assembly _X.TypeMap assemblies whose target Java binding was unused,
Expand Down
Loading