Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
65b4ad6
[NativeAOT] Make the trimmable typemap the default
simonrozsival Jun 30, 2026
43e8aff
[NativeAOT] Keep _PreTrimmingFixLegacyDesignerUpdateItems off the tri…
simonrozsival Jun 30, 2026
da9db40
[Tests] Skip NativeAOT cases that inspect illink's linked/ output
simonrozsival Jun 30, 2026
5790113
[Tests] More NativeAOT test adjustments for the trimmable typemap def…
simonrozsival Jun 30, 2026
405d7c6
[Tests] DotNetBuild expects mapping.txt for NativeAOT release
simonrozsival Jun 30, 2026
72ce17a
[NativeAOT] Only change the default typemap; keep managed/llvm-ir rea…
simonrozsival Jun 30, 2026
7660ac8
[Tests] Delete the BuildTest2.NativeAOT type-map test
simonrozsival Jun 30, 2026
2bd5e4f
[Tests] Skip SkiaSharpCanvasBasedAppRuns on NativeAOT
simonrozsival Jun 30, 2026
32d24e7
[Tests] Skip BindingWithAndroidJavaSource on NativeAOT
simonrozsival Jul 1, 2026
e75b335
[Tests] Skip CheckLintErrorsAndWarnings on NativeAOT
simonrozsival Jul 1, 2026
98a2929
[NativeAOT] Pass generated ACW keep rules to R8 on trimmable path
simonrozsival Jul 3, 2026
0eb136a
[Tests] Add NativeAOT regression test for R8-kept runtime ACWs
simonrozsival Jul 3, 2026
991bc62
[Tests] Skip XA4212 on NativeAOT (trimmable typemap)
simonrozsival Jul 16, 2026
bd785e3
[Tests] Skip AfterILLinkAdditionalStepsIsSkippedOnSecondBuild on Nati…
simonrozsival Jul 16, 2026
fde53ff
[NativeAOT] Stop emitting dead array-proxy codegen (MaxArrayRank=0)
simonrozsival Jul 16, 2026
cfb957c
Merge remote-tracking branch 'origin/main' into dev/simonrozsival/tri…
simonrozsival Jul 16, 2026
1f31574
[TrimmableTypeMap] Expand additional Java source globs
simonrozsival Jul 16, 2026
5d14ccf
[TrimmableTypeMap] Include generated Java sources in lint
simonrozsival Jul 16, 2026
96188a0
[Tests] Read NativeAOT JCWs from the trimmable output
simonrozsival Jul 16, 2026
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
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ This file contains the NativeAOT-specific MSBuild logic for .NET for Android.
<_AndroidRuntimePackRuntime>NativeAOT</_AndroidRuntimePackRuntime>
<_AndroidUseWorkloadNativeLinker Condition=" '$(_AndroidUseWorkloadNativeLinker)' == '' ">true</_AndroidUseWorkloadNativeLinker>
<_AndroidJcwCodegenTarget Condition=" '$(_AndroidJcwCodegenTarget)' == '' ">JavaInterop1</_AndroidJcwCodegenTarget>
<_AndroidTypeMapImplementation Condition=" '$(_AndroidTypeMapImplementation)' == '' ">managed</_AndroidTypeMapImplementation>
<_AndroidTypeMapImplementation Condition=" '$(_AndroidTypeMapImplementation)' == '' ">trimmable</_AndroidTypeMapImplementation>
<!-- .NET SDK gives: error NETSDK1191: A runtime identifier for the property 'PublishAot' couldn't be inferred. Specify a rid explicitly. -->
<AllowPublishAotWithoutRuntimeIdentifier Condition=" '$(AllowPublishAotWithoutRuntimeIdentifier)' == '' ">true</AllowPublishAotWithoutRuntimeIdentifier>
<!-- NativeAOT's targets currently gives an error about cross-compilation -->
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -819,6 +819,10 @@ public void BindingWithAndroidJavaSource ([Values (AndroidRuntime.CoreCLR, Andro
if (IgnoreUnsupportedConfiguration (runtime, release: isRelease)) {
return;
}

if (IgnoreOnNativeAot (runtime, "R8 shrinks bound library Java types out of classes.dex on the trimmable typemap path (missing proguard keeps). Tracked by https://github.com/dotnet/android/issues/11774.")) {
return;
}
var path = Path.Combine ("temp", TestName);
var lib = new XamarinAndroidBindingProject () {
IsRelease = isRelease,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,9 @@ public void DotNetBuild (string runtimeIdentifiers, bool isRelease, bool aot, bo
if (isRelease) {
expectedFiles.Add ($"{proj.PackageName}.aab");
expectedFiles.Add ($"{proj.PackageName}-Signed.aab");
if (runtime == AndroidRuntime.NativeAOT) {
expectedFiles.Add ("mapping.txt");
}
} else {
expectedFiles.Add ($"{proj.PackageName}.apk");
expectedFiles.Add ($"{proj.PackageName}-Signed.apk.idsig");
Expand Down Expand Up @@ -1056,6 +1059,12 @@ public void XA4212 ([Values (AndroidRuntime.CoreCLR, AndroidRuntime.NativeAOT)]
if (IgnoreUnsupportedConfiguration (runtime, release: isRelease)) {
return;
}
// TODO: the trimmable typemap generator (the NativeAOT default) does not yet emit the
// XA4212 custom-IJavaObject diagnostic that the managed/llvm-ir typemap paths raise via
// XAJavaTypeScanner. Re-enable once that detection is added to TrimmableTypeMapGenerator.
if (IgnoreOnNativeAot (runtime, "the trimmable typemap does not yet emit the XA4212 custom-IJavaObject diagnostic (tracked as a follow-up).")) {
return;
}

var proj = new XamarinAndroidApplicationProject () {
IsRelease = isRelease,
Expand Down Expand Up @@ -1931,6 +1940,9 @@ public void CheckLintErrorsAndWarnings ([Values (AndroidRuntime.CoreCLR, Android
if (IgnoreUnsupportedConfiguration (runtime, release: isRelease)) {
return;
}
if (IgnoreOnNativeAot (runtime, "the trimmable typemap generates additional Java Callable Wrappers that trip XA0102 lint warnings (e.g. CustomX509TrustManager, MissingApplicationIcon). Tracked by https://github.com/dotnet/android/issues/11774.")) {
return;
}

var proj = new XamarinAndroidApplicationProject {
IsRelease = isRelease,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -101,133 +101,6 @@ public void BasicApplicationPublishReadyToRun ([Values] bool isComposite, [Value
StringAssert.Contains ("@uncompressed_assemblies_data_buffer = dso_local local_unnamed_addr global [0 x i8] zeroinitializer, align 1", compressedAssembliesSourceText);
}

[Test]
public void NativeAOT ()
{
var proj = new XamarinAndroidApplicationProject {
IsRelease = true,
ProjectName = "Hello",
};
proj.SetRuntime (AndroidRuntime.NativeAOT);
proj.SetProperty ("_ExtraTrimmerArgs", "--verbose");

// Required for java/util/ArrayList assertion below
proj.MainActivity = proj.DefaultMainActivity
.Replace ("//${AFTER_ONCREATE}", "new Android.Runtime.JavaList (); new Android.Runtime.JavaList<int> ();");

using var b = CreateApkBuilder ();
Assert.IsTrue (b.Build (proj), "Build should have succeeded.");
b.Output.AssertTargetIsNotSkipped ("_PrepareLinking");

string [] mono_classes = [
"Lmono/MonoRuntimeProvider;",
];
string[] mono_files = [
"lib/arm64-v8a/libmonosgen-2.0.so",
"lib/x86_64/libmonosgen-2.0.so",
];
string [] nativeaot_files = [
$"lib/arm64-v8a/lib{proj.ProjectName}.so",
$"lib/x86_64/lib{proj.ProjectName}.so",
];

var intermediate = Path.Combine (Root, b.ProjectDirectory, proj.IntermediateOutputPath);
var output = Path.Combine (Root, b.ProjectDirectory, proj.OutputPath);

var linkedMonoAndroidAssembly = Path.Combine (intermediate, "android-arm64", "linked", "Mono.Android.dll");
FileAssert.Exists (linkedMonoAndroidAssembly);
var javaClassNames = new List<string> ();
var types = new List<TypeReference> ();

using (var assembly = AssemblyDefinition.ReadAssembly (linkedMonoAndroidAssembly)) {
var typeName = "Android.App.Activity";
var methodName = "GetOnCreate_Landroid_os_Bundle_Handler";
var type = assembly.MainModule.GetType (typeName);
Assert.IsNotNull (type, $"{linkedMonoAndroidAssembly} should contain {typeName}");
var method = type.Methods.FirstOrDefault (m => m.Name == methodName);
Assert.IsNotNull (method, $"{linkedMonoAndroidAssembly} should contain {typeName}.{methodName}");

type = assembly.MainModule.Types.FirstOrDefault (t => t.Name == "ManagedTypeMapping");
Assert.IsNotNull (type, $"{linkedMonoAndroidAssembly} should contain ManagedTypeMapping");
method = type.Methods.FirstOrDefault (m => m.Name == "GetJniNameByTypeNameHashIndex");
Assert.IsNotNull (method, $"{type.Name} should contain GetJniNameByTypeNameHashIndex");

foreach (var i in method.Body.Instructions) {
if (i.OpCode != Mono.Cecil.Cil.OpCodes.Ldstr)
continue;
if (i.Operand is not string javaName)
continue;
if (i.Next.OpCode != Mono.Cecil.Cil.OpCodes.Ret)
continue;
javaClassNames.Add (javaName);
}

method = type.Methods.FirstOrDefault (m => m.Name == "GetTypeByJniNameHashIndex");
Assert.IsNotNull (method, $"{type.Name} should contain GetTypeByJniNameHashIndex");

foreach (var i in method.Body.Instructions) {
if (i.OpCode != Mono.Cecil.Cil.OpCodes.Ldtoken)
continue;
if (i.Operand is not TypeReference typeReference)
continue;
if (i.Next?.OpCode != Mono.Cecil.Cil.OpCodes.Call)
continue;
if (i.Next.Next?.OpCode != Mono.Cecil.Cil.OpCodes.Ret)
continue;
types.Add (typeReference);
}

// Basic types
AssertTypeMap ("java/lang/Object", "Java.Lang.Object");
AssertTypeMap ("java/lang/String", "Java.Lang.String");
AssertTypeMap ("[Ljava/lang/Object;", "Java.Interop.JavaArray`1");
AssertTypeMap ("java/util/ArrayList", "Android.Runtime.JavaList");
AssertTypeMap ("android/app/Activity", "Android.App.Activity");
AssertTypeMap ("android/widget/Button", "Android.Widget.Button");
Assert.IsFalse (StringAssertEx.ContainsText (b.LastBuildOutput,
"Duplicate typemap entry for java/util/ArrayList => Android.Runtime.JavaList`1"),
"Should get log message about duplicate Android.Runtime.JavaList`1!");

// Special *Invoker case
AssertTypeMap ("android/view/View$OnClickListener", "Android.Views.View/IOnClickListener");
Assert.IsFalse (StringAssertEx.ContainsText (b.LastBuildOutput,
"Duplicate typemap entry for android/view/View$OnClickListener => Android.Views.View/IOnClickListenerInvoker"),
"Should get log message about duplicate IOnClickListenerInvoker!");
}

// Verify that Java stubs for Mono.Android.dll were generated, instead of using mono.android.jar/dex
var onLayoutChangeListenerImplementor = Path.Combine (intermediate, "android", "src", "mono", "android", "view", "View_OnClickListenerImplementor.java");
FileAssert.Exists (onLayoutChangeListenerImplementor);

var dexFile = Path.Combine (intermediate, "android", "bin", "classes.dex");
FileAssert.Exists (dexFile);
foreach (var className in mono_classes) {
Assert.IsFalse (DexUtils.ContainsClassWithMethod (className, "<init>", "()V", dexFile, AndroidSdkPath), $"`{dexFile}` should *not* include `{className}`!");
}

var apkFile = Path.Combine (output, $"{proj.PackageName}-Signed.apk");
FileAssert.Exists (apkFile);
using var zip = ZipHelper.OpenZip (apkFile);
foreach (var mono_file in mono_files) {
Assert.IsFalse (zip.ContainsEntry (mono_file, caseSensitive: true), $"APK must *not* contain `{mono_file}`.");
}
foreach (var nativeaot_file in nativeaot_files) {
Assert.IsTrue (zip.ContainsEntry (nativeaot_file, caseSensitive: true), $"APK must contain `{nativeaot_file}`.");
}

void AssertTypeMap(string javaName, string managedName)
{
var javaNameIndex = javaClassNames.FindIndex (name => name == javaName);
var typeIndex = types.FindIndex (td => td.ToString() == managedName);

if (javaNameIndex < 0) {
Assert.Fail ($"TypeMapping should contain \"{javaName}\"!");
} else if (typeIndex < 0) {
Assert.Fail ($"TypeMapping should contain \"{managedName}\"!");
}
}
}

[Test]
public void BuildBasicApplicationThenMoveIt ([Values] bool isRelease, [Values (AndroidRuntime.CoreCLR, AndroidRuntime.NativeAOT)] AndroidRuntime runtime)
{
Expand Down Expand Up @@ -280,6 +153,10 @@ public void BuildReleaseArm64 ([Values] bool forms, [Values (AndroidRuntime.Core
return;
}

if (IgnoreNativeAotLinkedAssemblyChecks (runtime)) {
return;
}

var proj = forms ?
new XamarinFormsAndroidApplicationProject () :
new XamarinAndroidApplicationProject ();
Expand Down Expand Up @@ -1671,6 +1548,36 @@ public void BuildProguardEnabledProject ([Values ("", "android-arm64")] string r
}
}

[Test]
public void NativeAotKeepsRuntimeAcwJavaTypesUnderR8 ()
{
const bool isRelease = true;
if (IgnoreUnsupportedConfiguration (AndroidRuntime.NativeAOT, release: isRelease)) {
return;
}
var proj = new XamarinAndroidApplicationProject {
IsRelease = isRelease,
LinkTool = "r8",
};
proj.SetRuntime (AndroidRuntime.NativeAOT);
using (var b = CreateApkBuilder ()) {
Assert.IsTrue (b.Build (proj), "Build should have succeeded.");

var intermediate = Path.Combine (Root, b.ProjectDirectory, proj.IntermediateOutputPath);
var dexFile = Path.Combine (intermediate, "android", "bin", "classes.dex");
FileAssert.Exists (dexFile);

// Regression test: the trimmable NativeAOT path generates its ACW keep rules from the
// ILC DGML into proguard_project_references.cfg. If that file is not passed to R8, R8
// tree-shakes the runtime ACW/JCW classes out of classes.dex and the app crashes at
// startup inside JavaInteropRuntime.init with a ClassNotFoundException for the
// UncaughtExceptionMarshaler Java Callable Wrapper. The JCW class name is CRC-hashed
// (e.g. `scrc64...UncaughtExceptionMarshaler`), so match on the type name suffix.
Assert.IsTrue (DexUtils.ContainsClass ("UncaughtExceptionMarshaler;", dexFile, AndroidSdkPath),
$"`{dexFile}` should include the UncaughtExceptionMarshaler ACW kept by the generated NativeAOT ProGuard rules.");
}
}

XamarinAndroidApplicationProject CreateMultiDexRequiredApplication (string debugConfigurationName = "Debug", string releaseConfigurationName = "Release")
{
var proj = new XamarinAndroidApplicationProject (debugConfigurationName, releaseConfigurationName);
Expand Down Expand Up @@ -1812,7 +1719,11 @@ public override void OnCreate()
using (var b = CreateApkBuilder ()) {
Assert.IsTrue (b.Build (proj), "Build should have succeeded.");
Assert.IsFalse (b.LastBuildOutput.ContainsText ("Duplicate zip entry"), "Should not get warning about [META-INF/MANIFEST.MF]");
var customAppContent = File.ReadAllText (Path.Combine (Root, b.ProjectDirectory, proj.IntermediateOutputPath, "android", "src", "com", "foxsports", "test", "CustomApp.java"));
var customAppJavaDirectory = runtime == AndroidRuntime.NativeAOT ?
Path.Combine ("typemap", "java") :
Path.Combine ("android", "src");
var customAppJava = b.Output.GetIntermediaryPath (Path.Combine (customAppJavaDirectory, "com", "foxsports", "test", "CustomApp.java"));
var customAppContent = File.ReadAllText (customAppJava);
Assert.IsTrue (customAppContent.Contains ("extends android.support.multidex.MultiDexApplication"),
"Custom App class should have inherited from android.support.multidex.MultiDexApplication.");
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -321,6 +321,10 @@ public void ProjectDependencies ([Values] bool projectReference, [Values (Androi
return;
}

if (IgnoreOnNativeAot (runtime, "the trimmable typemap trims Java Callable Wrappers for library types that are never instantiated, so the unused LibraryB JCWs are intentionally absent from classes.dex.")) {
return;
}

// Setup dependencies App A -> Lib B -> Lib C
var path = Path.Combine ("temp", TestName);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -606,6 +606,9 @@ public void AppProjectTargetsDoNotBreak ([Values (AndroidRuntime.CoreCLR, Androi
if (IgnoreUnsupportedConfiguration (runtime, release: isRelease)) {
return;
}
if (IgnoreNativeAotLinkedAssemblyChecks (runtime)) {
return;
}
var targets = new List<(string target, bool ignoreOnNAOT)> {
("_GeneratePackageManagerJava", true), // TODO: NativeAOT doesn't skip this target on 3rd attempt, check if that's ok?
("_ResolveLibraryProjectImports", false),
Expand Down Expand Up @@ -947,6 +950,9 @@ public void LinkAssembliesNoShrink ([Values (AndroidRuntime.CoreCLR, AndroidRunt
if (IgnoreUnsupportedConfiguration (runtime, release: isRelease)) {
return;
}
if (IgnoreNativeAotLinkedAssemblyChecks (runtime)) {
return;
}
var proj = new XamarinFormsAndroidApplicationProject {
IsRelease = isRelease,
};
Expand Down Expand Up @@ -1535,6 +1541,10 @@ public void ChangePackageNamingPolicy ([Values (AndroidRuntime.CoreCLR, AndroidR
return;
}

if (IgnoreOnNativeAot (runtime, "the 'Lowercase' $(AndroidPackageNamingPolicy) is intentionally unsupported with the trimmable typemap (only Crc64 and LowercaseCrc64 are supported).")) {
return;
}
Comment thread
simonrozsival marked this conversation as resolved.

var proj = new XamarinAndroidApplicationProject {
IsRelease = isRelease,
};
Expand Down Expand Up @@ -1837,6 +1847,9 @@ public void AfterILLinkAdditionalStepsIsSkippedOnSecondBuild ([Values (AndroidRu
if (IgnoreUnsupportedConfiguration (runtime, release: isRelease)) {
return;
}
if (IgnoreOnNativeAot (runtime, "the trimmable typemap (the NativeAOT default) generates the typemap at compile time, so `_RunAfterILLinkAdditionalSteps` is intentionally skipped and no `afterlink/` output is produced.")) {
return;
}

var proj = new XamarinAndroidApplicationProject {
IsRelease = isRelease,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1227,8 +1227,15 @@ public void ExportedErrorMessage ([Values (AndroidRuntime.CoreCLR, AndroidRuntim
b.ThrowOnBuildFailure = false;
Assert.IsFalse (b.Build (proj), "Build should have failed");
var extension = IsWindows ? ".exe" : "";
uint errorLine = runtime == AndroidRuntime.NativeAOT ? 11u : 12u;
Assert.IsTrue (b.LastBuildOutput.ContainsText ($"AndroidManifest.xml({errorLine},5): java{extension} error AMM0000:"), "Should receive AMM0000 error");
if (runtime == AndroidRuntime.NativeAOT) {
// The trimmable manifest generator emits the merged components in a different
// (but valid) order than the legacy path, so the offending <service> lands on a
// different manifest line. Assert the coded AMM0000 error itself rather than the
// exact line/column, which is an implementation detail of the manifest layout.
Assert.IsTrue (b.LastBuildOutput.ContainsText ($"java{extension} error AMM0000:"), "Should receive AMM0000 error");
} else {
Assert.IsTrue (b.LastBuildOutput.ContainsText ($"AndroidManifest.xml(12,5): java{extension} error AMM0000:"), "Should receive AMM0000 error");
}
Assert.IsTrue (b.LastBuildOutput.ContainsText ("Apps targeting Android 12 and higher are required to specify an explicit value for `android:exported`"), "Should receive AMM0000 error");
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -395,6 +395,10 @@ public void AndroidAddKeepAlives (bool isRelease, bool setAndroidAddKeepAlivesTr
return;
}

if (IgnoreNativeAotLinkedAssemblyChecks (runtime)) {
return;
}

if (runtime == AndroidRuntime.CoreCLR && isRelease && !setAndroidAddKeepAlivesTrue && setLinkModeNone && shouldAddKeepAlives) {
// This currently fails with the following exception:
//
Expand Down Expand Up @@ -512,6 +516,10 @@ public void AndroidUseNegotiateAuthentication ([Values (true, false, null)] bool
return;
}

if (IgnoreNativeAotLinkedAssemblyChecks (runtime)) {
return;
}

var proj = new XamarinAndroidApplicationProject { IsRelease = true };
proj.SetRuntime (runtime);
proj.AddReferences ("System.Net.Http");
Expand Down Expand Up @@ -549,6 +557,9 @@ public void PreserveIX509TrustManagerSubclasses ([Values] bool hasServerCertific
if (IgnoreUnsupportedConfiguration (runtime, release: isRelease)) {
return;
}
if (IgnoreNativeAotLinkedAssemblyChecks (runtime)) {
return;
}
var proj = new XamarinAndroidApplicationProject { IsRelease = isRelease };
proj.SetRuntime (runtime);
proj.AddReferences ("System.Net.Http");
Expand Down Expand Up @@ -589,6 +600,10 @@ public void PreserveServices ([Values (AndroidRuntime.CoreCLR, AndroidRuntime.Na
return;
}

if (IgnoreNativeAotLinkedAssemblyChecks (runtime)) {
return;
}

var proj = new XamarinAndroidApplicationProject {
IsRelease = isRelease,
TrimModeRelease = TrimMode.Full,
Expand Down Expand Up @@ -735,6 +750,10 @@ public void WarnWithReferenceToPreserveAttribute ([Values (AndroidRuntime.CoreCL
return;
}

if (IgnoreOnNativeAot (runtime, "ILC does not run illink, so the obsolete-PreserveAttribute IL6001 warning is not emitted.")) {
return;
}

var proj = new XamarinAndroidApplicationProject { IsRelease = isRelease };
proj.SetRuntime (runtime);
proj.AddReferences ("System.Net.Http");
Expand Down
Loading
Loading