Skip to content

[TrimmableTypeMap] Add CoreCLRTrimmable Mono.Android.NET-Tests lane#11091

Draft
simonrozsival wants to merge 26 commits intomainfrom
dev/simonrozsival/trimmable-test-plumbing
Draft

[TrimmableTypeMap] Add CoreCLRTrimmable Mono.Android.NET-Tests lane#11091
simonrozsival wants to merge 26 commits intomainfrom
dev/simonrozsival/trimmable-test-plumbing

Conversation

@simonrozsival
Copy link
Copy Markdown
Member

@simonrozsival simonrozsival commented Apr 8, 2026

Summary

Add a new CoreCLRTrimmable Mono.Android.NET-Tests lane to CI and fix the issues uncovered by running it.

CI lane plumbing

  • Add Mono.Android.NET_Tests-CoreCLRTrimmable in stage-package-tests.yaml
  • Keep existing CoreCLR lane on LLVM IR typemap
  • Pass extraBuildArgs to the Clean step

SDK packaging fixes

  • Add PreserveLists/** glob to Microsoft.Android.Sdk.proj for Trimmable.CoreCLR.xml
  • Restore correct Link=..\PreserveLists\... in ILLink.csproj
  • Add TrimmableTypeMap_PreserveList_IsPackagedInSdk regression test

Manifest generation fixes

  • Default <instrumentation android:targetPackage> to app package name
  • Add _FixRootAssembly target (Android apps have no Main())
  • Rewrite compat JNI names in manifest templates to CRC names

Generic type fixes

  • Use JavaPeerProxy<Java.Lang.Object> for open generic definitions (prevents TypeLoadException when loading the proxy type)
  • Skip TypeMapAssociation emission for generic definitions (prevents cross-assembly open generic resolution failure)

Runtime initialization

  • Single TrimmableTypeMap.Initialize() call after JniRuntime.SetCurrent() (matching the proven pattern from prior trimmable branches)

Typemap output path + stale artifact handling

  • Use _OuterIntermediateOutputPath for typemap outputs
  • Sentinel file for stale artifact detection on mode switch

Test exclusions

  • Exclude Export, NativeTypeMap, SSL, TrimmableIgnore categories
  • Exclude Java.Interop test fixtures without JCW classes

@simonrozsival simonrozsival force-pushed the dev/simonrozsival/trimmable-test-plumbing branch from f8106b0 to a437455 Compare April 9, 2026 09:19
@simonrozsival simonrozsival added trimmable-type-map copilot `copilot-cli` or other AIs were used to author this labels Apr 9, 2026
@simonrozsival simonrozsival force-pushed the dev/simonrozsival/trimmable-runtime-fixes branch from d5aefaa to 3a8d509 Compare April 10, 2026 09:49
Base automatically changed from dev/simonrozsival/trimmable-runtime-fixes to main April 10, 2026 22:50
@simonrozsival simonrozsival force-pushed the dev/simonrozsival/trimmable-test-plumbing branch 2 times, most recently from 1814a6d to d0e30f6 Compare April 11, 2026 09:42
@simonrozsival simonrozsival changed the title [TrimmableTypeMap] Test plumbing and CI lane for Mono.Android.NET-Tests [TrimmableTypeMap] Stabilize CoreCLR Mono.Android.NET-Tests lane Apr 11, 2026
@simonrozsival simonrozsival force-pushed the dev/simonrozsival/trimmable-test-plumbing branch from 17b123b to e2c70fd Compare April 11, 2026 23:28
@simonrozsival simonrozsival changed the base branch from main to dev/simonrozsival/root-manifest-referenced-types April 11, 2026 23:28
simonrozsival added a commit that referenced this pull request Apr 11, 2026
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@simonrozsival simonrozsival force-pushed the dev/simonrozsival/trimmable-test-plumbing branch from e2c70fd to 87c531f Compare April 11, 2026 23:50
@simonrozsival simonrozsival changed the title [TrimmableTypeMap] Stabilize CoreCLR Mono.Android.NET-Tests lane [TrimmableTypeMap] Add CoreCLR Mono.Android.NET-Tests lane plumbing Apr 11, 2026
@simonrozsival simonrozsival marked this pull request as ready for review April 12, 2026 08:01
Copilot AI review requested due to automatic review settings April 12, 2026 08:01
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 CoreCLRTrimmable APK instrumentation lane and pins the existing CoreCLR lane to llvm-ir.
  • Updates Mono.Android.NET-Tests.csproj to 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.

Copy link
Copy Markdown
Member Author

@simonrozsival simonrozsival left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 AI Review Summary

Verdict: ✅ LGTM

Found 1 suggestion:

  • 💡 Code organization — the trimmable category filtering is now split between Mono.Android.NET-Tests.csproj and NUnitInstrumentation.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.

Base automatically changed from dev/simonrozsival/root-manifest-referenced-types to main April 13, 2026 11:47
@simonrozsival simonrozsival force-pushed the dev/simonrozsival/trimmable-test-plumbing branch 2 times, most recently from 02780bb to c3a7ebc Compare April 13, 2026 12:15
Comment thread build-tools/automation/yaml-templates/stage-package-tests.yaml Outdated
@simonrozsival simonrozsival force-pushed the dev/simonrozsival/trimmable-test-plumbing branch from 619991d to 6a0644f Compare April 13, 2026 16:39
@simonrozsival simonrozsival changed the base branch from main to dev/simonrozsival/fix-deferred-registration-propagation April 13, 2026 16:39
Base automatically changed from dev/simonrozsival/fix-deferred-registration-propagation to main April 14, 2026 14:19
@simonrozsival simonrozsival force-pushed the dev/simonrozsival/trimmable-test-plumbing branch from 19ec2fa to e084238 Compare April 14, 2026 14:32
@simonrozsival simonrozsival changed the title [TrimmableTypeMap] Add CoreCLR Mono.Android.NET-Tests lane plumbing [TrimmableTypeMap] Add CoreCLRTrimmable Mono.Android.NET-Tests lane plumbing Apr 14, 2026
simonrozsival and others added 5 commits April 16, 2026 16:43
…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>
@simonrozsival simonrozsival force-pushed the dev/simonrozsival/trimmable-test-plumbing branch from 6da7dae to 9082b24 Compare April 16, 2026 15:10
simonrozsival added a commit that referenced this pull request Apr 16, 2026
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
simonrozsival and others added 12 commits April 16, 2026 18:16
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>
@simonrozsival
Copy link
Copy Markdown
Member Author

simonrozsival commented Apr 17, 2026

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 main.

Child PRs

# Scope Status
#11142 Package CoreCLR preserve list in SDK pack (+<None> with Link + test asserting the file is actually packaged) open
#11144 Fix IL1034 by excluding the app assembly from trimmer roots (Option A — preserves the whole point of the trimmable typemap by not rooting everything) merged
#11143 Manifest generator fixes: default instrumentation targetPackage, stale-manifest sentinel on mode switch, outer IntermediateOutputPath, CRC name rewriting in manifest template open
#11145 Runtime and typemap generator fixes: init ordering, generic type proxy loading, ClassLoader mismatch in RegisterNatives, abstract Instrumentation/Application subtypes, proxy self-application as [JavaPeerProxy] + regression test open

Once the children merge, this PR will be rebased to include only:

  • The new CoreCLRTrimmable CI lane + TestEnvironment plumbing.
  • Test-side adjustments and CoreCLRTrimmable-specific skips for failing JavaCast/JavaAs/JavaObjectArray tests.

Follow-ups (issues)

  • Unify the native-method registration approach: always defer via __md_registerNatives () for all types, remove the CannotRegisterInStaticConstructor flag and its propagation helpers, and revisit static { registerNatives } as a later optimization for provably safe types. ([TrimmableTypeMap] Runtime and typemap generator fixes #11145 notes the known gap this creates for concrete descendants of abstract Application/Instrumentation.)

@simonrozsival simonrozsival marked this pull request as draft April 17, 2026 10:35
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

copilot `copilot-cli` or other AIs were used to author this trimmable-type-map

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants