-
Notifications
You must be signed in to change notification settings - Fork 568
Description
Part of #10788
Blocking: #10800
Problem
The legacy _GenerateJavaStubs target in Microsoft.Android.Sdk.TypeMap.LlvmIr.targets is a mega-target that bundles 7 task invocations into a single MSBuild target:
GenerateJavaCallableWrappers— generate JCW.javafilesGenerateACWMap— generateacw-map.txt(managed name → Java/ACW name mapping)GenerateJavaStubs— Cecil-based type scanning, buildsNativeCodeGenStateRewriteMarshalMethods— rewrite assemblies with marshal method stubsGenerateTypeMappings— generate LLVM IR typemap lookup tablesGenerateMainAndroidManifest— merge user manifest + attribute-generated entriesGenerateAdditionalProviderSources— generate runtime provider.javafiles
The trimmable path (#10779) overrides _GenerateJavaStubs to a no-op, which means tasks 1, 2, 6, and 7 are also skipped — but some of these are NOT typemap-specific and are needed by any build path.
Analysis: Which tasks are shared vs. typemap-specific
Shared tasks (needed by both legacy and trimmable)
| Task | Why it is shared | Trimmable path action |
|---|---|---|
GenerateMainAndroidManifest |
Every Android app needs a merged manifest. Users can hand-edit their manifest with component entries that have no C# attribute. | Reuse as-is — needs to be extracted from the mega-target so both paths can invoke it. Currently depends on NativeCodeGenState from Cecil scanning — must be decoupled or fed from JavaPeerScanner results. |
GenerateAdditionalProviderSources |
Runtime provider .java files are needed regardless of typemap strategy. |
Reuse as-is — depends on GenerateMainAndroidManifest output. |
GenerateACWMap |
Produces acw-map.txt consumed by _ConvertCustomView to fix up custom view names in layout XMLs. Without it, custom views in layouts break. |
Generate from JavaPeerScanner results — the scanner already computes CRC64 JNI names. Writing ManagedName;JavaName lines from JavaPeerInfo is trivial. No need to reuse the Cecil-based task. |
Typemap-specific tasks (legacy only, NOT needed by trimmable)
| Task | Why it is legacy-only |
|---|---|
GenerateJavaCallableWrappers |
Cecil-based JCW generation. Trimmable path generates JCWs from JavaPeerInfo in the generator (#10799). |
GenerateJavaStubs |
Cecil-based type scanning → NativeCodeGenState. Replaced by JavaPeerScanner (#10798). |
RewriteMarshalMethods |
Rewrites assemblies with marshal method stubs. Trimmable path uses RegisterNatives instead. |
GenerateTypeMappings |
LLVM IR typemap generation. Replaced by TypeMapAttribute-based assembly. |
What the legacy LLVM IR flow looks like today
_GenerateJavaStubs (LlvmIr.targets, single monolithic target)
├── GenerateJavaCallableWrappers → JCW .java files
├── GenerateACWMap → acw-map.txt (managed name → Java name)
├── GenerateJavaStubs → Cecil scan → NativeCodeGenState
├── RewriteMarshalMethods → assembly rewriting (Release)
├── GenerateTypeMappings → LLVM IR .ll files → compiled to .o
├── GenerateMainAndroidManifest → merged AndroidManifest.xml
└── GenerateAdditionalProviderSources → provider .java files
_ConvertCustomView (Common.targets, depends on acw-map.txt)
├── reads customview-map.txt (from _ConvertResourcesCases / Aapt2.targets)
├── reads acw-map.txt
└── replaces managed names with ACW names in layout XMLs
What the trimmable flow should look like
Per-assembly (runs once per assembly, MSBuild Inputs/Outputs skips unchanged):
_ScanAndGenerateTypeMap(Foo.dll) → Foo.TypeMap.dll + acw-map.Foo.txt + Foo JCW .java files
_ScanAndGenerateTypeMap(Bar.dll) → Bar.TypeMap.dll + acw-map.Bar.txt + Bar JCW .java files
_ScanAndGenerateTypeMap(Mono.Android.dll) → Mono.Android.TypeMap.dll + acw-map.Mono.Android.txt + ...
Merge (runs when any individual acw-map changes):
_MergeAcwMaps → concatenate acw-map.*.txt → acw-map.txt
Shared (unchanged):
GenerateMainAndroidManifest → merged AndroidManifest.xml
GenerateAdditionalProviderSources → provider .java files
Layout rewriting (unchanged):
_ConvertCustomView → reads acw-map.txt + customview-map.txt → fixes layout XMLs
Architecture: Per-assembly scanning
Each assembly is scanned independently. MSBuild Inputs/Outputs skips scanning for assemblies whose inputs haven't changed. This is a major improvement over the legacy flow which scans all assemblies in a single monolithic pass.
Per-assembly outputs
Each assembly scan+generate step produces:
Foo.TypeMap.dll— the TypeMap assembly for types in that assemblyacw-map.Foo.txt— per-assemblyManagedName;JavaNamelines- JCW
.javafiles — Java callable wrapper source files for types in that assembly - Component info — attribute data for manifest generation (activities, services, etc.)
Combining step: _MergeAcwMaps
A separate MSBuild target merges per-assembly acw-map.*.txt files into a single acw-map.txt. This target has Inputs="@(_PerAssemblyAcwMaps)" / Outputs="$(IntermediateOutputPath)acw-map.txt" so it only runs when any individual map changes.
The acw-map file is a flat list of Key;Value lines (order doesn't matter — the consumer LoadMapFile reads them into a Dictionary). The merge is a simple file concatenation using built-in MSBuild tasks:
<Target Name="_MergeAcwMaps"
Inputs="@(_PerAssemblyAcwMaps)"
Outputs="$(_AcwMapFile)">
<ReadLinesFromFile File="%(_PerAssemblyAcwMaps.Identity)">
<Output TaskParameter="Lines" ItemName="_AcwMapLines" />
</ReadLinesFromFile>
<WriteLinesToFile File="$(_AcwMapFile)"
Lines="@(_AcwMapLines)"
Overwrite="true"
WriteOnlyWhenDifferent="true" />
</Target>Open question:
WriteLinesToFilehas aWriteOnlyWhenDifferentparameter — need to verify it works correctly for this use case (avoids unnecessary timestamp changes that would trigger downstream_ConvertCustomView). If not, fall back to the existingFiles.CopyIfStreamChangedutility in a small custom task.
Cross-assembly conflict detection (XA4214/XA4215 — two types from different assemblies mapping to the same managed or Java key) currently lives in ACWMapGenerator. This validation could be done as a separate lightweight validation step after the merge, or deferred to consumption time (LoadMapFile already silently takes the first entry for duplicates).
Additionally, this combining step:
- Collects component info from all assemblies → feeds
GenerateMainAndroidManifest - Resolves cross-assembly references (e.g.,
[Application(BackupAgent=typeof(MyBackupAgent))]whereMyBackupAgentis in a library assembly)
Incrementality and parallelization
Both incrementality and parallelization should be handled at the MSBuild level, not inside the task:
- Incrementality: MSBuild
Inputs/Outputson the per-assembly scan target. If an assembly hasn't changed, MSBuild skips the target invocation entirely. - Parallelization: MSBuild
BuildInParallel="true"on theMSBuildtask, using the same pattern as_RunAotForAllRIDsinAssemblyResolution.targets— each assembly becomes a project-to-build item withAdditionalProperties.
The scan task itself should be simple: accept a single assembly path, scan it, produce outputs. MSBuild orchestrates which assemblies to scan and whether to run them in parallel.
Open question: Need to validate with MSBuild team that per-assembly
BuildInParallelgives a net benefit given the short per-assembly scan times (<1s forMono.Android.dllwith ~12k types). Node spawning + IPC overhead might negate the parallelism. If so, a sequential batched approach withInputs/Outputsincrementality (skip unchanged assemblies) may be sufficient.
Assembly resolution during scanning
When scanning MyApp.dll, the scanner needs to resolve type references to Mono.Android.dll (e.g., to walk inheritance chains for [Register] attributes on base types). This means Mono.Android.dll gets read during MyApp.dll's scan — but the SRM MetadataReader is memory-mapped, so only pages touched are loaded. The full type enumeration + attribute parsing only happens when Mono.Android.dll is the scan target.
Cross-assembly unconditional forcing
[Application(BackupAgent=typeof(MyBackupAgent))] in MyApp.dll references MyBackupAgent which might be defined in the same assembly or a library assembly:
- Same-assembly case (common): handled by the scanner's Phase 3 during the assembly scan
- Cross-assembly case (uncommon): handled in the combining step after all scans complete
GenerateMainAndroidManifest: Cecil dependency
The current GenerateMainAndroidManifest task depends on NativeCodeGenState which provides Cecil TypeDefinition objects. The manifest generation flow:
ManifestDocument.Merge()iterates all Java types- Checks inheritance via
IsSubclassOf()(Cecil) - Reads component attributes via
FromTypeDefinition()(Cecil) - Converts to XML via
ToElement()(Cecil attribute reading)
This reads ~45+ properties from [Activity] alone, plus [IntentFilter], [MetaData], [Layout], [Property] sub-attributes. Total across all component attributes: ~160+ properties.
Decoupling approach: IManifestComponentInfo interface (Option A — no Cecil)
The scanner extracts ALL component attribute properties from SRM CustomAttribute blobs as raw name-value pairs. The manifest generator works against an interface:
interface IManifestComponentInfo {
string ManagedTypeName { get; }
string JavaName { get; }
ManifestComponentKind Kind { get; } // Activity, Service, BroadcastReceiver, ContentProvider, Application, Instrumentation
IReadOnlyDictionary<string, object?> AttributeProperties { get; }
IReadOnlyList<IIntentFilterInfo> IntentFilters { get; }
IReadOnlyList<IMetaDataInfo> MetaData { get; }
}Both Cecil path (from TypeDefinition + FromTypeDefinition()) and SRM path produce the same IManifestComponentInfo. The manifest generator works on this interface instead of Cecil types directly. The scanner doesn't need to understand the semantic meaning of properties — it passes them through as raw data.
Component attribute property counts
| Attribute | # Properties |
|---|---|
ActivityAttribute |
~46 |
ApplicationAttribute |
~40 |
IntentFilterAttribute |
~22 |
ContentProviderAttribute |
~14 |
ServiceAttribute |
~12 |
BroadcastReceiverAttribute |
~10 |
InstrumentationAttribute |
~8 |
LayoutAttribute |
~5 |
MetaDataAttribute |
~3 |
Hand-edited manifest ordering dependency
ManifestDocument.Merge() preserves manually-declared component entries in the user's AndroidManifest.xml — if an <activity android:name="..."> already exists, it doesn't generate a duplicate from C# attributes.
This creates an ordering dependency: the scanner needs to mark manifest-referenced types as unconditional, but it can only know which types are in the manifest AFTER GenerateMainAndroidManifest runs. However, the scanner also provides data that feeds manifest generation.
Resolution:
- Scanner marks attribute-based types (types with
[Activity],[Service], etc.) as unconditional — these are always in the manifest - Scanner marks
[Application]cross-references (BackupAgent, ManageSpaceActivity) as unconditional - Post-manifest pass: After
GenerateMainAndroidManifestruns, a separate step reads the merged manifest and marks any additional types referenced byandroid:nameas unconditional (these are types that appear in the manifest without C# component attributes — hand-edited entries)
This post-manifest pass runs in the MSBuild task (not the scanner) and feeds the TypeMap generator to ensure hand-edited manifest entries are not trimmed.
No disk caching
The scan + generate pipeline runs in a single MSBuild task invocation. All intermediate state stays in memory for best efficiency.
MSBuild Inputs/Outputs handles target-level incrementality — the entire target is skipped if no input assemblies changed. There is no need for per-assembly disk serialization of scan results.
(Note: Issue #10798 originally mentioned "cached to disk" — this is outdated. In-memory pipeline is the correct approach.)
Proposed targets structure for #10800
The trimmable _GenerateJavaStubs should be decomposed into separate targets for incrementality:
<!-- Trimmable.targets -->
<!-- Scan all assemblies (per-assembly incremental), generate JCWs, acw-map.txt, and TypeMap assemblies -->
<Target Name="_GenerateTrimmableTypeMap"
Inputs="@(_ResolvedAssemblies)"
Outputs="$(IntermediateOutputPath)typemap\acw-map.txt;@(_TypeMapAssemblies)">
<GenerateTrimmableTypeMap ... />
</Target>
<!-- Shared: Generate merged AndroidManifest.xml from scanner component info -->
<Target Name="_GenerateAndroidManifest"
DependsOnTargets="_GenerateTrimmableTypeMap"
Inputs="$(IntermediateOutputPath)typemap\component-info.cache;$(AndroidManifest)"
Outputs="$(IntermediateOutputPath)android\AndroidManifest.xml">
<GenerateMainAndroidManifest ... />
</Target>
<!-- Post-manifest: Mark hand-edited manifest entries as unconditional -->
<Target Name="_RootManifestReferencedTypes"
DependsOnTargets="_GenerateAndroidManifest"
Inputs="$(IntermediateOutputPath)android\AndroidManifest.xml"
Outputs="$(IntermediateOutputPath)typemap\rooted-types.cache">
<!-- Read merged manifest, find types not already marked unconditional, update TypeMap -->
</Target>
<!-- Shared: Generate additional provider sources -->
<Target Name="_GenerateProviderSources"
DependsOnTargets="_GenerateAndroidManifest">
<GenerateAdditionalProviderSources ... />
</Target>This achieves:
- Incremental builds: Only rescan when assemblies change
- Manifest generation: Reuses existing shared task
- ACW map: Generated from scanner results, consumed by
_ConvertCustomViewas before - Custom view fixup:
_ConvertCustomViewin Common.targets works unchanged - Hand-edited manifest support: Post-manifest pass ensures no type is incorrectly trimmed
External type rooting
The scanner also needs to mark types as unconditional (not trimmable) based on external sources:
| Source | File | Format | Available when? |
|---|---|---|---|
| Layout XML custom views | customview-map.txt |
ManagedTypeName;ResourceFilePath |
✅ Available — generated by _ConvertResourcesCases (Aapt2.targets, runs for all paths) |
| Manifest components | merged AndroidManifest.xml |
android:name attrs with Java class names |
Available after GenerateMainAndroidManifest runs |
[Application] attributes |
Assembly metadata | BackupAgent, ManageSpaceActivity type properties |
✅ Available — already extracted by JavaPeerScanner |
Relationship to #10779
Issue #10779 (build targets refactoring) should extract GenerateMainAndroidManifest and GenerateAdditionalProviderSources from the LLVM IR mega-target into separate, shared targets that both legacy and trimmable paths can use. The current PR1 implementation keeps them inside the legacy _GenerateJavaStubs — they need to be pulled out.
Definition of Done
-
GenerateMainAndroidManifestruns in the trimmable path (extracted from mega-target in [TrimmableTypeMap] Separate typemap build targets into dedicated.targetsfiles #10779) -
GenerateAdditionalProviderSourcesruns in the trimmable path -
acw-map.txtis generated fromJavaPeerInfoscanner results -
_ConvertCustomViewworks correctly with the trimmable-generatedacw-map.txt - Custom views in layout XMLs are correctly fixed up with ACW names
- Hand-edited manifest entries are preserved
- Types referenced in
customview-map.txtare marked unconditional - Types referenced in merged manifest are marked unconditional (post-manifest pass)
- Per-assembly incremental builds: unchanged assemblies are not rescanned
- Cross-assembly unconditional forcing works in combining step