[TrimmableTypeMap] Add GenerateTrimmableTypeMap MSBuild task and targets#10924
[TrimmableTypeMap] Add GenerateTrimmableTypeMap MSBuild task and targets#10924simonrozsival wants to merge 23 commits intomainfrom
Conversation
d28814e to
04e0b2e
Compare
2d666a1 to
5bf8492
Compare
simonrozsival
left a comment
There was a problem hiding this comment.
🤖 AI Review Summary
Verdict: ✅ LGTM
Found 0 issues.
Reviewed against all 14 rule categories:
- ✅ MSBuild tasks: Extends
AndroidTask,TaskPrefixdefined, returns!Log.HasLoggedErrors,[Required]properties have defaults,[Output]properties nullable - ✅ MSBuild targets: Internal names prefixed with
_,Inputs/Outputswith stamp file for incrementality,AfterTargetsused appropriately (noDependsOnavailable), noTrimmerRootAssembly(correct — trimmer must process TypeMapAttributes) - ✅ Nullable: No null-forgiving
!, no#nullable enable(project-level) - ✅ Error handling:
ParseTargetFrameworkVersionthrows on bad input, no empty catch blocks - ✅ Performance: BCL assemblies filtered via
FrameworkAssembly/HasMonoAndroidReferencemetadata, per-assembly timestamp check skips unchanged typemaps, LINQ used cleanly - ✅ Code organization: Types made
publicfor cross-assembly consumption (build-time only, not shipped in apps),UsingTaskscoped to trimmable targets only - ✅ Formatting: Tabs, Mono style throughout
👍 Clean task design with good incrementality (per-assembly timestamp skipping). Benchmark data in the PR description validates the approach. RuntimeHostConfigurationOption correctly shared between CoreCLR and NativeAOT. AfterTargets usage is justified since there's no DependsOn property to hook into.
Review generated by android-reviewer from review guidelines.
81102b5 to
791bd4d
Compare
simonrozsival
left a comment
There was a problem hiding this comment.
🤖 AI Review Summary
Verdict:
Found 4 issues (0 errors, 2 warnings, 2 suggestions).
⚠️ MSBuild targets: Generated typemap assemblies and Java files under$(IntermediateOutputPath)typemap\are not registered in@(FileWrites).IncrementalCleanmay delete them while the stamp file remains, breaking subsequent builds. (Trimmable.targets:47)⚠️ MSBuild targets: Item group empty check'@(_GeneratedTypeMapAssemblies)' != ''should use->Count()to avoid joining all paths into a string. (CoreCLR.targets:10,NativeAOT.targets:10)- 💡 API design: Several types were changed from
internaltopublicbut are only consumed by theGenerateTrimmableTypeMapMSBuild task. Consider adding[InternalsVisibleTo]forXamarin.Android.Build.Tasksinstead to keep the API surface minimal. (JavaPeerScanner.cs:17,JavaPeerInfo.cs:11, and 5 more files) - 💡 Nullable:
GenerateTrimmableTypeMap.csuses nullable annotations (ITaskItem[]?) but lacks#nullable enable— nullable flow analysis isn't active for this file. Other new task files in this project use per-file#nullable enable. (GenerateTrimmableTypeMap.cs:1)
👍 Positive callouts:
- Clean
AndroidTaskimplementation with correctreturn !Log.HasLoggedErrorspattern and proper[Required]/[Output]property declarations. - Good per-assembly incremental check via
IsUpToDatetimestamp comparison. - Thorough test coverage including empty inputs, real assembly scanning, incrementality, touched-source regeneration, and TFV parsing.
- The
FileWritespattern fromLlvmIr.targets(the existing typemap path) is a good reference for how to fix the FileWrites concern.
CI status: Pending — strong-name signing fix was just pushed. Previous run failed with CS8002 (unsigned assembly).
Review generated by android-reviewer from review guidelines.
...id.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.TypeMap.Trimmable.targets
Show resolved
Hide resolved
....Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.TypeMap.Trimmable.CoreCLR.targets
Outdated
Show resolved
Hide resolved
| /// Phase 2: Analyze types using cached indices | ||
| /// </summary> | ||
| sealed class JavaPeerScanner : IDisposable | ||
| public sealed class JavaPeerScanner : IDisposable |
There was a problem hiding this comment.
🤖 💡 API design — This type (and JavaPeerInfo, MarshalMethodInfo, JniParameterInfo, JavaConstructorInfo, ActivationCtorInfo, ActivationCtorStyle, JcwJavaSourceGenerator, RootTypeMapAssemblyGenerator, TypeMapAssemblyGenerator) were changed from internal to public. These are only consumed by the GenerateTrimmableTypeMap MSBuild task in Xamarin.Android.Build.Tasks. Making them public exposes them as API surface that customers could depend on.
Consider keeping them internal and adding [InternalsVisibleTo("Xamarin.Android.Build.Tasks")] in the TrimmableTypeMap csproj instead. This is consistent with the existing InternalsVisibleTo entries for test projects.
Rule: New helpers default to internal (Code Organization #4)
There was a problem hiding this comment.
Intentionally keeping these public. The benchmark script in the PR description consumes them directly, and they'll also be useful for the pre-generation step in #10760 which runs outside of Xamarin.Android.Build.Tasks. This assembly isn't part of the public SDK surface shipped to customers — it's an internal build-time dependency.
src/Xamarin.Android.Build.Tasks/Tasks/GenerateTrimmableTypeMap.cs
Outdated
Show resolved
Hide resolved
| static bool IsUpToDate (string outputPath, string assemblyName, Dictionary<string, string> sourcePathByName) | ||
| { | ||
| if (!File.Exists (outputPath)) { | ||
| return false; | ||
| } | ||
| if (!sourcePathByName.TryGetValue (assemblyName, out var sourcePath)) { | ||
| return false; | ||
| } | ||
| return File.GetLastWriteTimeUtc (outputPath) >= File.GetLastWriteTimeUtc (sourcePath); | ||
| } |
There was a problem hiding this comment.
I think this is ok for a first pass, but I think we could let MSBuild natively do the timestamp checking & run the target partially.
If you had:
<Target
Inputs="a.in;b.in;c.in"
Outputs="a.out;b.out;c.out"
<!-- ... -->
<MyTask InFiles="a.in;b.in;c.in" />If only b.in changes during an incremental build, MSBuild can run the target "partially", and it would automatically only pass in b.in to the task.
The Inputs/Outputs need to be 1-1, and then you can have additional Inputs files at the end of the list -- so you'd have to make sure the files coming are ordered that way.
If we did this, we'd get rid of _GenerateJavaStubs.stamp.
There is a AssertTargetIsPartiallyBuilt, we have in a few tests.
There was a problem hiding this comment.
Agreed — MSBuild partial builds with 1-1 Inputs/Outputs would be the right approach here. We iterated on the incremental build story during this PR and it was getting out of hand, so I'd like to tackle it in a dedicated follow-up PR once the trimmable typemap path is functionally complete. The current timestamp check inside the task is a reasonable first pass.
src/Xamarin.Android.Build.Tasks/Tasks/GenerateTrimmableTypeMap.cs
Outdated
Show resolved
Hide resolved
src/Xamarin.Android.Build.Tasks/Tasks/GenerateTrimmableTypeMap.cs
Outdated
Show resolved
Hide resolved
Xamarin.Android-PR CI Failure Summary (build 13546162)1.
|
...Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Tasks/GenerateTrimmableTypeMapTests.cs
Show resolved
Hide resolved
| ITaskItem [] GenerateJcwJavaSources (List<JavaPeerInfo> allPeers) | ||
| { | ||
| var jcwGenerator = new JcwJavaSourceGenerator (); | ||
| var files = jcwGenerator.Generate (allPeers, JavaSourceOutputDirectory); |
There was a problem hiding this comment.
Is it worth seeing how/if having the JcwJavaSourceGenerator generate source for each type in parallel affects the build times? From what I can tell from #10917, each generation should be pretty independent.
There was a problem hiding this comment.
Same question for scanning.
There was a problem hiding this comment.
I will open an issue so we revisit this later and improve build time if possible.
There was a problem hiding this comment.
Filed #10958 to track this and other build-time optimization opportunities.
Add the MSBuild task that wires the TrimmableTypeMap scanner and generators into the build pipeline, replacing the stub _GenerateJavaStubs target. ### Task (GenerateTrimmableTypeMap) - Extends AndroidTask, TaskPrefix 'GTT' - Scans resolved assemblies for Java peer types - Generates per-assembly TypeMap .dll assemblies - Generates root _Microsoft.Android.TypeMaps.dll - Generates JCW .java source files for ACW types ### Targets - Microsoft.Android.Sdk.TypeMap.Trimmable.targets: replaces stub with real GenerateTrimmableTypeMap task call - CoreCLR.targets: adds generated assemblies as TrimmerRootAssembly, configures RuntimeHostConfigurationOption for TypeMappingEntryAssembly - NativeAOT.targets: adds to IlcReference, UnmanagedEntryPointsAssembly, and TrimmerRootAssembly Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Tests using MockBuildEngine: - Empty assembly list succeeds with no outputs - Real Mono.Android.dll produces per-assembly + root typemap assemblies - Different TargetFrameworkVersion formats all parse correctly Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Full build integration tests: - Build with _AndroidTypeMapImplementation=trimmable succeeds on CoreCLR - Incremental build skips _GenerateJavaStubs when nothing changed Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…n bad version - Extract Phase 1-5 into named methods (ScanAssemblies, GenerateTypeMapAssemblies, GenerateJcwJavaSources) — no more // Phase N comments - Filter BCL assemblies: skip FrameworkAssembly=true unless HasMonoAndroidReference - Throw on unparseable TargetFrameworkVersion instead of silent fallback - Use LINQ for grouping peers by assembly and filtering paths - Deterministic output ordering via OrderBy on assembly name Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Both GenerateTypeMapAssemblies and GenerateJcwJavaSources now return ITaskItem[] directly — no intermediate conversion in RunTask - Move Java output under typemap dir (typemap/java instead of android/src) - Remove TrimmerRootAssembly from generated assemblies — the trimmer must process TypeMapAttributes and trim entries whose trimTarget types were removed. TrimmerRootAssembly would prevent this, defeating the purpose. - NativeAOT: keep IlcReference + UnmanagedEntryPointsAssembly but remove TrimmerRootAssembly for the same reason. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Compare source assembly timestamp against generated typemap .dll — skip emission when the output is newer. Root assembly only regenerated when any per-assembly typemap changed. Typical incremental build: only app assembly changed → scan all (for cross-assembly resolution) but only emit _MyApp.TypeMap.dll + root. Mono.Android scan is unavoidable (xref resolution) but its typemap emission is skipped. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- SecondRun_SkipsUpToDateAssemblies: run twice with same inputs, verify typemap file timestamp unchanged and 'up to date' logged - SourceTouched_RegeneratesOnlyChangedAssembly: touch source assembly, verify typemap is regenerated with newer timestamp - InvalidTargetFrameworkVersion_Throws: verify ArgumentException - Extracted CreateTask helper to reduce test boilerplate - ParsesTargetFrameworkVersion converted to [TestCase] parameterized test Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Both CoreCLR and NativeAOT need to know the TypeMap entry assembly. RuntimeHostConfigurationOption works for both (runtimeconfig.json for CoreCLR, ILC feature switch for NativeAOT). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Make types consumed by the MSBuild task public (they're build-time only, not shipped in apps): JavaPeerInfo, MarshalMethodInfo, JniParameterInfo, JavaConstructorInfo, ActivationCtorInfo, ActivationCtorStyle, JavaPeerScanner, TypeMapAssemblyGenerator, RootTypeMapAssemblyGenerator, JcwJavaSourceGenerator. Fix Execute_InvalidTargetFrameworkVersion test: AndroidTask catches exceptions and logs them as errors, so Assert.Throws doesn't work. Check task.Execute() returns false and errors are logged instead. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Generate Java content to StringWriter first, compare with existing file. Only write to disk if content changed. This avoids unnecessary javac recompilation on incremental builds where types haven't changed. Benchmark showed JCW file writing was the biggest bottleneck (~511ms p50 for 315 files). With this change, incremental builds that don't change any types skip all disk writes. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…builds" This reverts commit ac4227b.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The Xamarin.Android.Build.Tasks project is strong-name signed and references Microsoft.Android.Sdk.TrimmableTypeMap, which was not signed. This caused CS8002 on all CI platforms. Add SignAssembly + AssemblyOriginatorKeyFile to both the library and its unit test project, and add the public key to the InternalsVisibleTo entry. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Add @(FileWrites) for generated typemap assemblies and Java files to prevent IncrementalClean from deleting them. - Use ->Count() instead of != '' for item group empty checks in CoreCLR and NativeAOT targets. - Add #nullable enable to GenerateTrimmableTypeMap.cs. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Exercises the early-return path in RunTask() when the scanner finds no [Register] types in the provided assemblies. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…ectory Remove hardcoded Microsoft.Android.Ref.35 path that would break when the API level changes. MonoAndroidFrameworkDirectory already resolves the correct version dynamically via XABuildConfig. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Leave GeneratedAssemblies/GeneratedJavaFiles null when no peers found instead of assigning empty arrays (review: jonathanpeppers) - Use MonoAndroidHelper.IsMonoAndroidAssembly() instead of custom FrameworkAssembly/HasMonoAndroidReference logic (review: jonathanpeppers) - Replace LINQ Select+ToArray with simple loop in GenerateJcwJavaSources to avoid unnecessary ITaskItem cloning (review: jonathanpeppers) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The bare filename 'Xamarin.Android.Build.Tasks.dll' doesn't resolve at runtime. All other shipped UsingTask elements use the $(_XamarinAndroidBuildTasksAssembly) property which contains the full path to the task assembly. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Two fixes for CI test failures: 1. Integration tests (BuildWithTrimmableTypeMap*): Add Microsoft.Android.Sdk.TrimmableTypeMap.dll/pdb to _MSBuildFiles in create-installers.targets so the dependency is packaged into the SDK tools directory. Without this, GenerateTrimmableTypeMap task fails at runtime with a missing assembly error. 2. Unit tests (Execute*): FindMonoAndroidDll now returns ITaskItem with HasMonoAndroidReference=True metadata. The review change in 64725e6 switched GetJavaInteropAssemblyPaths to use MonoAndroidHelper.IsMonoAndroidAssembly which requires metadata on TaskItems. Bare TaskItem(path) was rejected, causing 0 assemblies to be scanned. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
e2c8868 to
3e0d62f
Compare
JavaFieldInfo is exposed through the public JavaPeerInfo.JavaFields property but was not itself marked public, causing CS0050 accessibility errors when building Xamarin.Android.Build.Tasks which references the assembly. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…ults Part of #10807. Stacked on #10924. The trimmable typemap path needs acw-map.txt for _ConvertCustomView to fix up custom view names in layout XMLs. This adds acw-map generation as a side-output of GenerateTrimmableTypeMap — the task already has all JavaPeerInfo records from scanning, so no extra scan is needed. Changes: - AcwMapWriter: converts JavaPeerInfo → acw-map.txt format (3 lines per type: partial-asm-qualified, managed, compat-jni — matching legacy format) - GenerateTrimmableTypeMap: new AcwMapDirectory input + PerAssemblyAcwMapFiles output, writes per-assembly acw-map.{AssemblyName}.txt during generation - Trimmable.targets: _MergeAcwMaps target concatenates per-assembly files into the single acw-map.txt consumed by _ConvertCustomView and R8 - 8 unit tests for AcwMapWriter (263 total tests pass) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…ults Part of #10807. Stacked on #10924. The trimmable typemap path needs acw-map.txt for _ConvertCustomView to fix up custom view names in layout XMLs. This adds acw-map generation as a side-output of GenerateTrimmableTypeMap — the task already has all JavaPeerInfo records from scanning, so no extra scan is needed. Changes: - AcwMapWriter: converts JavaPeerInfo → acw-map.txt format (3 lines per type: partial-asm-qualified, managed, compat-jni — matching legacy format) - GenerateTrimmableTypeMap: new AcwMapDirectory input + PerAssemblyAcwMapFiles output, writes per-assembly acw-map.{AssemblyName}.txt during generation - Trimmable.targets: _MergeAcwMaps target concatenates per-assembly files into the single acw-map.txt consumed by _ConvertCustomView and R8 - 8 unit tests for AcwMapWriter (263 total tests pass) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…ults Part of #10807. Stacked on #10924. The trimmable typemap path needs acw-map.txt for _ConvertCustomView to fix up custom view names in layout XMLs. This adds acw-map generation as a side-output of GenerateTrimmableTypeMap — the task already has all JavaPeerInfo records from scanning, so no extra scan is needed. Changes: - AcwMapWriter: converts JavaPeerInfo → acw-map.txt format (3 lines per type: partial-asm-qualified, managed, compat-jni — matching legacy format) - GenerateTrimmableTypeMap: new AcwMapDirectory input + PerAssemblyAcwMapFiles output, writes per-assembly acw-map.{AssemblyName}.txt during generation - Trimmable.targets: _MergeAcwMaps target concatenates per-assembly files into the single acw-map.txt consumed by _ConvertCustomView and R8 - 8 unit tests for AcwMapWriter (263 total tests pass) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Full Build,SignAndroidPackage cannot succeed yet because manifest generation (GenerateMainAndroidManifest) is not implemented for the trimmable typemap path. Scope tests to _GenerateJavaStubs target which validates the typemap + JCW generation that is implemented. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Part of #10800
Stacked on #10917.
Summary
Adds the
GenerateTrimmableTypeMapMSBuild task and wires it into the trimmable targets files, replacing the stub_GenerateJavaStubstarget with real typemap + JCW generation.Visibility & signing changes
JavaPeerScanner,JavaPeerInfo,JcwJavaSourceGenerator,TypeMapAssemblyGenerator,RootTypeMapAssemblyGenerator, etc.)publicso they can be consumed by the MSBuild task inXamarin.Android.Build.Tasks.product.snk) toMicrosoft.Android.Sdk.TrimmableTypeMapand its unit test project — required becauseXamarin.Android.Build.Tasksis strong-name signed.Task (
GenerateTrimmableTypeMap)AndroidTask(TaskPrefixGTT).dllassemblies → generates root_Microsoft.Android.TypeMaps.dll→ generates JCW.javasource filesFrameworkAssembly=truewithoutHasMonoAndroidReference) to skip unnecessary scanning.TypeMap.dllis newer than source.dllTargets
Microsoft.Android.Sdk.TypeMap.Trimmable.targets: Replaces stub with realGenerateTrimmableTypeMaptask call. Defines_TypeMapOutputDirectoryand_TypeMapJavaOutputDirectory(co-located undertypemap/). ConfiguresRuntimeHostConfigurationOptionforTypeMappingEntryAssembly.Trimmable.CoreCLR.targets: Adds generated assemblies to_ResolvedAssembliesfor the linker (withoutTrimmerRootAssembly— the trimmer must processTypeMapAttributeentries and trim ones whose target types were removed).Trimmable.NativeAOT.targets: Adds toIlcReference+UnmanagedEntryPointsAssembly(withoutTrimmerRootAssembly).Performance
Benchmark on MacBook M1, scanning
Mono.Android.dll(~8870 types), 169 iterations over 5 minutes:Cold build cost is dominated by JCW file I/O (high p95 variance from disk contention). Incremental builds (unchanged assemblies) skip emit entirely — only the scan runs (~215ms).
Note: scanning currently rescans all assemblies on every build (needed for cross-assembly type resolution). This is a known limitation — future optimization could cache scan results or add a two-list scan overload that only produces
JavaPeerInfofor changed assemblies while still indexing all for cross-references. Profiling is needed before optimizing.Benchmark script
Tests
Unit tests (8 tests with
MockBuildEngine):Integration tests (full
dotnet build):_AndroidTypeMapImplementation=trimmableon CoreCLR succeeds_GenerateJavaStubsFollow-up work
Scanoverload or cached indices)_Mono.Android.TypeMap.dllduring SDK build ([CoreCLR] Build R2R images for SDK assemblies in Debug builds #10760)