[TrimmableTypeMap] Add CoreCLRTrimmable Mono.Android.NET-Tests lane#11091
[TrimmableTypeMap] Add CoreCLRTrimmable Mono.Android.NET-Tests lane#11091simonrozsival wants to merge 26 commits intomainfrom
Conversation
f8106b0 to
a437455
Compare
d5aefaa to
3a8d509
Compare
1814a6d to
d0e30f6
Compare
17b123b to
e2c70fd
Compare
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
e2c70fd to
87c531f
Compare
There was a problem hiding this comment.
Pull request overview
Adds CI/test-lane plumbing to run Mono.Android.NET-Tests on CoreCLR with the trimmable typemap implementation, keeping currently-unsupported test buckets explicitly excluded to maintain green coverage while follow-up runtime/generator work lands separately.
Changes:
- Adds a new
CoreCLRTrimmableAPK instrumentation lane and pins the existingCoreCLRlane tollvm-ir. - Updates
Mono.Android.NET-Tests.csprojto default to CoreCLR + exclude trimmable-specific buckets when_AndroidTypeMapImplementation=trimmable. - Centralizes additional trimmable-mode runtime test exclusions in
NUnitInstrumentation, and adjusts a few Java.Interop tests/projects to avoid unsupported coverage.
Reviewed changes
Copilot reviewed 5 out of 5 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
| tests/Mono.Android-Tests/Mono.Android-Tests/Xamarin.Android.RuntimeTests/NUnitInstrumentation.cs | Adds runtime-switch-driven exclusions for categories and specific Java.Interop test names under trimmable typemap. |
| tests/Mono.Android-Tests/Mono.Android-Tests/Mono.Android.NET-Tests.csproj | Sets defaults/exclusions for trimmable typemap runs (incl. CoreCLRTrimmable flavor). |
| tests/Mono.Android-Tests/Mono.Android-Tests/Java.Interop/JnienvTest.cs | Marks specific tests as TrimmableIgnore. |
| tests/Mono.Android-Tests/Mono.Android-Tests/Java.Interop/JavaObjectExtensionsTests.cs | Marks a specific test as TrimmableIgnore (and normalizes file header). |
| tests/Mono.Android-Tests/Java.Interop-Tests/Java.Interop-Tests.NET.csproj | Removes JniTypeUtf8Test.cs from compilation due to unsupported APIs in the product assembly. |
| build-tools/automation/yaml-templates/stage-package-tests.yaml | Updates CoreCLR lane args and adds the new CoreCLRTrimmable lane. |
simonrozsival
left a comment
There was a problem hiding this comment.
🤖 AI Review Summary
Verdict: ✅ LGTM
Found 1 suggestion:
- 💡 Code organization — the trimmable category filtering is now split between
Mono.Android.NET-Tests.csprojandNUnitInstrumentation.cs(NUnitInstrumentation.cs:29). Keeping one source of truth would make the temporary exclusions easier to retire.
The lane wiring itself looks good, and I like that the unsupported cases stay explicit instead of being hidden behind runtime fallback behavior.
Review generated by android-reviewer from review guidelines.
02780bb to
c3a7ebc
Compare
619991d to
6a0644f
Compare
19ec2fa to
e084238
Compare
…ge name The legacy ManifestDocument automatically sets targetPackage to the app's PackageName when [Instrumentation] doesn't specify it. Without this, the generated manifest has <instrumentation> without android:targetPackage, causing INSTALL_PARSE_FAILED_MANIFEST_MALFORMED on the device. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…on mode switch
The CoreCLRTrimmable CI test was crashing with:
ClassNotFoundException: Didn't find class "android.apptests.App"
Root cause: when switching from the default (LLVM IR) typemap build to the
trimmable path in the same intermediate directory (sequential CI test runs),
_GenerateTrimmableTypeMap was incorrectly skipped because its output DLL
existed from a prior run. The stale LLVM IR manifest (which uses compat JNI
names like "android.apptests.App") remained while the JCW was generated with
the CRC-based name ("crc64.../App"), causing a name mismatch at runtime.
Fixes:
1. Add a sentinel file (.trimmable) written when _GenerateTrimmableTypeMap
runs. A new target _CleanStaleNonTrimmableState deletes the stale
typemap DLL when the sentinel is missing, forcing regeneration.
2. Pass extraBuildArgs to the Clean step in apk-instrumentation.yaml so
the Clean imports the same targets as the build and properly cleans
trimmable-specific files.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…est generation The _GenerateTrimmableTypeMap target runs AfterTargets="CoreCompile" which executes during per-RID inner builds where IntermediateOutputPath includes the RID subdirectory (e.g. obj/.../android-arm64/). The generated manifest was written there, but _ManifestMerger and _GenerateJavaStubs (packaging phase) read from the outer IntermediateOutputPath (obj/.../net11.0-android/). This caused the stale LLVM IR manifest (with compat JNI names like "android.apptests.App") to be used instead of the trimmable manifest (with CRC-based JCW names), resulting in ClassNotFoundException at runtime. Fix: prefer _OuterIntermediateOutputPath (set by the outer build for inner per-RID builds) so typemap outputs land where the packaging phase expects. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…RC names Manifest templates may hardcode compat JNI names (e.g., android.apptests.App) but the trimmable JCW generator uses CRC-based names (e.g., crc64.../App). The compat name rewrite must happen before collecting existingTypes so the duplicate check works correctly and we don't end up with both versions. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…pe crash Two fixes for the CoreCLRTrimmable test lane: 1. Split TrimmableTypeMap.Initialize() into CreateInstance() + RegisterNativeMethods(). CreateInstance() runs BEFORE JniRuntime creation (so ManagedPeer..cctor() can resolve types via TrimmableTypeMap.Instance). RegisterNativeMethods() runs AFTER (because it needs JNI). This fixes a chicken-and-egg crash where ManagedPeer's static constructor triggered type resolution before the typemap was initialized. 2. Catch TypeLoadException in OnRegisterNatives for open generic definitions (e.g., TestInstrumentation`1). These types have JCW classes but can't be loaded via Type.GetType() as open generics. Registration is skipped — their native methods will be registered when the concrete derived class loads. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
6da7dae to
9082b24
Compare
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Two fixes for open generic definitions (e.g., TestInstrumentation<T>): 1. Use Java.Lang.Object as the JavaPeerProxy<T> type parameter instead of the open generic type. Loading the proxy type forces the CLR to resolve its base class generic argument, and open generics from external assemblies can't be resolved by the TypeMapLazyDictionary loader. The T parameter only affects CreateInstance, which already throws for generic definitions. 2. Skip TypeMapAssociation emission for generic definitions. The association attribute references typeof(TestInstrumentation<>) which triggers the same cross-assembly open generic resolution failure when the runtime scans assembly-level attributes. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…ter runtime creation The two-phase split (CreateInstance before runtime, RegisterNativeMethods after) was a workaround for a Debug-only issue (#if DEBUG in JniPeerMembers). But it caused OnRegisterNatives to never be called — the JNI RegisterNatives succeeded but was ineffective. Revert to the working pattern: single Initialize() after JniRuntime.SetCurrent(), matching the proven java-interop-proxies branch where 758 tests passed. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Use the string overload of JniType which resolves via Class.forName with the runtime's app ClassLoader. The UTF-8 span overload (ReadOnlySpan<byte>) uses raw JNI FindClass which resolves via the system ClassLoader — returning a different Runtime class instance than the one JCWs reference. This caused OnRegisterNatives to never be called because the native method was registered on the wrong class. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…n subtypes
Two fixes for the Instrumentation class loading order:
1. Export Runtime.registerNatives as a JNI native method from C++ so it's
available via JNI auto-discovery when libmonodroid.so is loaded. Without
this, calling registerNatives before the managed runtime initializes
causes UnsatisfiedLinkError. The C++ stub is a no-op fallback — once the
managed runtime starts, TrimmableTypeMap.Initialize() re-registers the
method with the managed OnRegisterNatives callback.
2. Propagate CannotRegisterInStaticConstructor to all descendants of
Application and Instrumentation types. Android loads Instrumentation
subclasses before the native library, so ALL types in the hierarchy
must use the lazy __md_registerNatives pattern instead of static {}.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…tion Three fixes: 1. Fix _RemoveRegisterAttribute override to reset _ShrunkAssemblies to _ResolvedAssemblies. When PublishTrimmed=true, ProcessAssemblies points shrunk items to R2R/shrunk/ which is only created by the (now no-op) _RemoveRegisterAttribute. Without this fix, CompressAssemblies and CreateAssemblyStore fail with DirectoryNotFoundException. 2. Exclude DoNotGenerateAcw types from ApplicationRegistration to prevent referencing deprecated android.test framework types. 3. Export Runtime.registerNatives from C++ as a JNI native method and propagate CannotRegisterInStaticConstructor to all Instrumentation/ Application descendants. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Instead of trying to override the JNI-registered registerNatives from managed code (which doesn't work due to ClassLoader identity issues), use the same function pointer pattern as registerJniNativesFn: 1. Add registerNativesFn to JnienvInitializeArgs struct 2. Set it to OnRegisterNatives during Initialize 3. C++ stub calls through the function pointer when set This ensures the correct class identity is maintained because the C++ stub receives the exact jclass from the JVM, and passes it directly to the managed callback. The managed RegisterNatives JNI call is removed since the C++ function pointer handles the routing. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…of FindClass OnRegisterNatives was creating a new JniType from the class name which resolved via FindClass to potentially a different class instance. Use the nativeClassHandle passed from C++ (the actual jclass from the JVM) instead. Also remove the unused RegisterNatives method that tried JNI RegisterNatives from managed code (replaced by C++ function pointer routing). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…pes in ApplicationRegistration Abstract types like TestInstrumentation<T> also need their native methods registered via ApplicationRegistration.registerApplications(). Without this, the lazy __md_registerNatives pattern fails because the native methods declared on the abstract base class (n_OnCreate, n_OnStart) are never registered. Note: this alone doesn't fix the Instrumentation lifecycle ordering issue — Instrumentation.onCreate() runs before Application.onCreate(), so the native methods are needed before ApplicationRegistration runs. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…naged runtime Classes that call Runtime.registerNatives() before the managed runtime is initialized (e.g., Instrumentation subclasses loaded during handleBindApplication) are now queued in C++ and replayed after JNIEnvInit.Initialize sets the managed callback. This fixes the UnsatisfiedLinkError for n_OnCreate on abstract Instrumentation base classes. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…x OnRegisterNatives The root cause of all the UnsatisfiedLinkError crashes: the proxy types were missing the self-application [JavaPeerProxy] custom attribute. Without it, type.GetCustomAttribute<JavaPeerProxy>() returns null, so RegisterNatives is never called for any JCW class. Added metadata.AddCustomAttribute(typeDefHandle, selfAttrCtorRef, blob) to emit the self-application attribute on each proxy type definition. This enables the AOT-safe type resolution pattern where GetCustomAttribute instantiates the proxy type as the attribute value. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
These 22 tests require alias/interface resolution and generic container
factory support in the trimmable typemap, which will be addressed in a
follow-up PR.
- JavaObjectExtensionsTests: JavaCast_BadInterfaceCast,
JavaCast_InvalidTypeCastThrows, JavaCast_CheckForManagedSubclasses,
JavaAs → [Category("TrimmableIgnore")]
- JavaPeerableExtensionsTests: JavaAs, JavaAs_Exceptions,
JavaAs_InstanceThatDoesNotImplementInterfaceReturnsNull →
ExcludedTestNames (submodule tests)
- JavaObjectArray_object_ContractTest (16 tests) →
ExcludedTestNames (submodule tests)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
During the CoreCLRTrimmable investigation, several speculative changes were added that turned out not to be needed once the real root cause (missing self-application attribute on generated JavaPeerProxy types) was fixed: - src/native/: revert the 'registerNativesFn' callback, deferred-class queue, and JNI export symbol. Verified via unconditional logging during a full device test run that Java_mono_android_Runtime_registerNatives is never called when the managed TrimmableTypeMap.OnRegisterNatives handler is wired via JniEnvironment.Types.RegisterNatives. - src/Mono.Android/Android.Runtime/JNIEnvInit.cs: drop the now-unused registerNativesFn assignment. - src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMap.cs: restore the 'u8' optimized JNI type name literal 'mono/android/Runtime'u8. All 794 CoreCLRTrimmable device tests still pass. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
e02ec3c to
4abdc1e
Compare
|
Splitting this PR into focused, reviewable pieces so each fix gets the attention it deserves. This PR will stay open and become a thin, clean addition to CI configuration once all the children land on Child PRs
Once the children merge, this PR will be rebased to include only:
Follow-ups (issues)
|
Summary
Add a new
CoreCLRTrimmableMono.Android.NET-Testslane to CI and fix the issues uncovered by running it.CI lane plumbing
Mono.Android.NET_Tests-CoreCLRTrimmableinstage-package-tests.yamlCoreCLRlane on LLVM IR typemapextraBuildArgsto the Clean stepSDK packaging fixes
PreserveLists/**glob toMicrosoft.Android.Sdk.projforTrimmable.CoreCLR.xmlLink=..\PreserveLists\...inILLink.csprojTrimmableTypeMap_PreserveList_IsPackagedInSdkregression testManifest generation fixes
<instrumentation android:targetPackage>to app package name_FixRootAssemblytarget (Android apps have noMain())Generic type fixes
JavaPeerProxy<Java.Lang.Object>for open generic definitions (preventsTypeLoadExceptionwhen loading the proxy type)TypeMapAssociationemission for generic definitions (prevents cross-assembly open generic resolution failure)Runtime initialization
TrimmableTypeMap.Initialize()call afterJniRuntime.SetCurrent()(matching the proven pattern from prior trimmable branches)Typemap output path + stale artifact handling
_OuterIntermediateOutputPathfor typemap outputsTest exclusions
Export,NativeTypeMap,SSL,TrimmableIgnorecategoriesJava.Interoptest fixtures without JCW classes