From a1c7ecc45ba7631fc2dd6db4e3a58a9a51f206b9 Mon Sep 17 00:00:00 2001 From: Sven Boemer Date: Fri, 16 Jan 2026 21:11:57 -0800 Subject: [PATCH 1/9] Use android-native-tools for NativeAOT linking instead of Android NDK This eliminates the NDK dependency for NativeAOT builds by using our custom LLVM build (android-native-tools) that ships with the Android workload. Changes: - Add LinkNativeAotLibrary task that invokes ld.lld from android-native-tools - Set NativeCompilationDuringPublish=false and NativeLib=Static so ILC produces only object files, then we link them ourselves - Add -z nostart-stop-gc linker flag for __start/__stop symbols - Include sysroot libraries (libc++_static.a, libunwind.a, etc.) in NativeAOT runtime pack - Fix duplicate assembly error by removing items before re-adding them to ResolvedFileToPublish - Keep legacy NDK path behind AndroidNativeAotUseNdk=true flag --- .../Microsoft.Android.Runtime.proj | 14 + ...oft.Android.Sdk.AssemblyResolution.targets | 7 +- .../Microsoft.Android.Sdk.NativeAOT.targets | 213 +++++++++++++--- .../Tasks/LinkNativeAotLibrary.cs | 239 ++++++++++++++++++ .../ProcessRuntimePackLibraryDirectories.cs | 3 +- .../Utilities/NativeLinker.cs | 7 + 6 files changed, 443 insertions(+), 40 deletions(-) create mode 100644 src/Xamarin.Android.Build.Tasks/Tasks/LinkNativeAotLibrary.cs diff --git a/build-tools/create-packs/Microsoft.Android.Runtime.proj b/build-tools/create-packs/Microsoft.Android.Runtime.proj index 93452ea06b6..0289d0aa47a 100644 --- a/build-tools/create-packs/Microsoft.Android.Runtime.proj +++ b/build-tools/create-packs/Microsoft.Android.Runtime.proj @@ -105,6 +105,20 @@ projects that use the Microsoft.Android.Runtimes framework in .NET 6+. + + + + + + + + + + + + + + <_AndroidRuntimePackAssemblies Include="$(_MonoAndroidNETOutputRoot)$(AndroidLatestStableApiLevel)\Microsoft.Android.Runtime.NativeAOT.dll" /> diff --git a/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.AssemblyResolution.targets b/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.AssemblyResolution.targets index b1717d2b0fe..dd9d74553f0 100644 --- a/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.AssemblyResolution.targets +++ b/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.AssemblyResolution.targets @@ -32,8 +32,11 @@ _ResolveAssemblies MSBuild target. - - <_RunAotMaybe Condition=" '$(_AndroidUseMarshalMethods)' != 'True' ">_AndroidAot + + <_RunAotMaybe Condition=" '$(_AndroidUseMarshalMethods)' != 'True' and '$(_AndroidRuntime)' != 'NativeAOT' ">_AndroidAot @@ -10,6 +17,8 @@ This file contains the NativeAOT-specific MSBuild logic for .NET for Android. + + @@ -22,6 +31,28 @@ This file contains the NativeAOT-specific MSBuild logic for .NET for Android. true <_IsPublishing Condition=" '$(_IsPublishing)' == '' ">true + + + false + + + true + + + false + + + <_RunAotMaybe Condition=" '$(_AndroidUseMarshalMethods)' != 'True' ">_AndroidNativeAotCompileAndLink @@ -43,40 +74,38 @@ This file contains the NativeAOT-specific MSBuild logic for .NET for Android. /> - + _AndroidBeforeIlcCompile; + Compile; SetupOSSpecificProps; PrepareForILLink; ILLink; ComputeIlcCompileInputs; _AndroidComputeIlcCompileInputs; - $(IlcCompileDependsOn) - - <_NdkAbi Condition=" '$(RuntimeIdentifier)' == 'android-arm64' ">aarch64 - <_NdkAbi Condition=" '$(RuntimeIdentifier)' == 'android-x64' ">x86_64 - <_NdkSysrootAbi>$(_NdkAbi)-linux-android - <_NdkPrebuiltAbi Condition=" $([MSBuild]::IsOSPlatform('windows')) ">windows-x86_64 - <_NdkPrebuiltAbi Condition=" $([MSBuild]::IsOSPlatform('osx')) ">darwin-x86_64 - <_NdkPrebuiltAbi Condition=" $([MSBuild]::IsOSPlatform('linux')) ">linux-x86_64 - <_NdkSysrootDir>$(_AndroidNdkDirectory)toolchains/llvm/prebuilt/$(_NdkPrebuiltAbi)/sysroot/usr/lib/$(_NdkSysrootAbi)/ - <_NdkBinDir>$(_AndroidNdkDirectory)toolchains/llvm/prebuilt/$(_NdkPrebuiltAbi)/bin/ - clang++ - llvm-objcopy + + Static false - + true + <_ExtraTrimmerArgs>$(_ExtraTrimmerArgs) --notrimwarn - - @@ -96,39 +123,35 @@ This file contains the NativeAOT-specific MSBuild logic for .NET for Android. $(_OriginalSuppressTrimAnalysisWarnings) - - - - - + <_AndroidILLinkAssemblies Include="@(ManagedAssemblyToLink->'$(IntermediateLinkDir)%(Filename)%(Extension)')" Condition="Exists('$(IntermediateLinkDir)%(Filename)%(Extension)')" /> + + <_AndroidILLinkAssemblies Remove="$(IntermediateLinkDir)$(TargetName)$(TargetExt)" /> + - + + + + - <_NdkLibs Include="@(RuntimePackAsset->WithMetadataValue('Filename', 'libnaot-android.$(Configuration.ToLower())-static-$(Configuration.ToLower())'))" /> - - - <_NdkLibs Include="$(_NdkSysrootDir)libc++_static.a" /> - <_NdkLibs Include="$(_NdkSysrootDir)libc++abi.a" /> - - - - @@ -192,19 +215,135 @@ This file contains the NativeAOT-specific MSBuild logic for .NET for Android. DebugBuild="$(AndroidIncludeDebugSymbols)" WorkingDirectory="$(_NativeAssemblySourceDir)" AndroidBinUtilsDirectory="$(AndroidBinUtilsDirectory)" /> + + + + + <_AndroidNativeAotLinkTarget Condition=" '$(AndroidNativeAotUseNdk)' != 'true' ">_LinkNativeAotLibrary + <_AndroidNativeAotLinkTarget Condition=" '$(AndroidNativeAotUseNdk)' == 'true' ">_GenerateNativeAotAndroidAppLibrary + + + + + + + - - + + <_NativeAotObjectFiles Include="@(_PrivateJniInitFuncsNativeObjectFile)" /> + <_NativeAotObjectFiles Include="@(_PrivateEnvironmentNativeObjectFile)" /> + + + <_NativeAotArchives Include="@(NativeLibrary)" /> + + + <_NativeAotArchives Include="@(RuntimePackAsset->WithMetadataValue('Filename', 'libnaot-android.$(Configuration.ToLower())-static-$(Configuration.ToLower())'))" /> + + + + + + + + <_NativeAotOutputLibrary>$(NativeOutputPath)lib$(TargetName).so + + + + + + + + + + + + + + + + lib$(TargetName).so + lib$(TargetName).so + PreserveNewest + - + + DependsOnTargets="_GenerateNativeAotAndroidAppAssemblerSources" + Condition=" '$(AndroidNativeAotUseNdk)' == 'true' "> + + + <_NdkAbi Condition=" '$(RuntimeIdentifier)' == 'android-arm64' ">aarch64 + <_NdkAbi Condition=" '$(RuntimeIdentifier)' == 'android-x64' ">x86_64 + <_NdkSysrootAbi>$(_NdkAbi)-linux-android + <_NdkPrebuiltAbi Condition=" $([MSBuild]::IsOSPlatform('windows')) ">windows-x86_64 + <_NdkPrebuiltAbi Condition=" $([MSBuild]::IsOSPlatform('osx')) ">darwin-x86_64 + <_NdkPrebuiltAbi Condition=" $([MSBuild]::IsOSPlatform('linux')) ">linux-x86_64 + <_NdkSysrootDir>$(_AndroidNdkDirectory)toolchains/llvm/prebuilt/$(_NdkPrebuiltAbi)/sysroot/usr/lib/$(_NdkSysrootAbi)/ + <_NdkBinDir>$(_AndroidNdkDirectory)toolchains/llvm/prebuilt/$(_NdkPrebuiltAbi)/bin/ + clang++ + llvm-objcopy + + + + + + + + + + + <_NdkLibs Include="@(RuntimePackAsset->WithMetadataValue('Filename', 'libnaot-android.$(Configuration.ToLower())-static-$(Configuration.ToLower())'))" /> + + + <_NdkLibs Include="$(_NdkSysrootDir)libc++_static.a" /> + <_NdkLibs Include="$(_NdkSysrootDir)libc++abi.a" /> + + + + + diff --git a/src/Xamarin.Android.Build.Tasks/Tasks/LinkNativeAotLibrary.cs b/src/Xamarin.Android.Build.Tasks/Tasks/LinkNativeAotLibrary.cs new file mode 100644 index 00000000000..bf524fa2946 --- /dev/null +++ b/src/Xamarin.Android.Build.Tasks/Tasks/LinkNativeAotLibrary.cs @@ -0,0 +1,239 @@ +#nullable enable +using System; +using System.Collections.Generic; +using System.IO; + +using Microsoft.Build.Framework; +using Microsoft.Build.Utilities; + +using Microsoft.Android.Build.Tasks; + +namespace Xamarin.Android.Tasks; + +/// +/// Links NativeAOT-compiled object files into a shared library (.so) for Android. +/// Uses android-native-tools (our custom LLVM build) instead of the Android NDK. +/// +public class LinkNativeAotLibrary : AndroidTask +{ + public override string TaskPrefix => "LNA"; + + [Required] + public string AndroidBinUtilsDirectory { get; set; } = ""; + + [Required] + public string IntermediateOutputPath { get; set; } = ""; + + /// + /// The main object file produced by ILC ($(NativeObject)). + /// + [Required] + public string NativeObject { get; set; } = ""; + + /// + /// Additional object files (e.g., generated assembler sources). + /// + public ITaskItem[] NativeObjectFiles { get; set; } = []; + + /// + /// Static archives from ILC SDK and runtime packs. + /// + [Required] + public ITaskItem[] NativeArchives { get; set; } = []; + + [Required] + public string OutputLibrary { get; set; } = ""; + + [Required] + public string RuntimeIdentifier { get; set; } = ""; + + [Required] + public ITaskItem[] RuntimePackLibraryDirectories { get; set; } = []; + + public bool StripDebugSymbols { get; set; } = true; + public bool SaveDebugSymbols { get; set; } = true; + + public override bool RunTask () + { + string abi = GetAbiFromRuntimeIdentifier (RuntimeIdentifier); + string clangArch = GetClangArchFromRuntimeIdentifier (RuntimeIdentifier); + + // Compute soname - Android requires a proper soname or it will refuse to load the library + string soname = Path.GetFileNameWithoutExtension (OutputLibrary); + if (soname.StartsWith ("lib", StringComparison.OrdinalIgnoreCase)) { + soname = soname.Substring (3); + } + + // Find the sysroot directory from runtime pack library directories + string? sysrootDir = FindSysrootDirectory (); + if (sysrootDir == null) { + Log.LogError ("Could not find sysroot directory containing C++ runtime libraries in runtime pack"); + return false; + } + + var linker = new NativeLinker (Log, abi, soname, AndroidBinUtilsDirectory, IntermediateOutputPath, RuntimePackLibraryDirectories) { + StripDebugSymbols = StripDebugSymbols, + SaveDebugSymbols = SaveDebugSymbols, + AllowUndefinedSymbols = false, + UseNdkLibraries = false, + TargetsCLR = false, // NativeAOT doesn't need --eh-frame-hdr like CoreCLR does + UseSymbolic = true, + IsNativeAOT = true, // Enable NativeAOT-specific linker flags + }; + + List linkItems = OrganizeCommandLineItems (abi, sysrootDir, clangArch); + List linkStartFiles = GetCrtStartFiles (abi, sysrootDir); + List linkEndFiles = GetCrtEndFiles (abi, sysrootDir); + + bool success = linker.Link ( + CreateItemWithAbi (OutputLibrary, abi), + linkItems, + linkStartFiles, + linkEndFiles, + exportDynamicSymbols: null + ); + + if (!success) { + Log.LogError ($"Failed to link NativeAOT library: {OutputLibrary}"); + } + + return success; + } + + string GetAbiFromRuntimeIdentifier (string rid) + { + return rid switch { + "android-arm64" => "arm64-v8a", + "android-x64" => "x86_64", + _ => throw new NotSupportedException ($"Unsupported RuntimeIdentifier for NativeAOT: {rid}") + }; + } + + string GetClangArchFromRuntimeIdentifier (string rid) + { + return rid switch { + "android-arm64" => "aarch64", + "android-x64" => "x86_64", + _ => throw new NotSupportedException ($"Unsupported RuntimeIdentifier for NativeAOT: {rid}") + }; + } + + /// + /// Finds the sysroot directory containing C++ runtime libraries. + /// + string? FindSysrootDirectory () + { + foreach (var dir in RuntimePackLibraryDirectories) { + string libcppPath = Path.Combine (dir.ItemSpec, "libc++_static.a"); + if (File.Exists (libcppPath)) { + return dir.ItemSpec; + } + } + return null; + } + + /// + /// Get CRT start files (crtbegin_so.o). + /// + List GetCrtStartFiles (string abi, string sysrootDir) + { + var items = new List (); + string crtbegin = Path.Combine (sysrootDir, "crtbegin_so.o"); + if (File.Exists (crtbegin)) { + items.Add (CreateItemWithAbi (crtbegin, abi)); + } else { + Log.LogError ($"Required CRT file 'crtbegin_so.o' not found in {sysrootDir}. The NativeAOT runtime pack may be incomplete."); + } + return items; + } + + /// + /// Get CRT end files (crtend_so.o). + /// + List GetCrtEndFiles (string abi, string sysrootDir) + { + var items = new List (); + string crtend = Path.Combine (sysrootDir, "crtend_so.o"); + if (File.Exists (crtend)) { + items.Add (CreateItemWithAbi (crtend, abi)); + } else { + Log.LogError ($"Required CRT file 'crtend_so.o' not found in {sysrootDir}. The NativeAOT runtime pack may be incomplete."); + } + return items; + } + + /// + /// Organizes link items in the correct order for the native linker. + /// Order matters for static linking! + /// + List OrganizeCommandLineItems (string abi, string sysrootDir, string clangArch) + { + var items = new List (); + + // First: ILC's main object file + items.Add (CreateItemWithAbi (NativeObject, abi)); + + // Then: additional object files (generated assembler sources) + foreach (ITaskItem objFile in NativeObjectFiles) { + items.Add (CreateItemWithAbi (objFile.ItemSpec, abi)); + } + + // Then: static archives from ILC SDK and runtime packs + foreach (ITaskItem archive in NativeArchives) { + var item = CreateItemWithAbi (archive.ItemSpec, abi); + // Check if this archive should be included with --whole-archive + string? wholeArchive = archive.GetMetadata (KnownMetadata.NativeLinkWholeArchive); + if (!wholeArchive.IsNullOrEmpty () && Boolean.Parse (wholeArchive)) { + item.SetMetadata (KnownMetadata.NativeLinkWholeArchive, "true"); + } + items.Add (item); + } + + // C++ standard library (required by NativeAOT runtime for std::nothrow, operator new/delete, etc.) + string libcppStatic = Path.Combine (sysrootDir, "libc++_static.a"); + if (File.Exists (libcppStatic)) { + items.Add (CreateItemWithAbi (libcppStatic, abi)); + } else { + Log.LogError ($"Required library 'libc++_static.a' not found in {sysrootDir}. The NativeAOT runtime pack may be incomplete."); + } + + string libcppabi = Path.Combine (sysrootDir, "libc++abi.a"); + if (File.Exists (libcppabi)) { + items.Add (CreateItemWithAbi (libcppabi, abi)); + } else { + Log.LogError ($"Required library 'libc++abi.a' not found in {sysrootDir}. The NativeAOT runtime pack may be incomplete."); + } + + // Unwinding support + string libunwind = Path.Combine (sysrootDir, "libunwind.a"); + if (File.Exists (libunwind)) { + items.Add (CreateItemWithAbi (libunwind, abi)); + } else { + Log.LogError ($"Required library 'libunwind.a' not found in {sysrootDir}. The NativeAOT runtime pack may be incomplete."); + } + + // Compiler runtime builtins (required for atomic intrinsics and TLS emulation) + string libclangBuiltins = Path.Combine (sysrootDir, $"libclang_rt.builtins-{clangArch}-android.a"); + if (File.Exists (libclangBuiltins)) { + items.Add (CreateItemWithAbi (libclangBuiltins, abi)); + } else { + Log.LogError ($"Required library 'libclang_rt.builtins-{clangArch}-android.a' not found in {sysrootDir}. The NativeAOT runtime pack may be incomplete."); + } + + // Add required system libraries (linked dynamically) + items.Add (NativeLinker.MakeLibraryItem ("log", abi)); // Android logging + items.Add (NativeLinker.MakeLibraryItem ("z", abi)); // zlib compression + items.Add (NativeLinker.MakeLibraryItem ("m", abi)); // math library + items.Add (NativeLinker.MakeLibraryItem ("dl", abi)); // dynamic linking + items.Add (NativeLinker.MakeLibraryItem ("c", abi)); // C library (must be last) + + return items; + } + + ITaskItem CreateItemWithAbi (string path, string abi) + { + var item = new TaskItem (path); + item.SetMetadata (KnownMetadata.Abi, abi); + return item; + } +} diff --git a/src/Xamarin.Android.Build.Tasks/Tasks/ProcessRuntimePackLibraryDirectories.cs b/src/Xamarin.Android.Build.Tasks/Tasks/ProcessRuntimePackLibraryDirectories.cs index 142a0dccde6..c4d10e2f3f9 100644 --- a/src/Xamarin.Android.Build.Tasks/Tasks/ProcessRuntimePackLibraryDirectories.cs +++ b/src/Xamarin.Android.Build.Tasks/Tasks/ProcessRuntimePackLibraryDirectories.cs @@ -95,6 +95,7 @@ bool IsInSupportedRuntimePack (ITaskItem item) } return NuGetPackageId.StartsWith ("Microsoft.Android.Runtime.CoreCLR.", StringComparison.OrdinalIgnoreCase) || - NuGetPackageId.StartsWith ("Microsoft.Android.Runtime.Mono.", StringComparison.OrdinalIgnoreCase); + NuGetPackageId.StartsWith ("Microsoft.Android.Runtime.Mono.", StringComparison.OrdinalIgnoreCase) || + NuGetPackageId.StartsWith ("Microsoft.Android.Runtime.NativeAOT.", StringComparison.OrdinalIgnoreCase); } } diff --git a/src/Xamarin.Android.Build.Tasks/Utilities/NativeLinker.cs b/src/Xamarin.Android.Build.Tasks/Utilities/NativeLinker.cs index ac53cfebd11..44df0f0cf33 100644 --- a/src/Xamarin.Android.Build.Tasks/Utilities/NativeLinker.cs +++ b/src/Xamarin.Android.Build.Tasks/Utilities/NativeLinker.cs @@ -49,6 +49,7 @@ class NativeLinker public bool UseNdkLibraries { get; set; } = false; public bool TargetsCLR { get; set; } public bool UseSymbolic { get; set; } + public bool IsNativeAOT { get; set; } public string? NdkRootPath { get; set; } public string? NdkApiLevel { get; set; } public int ZipAlignmentPages { get; set; } = AndroidZipAlign.DefaultZipAlignment64Bit; @@ -163,6 +164,12 @@ public bool Link (ITaskItem outputLibraryPath, List linkItems, List Date: Tue, 20 Jan 2026 15:55:56 -0800 Subject: [PATCH 2/9] Add --eh-frame-hdr linker flag for NativeAOT builds NativeAOT's DWARF-based stack unwinder (LLVM libunwind) requires the .eh_frame_hdr section to locate Frame Description Entries during GC stack walking. Without this section, the runtime crashes with SIGSEGV when attempting to read unwind information from invalid memory. --- src/Xamarin.Android.Build.Tasks/Tasks/LinkNativeAotLibrary.cs | 2 +- src/Xamarin.Android.Build.Tasks/Utilities/NativeLinker.cs | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Xamarin.Android.Build.Tasks/Tasks/LinkNativeAotLibrary.cs b/src/Xamarin.Android.Build.Tasks/Tasks/LinkNativeAotLibrary.cs index bf524fa2946..3fa91120660 100644 --- a/src/Xamarin.Android.Build.Tasks/Tasks/LinkNativeAotLibrary.cs +++ b/src/Xamarin.Android.Build.Tasks/Tasks/LinkNativeAotLibrary.cs @@ -76,7 +76,7 @@ public override bool RunTask () SaveDebugSymbols = SaveDebugSymbols, AllowUndefinedSymbols = false, UseNdkLibraries = false, - TargetsCLR = false, // NativeAOT doesn't need --eh-frame-hdr like CoreCLR does + TargetsCLR = false, // NativeAOT uses its own runtime, not CoreCLR UseSymbolic = true, IsNativeAOT = true, // Enable NativeAOT-specific linker flags }; diff --git a/src/Xamarin.Android.Build.Tasks/Utilities/NativeLinker.cs b/src/Xamarin.Android.Build.Tasks/Utilities/NativeLinker.cs index 44df0f0cf33..5b5f5d89262 100644 --- a/src/Xamarin.Android.Build.Tasks/Utilities/NativeLinker.cs +++ b/src/Xamarin.Android.Build.Tasks/Utilities/NativeLinker.cs @@ -156,8 +156,8 @@ public bool Link (ITaskItem outputLibraryPath, List linkItems, List Date: Wed, 21 Jan 2026 13:34:55 -0800 Subject: [PATCH 3/9] Fix NativeAOT linking for projects with spaces in names Quote the soname argument to handle project names containing spaces. Without this fix, a project named 'Test Me' would cause the linker to fail with 'cannot open Me: No such file or directory' because the unquoted soname would be split into separate arguments. --- src/Xamarin.Android.Build.Tasks/Utilities/NativeLinker.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Xamarin.Android.Build.Tasks/Utilities/NativeLinker.cs b/src/Xamarin.Android.Build.Tasks/Utilities/NativeLinker.cs index 5b5f5d89262..8b5c04d767a 100644 --- a/src/Xamarin.Android.Build.Tasks/Utilities/NativeLinker.cs +++ b/src/Xamarin.Android.Build.Tasks/Utilities/NativeLinker.cs @@ -66,7 +66,7 @@ public NativeLinker (TaskLoggingHelper log, string abi, string soname, string bi ld = Path.Combine (binutilsDir, MonoAndroidHelper.GetExecutablePath (binutilsDir, "ld")); objcopy = Path.Combine (binutilsDir, MonoAndroidHelper.GetExecutablePath (binutilsDir, "llvm-objcopy")); - extraArgs.Add ($"-soname {soname}"); + extraArgs.Add ($"-soname {MonoAndroidHelper.QuoteFileNameArgument (soname)}"); string? elfArch = null; uint maxPageSize; From d2c6f45c55590e1e810cb897dfef3a00fbac27b1 Mon Sep 17 00:00:00 2001 From: Sven Boemer Date: Mon, 2 Feb 2026 11:48:48 -0800 Subject: [PATCH 4/9] Remove sysroot libraries from NativeAOT publish output The sysroot libraries (libc.so, libdl.so, liblog.so, libm.so, libz.so) are needed by ld.lld for symbol resolution during linking, but should not be packaged in the APK - the real implementations are provided by the Android system at runtime. For Mono/CoreCLR, these are removed in _ResolveAssemblies via ProcessRuntimePackLibraryDirectories, but NativeAOT was excluded from that logic. Remove the exclusion so all runtimes use the same removal logic in the outer build. The inner build still runs ProcessRuntimePackLibraryDirectories to get _RuntimePackLibraryDirectory for linking, but doesn't need to handle the removal - that's done by the outer build after it receives the ResolvedFileToPublish items. --- .../targets/Microsoft.Android.Sdk.AssemblyResolution.targets | 1 - .../targets/Microsoft.Android.Sdk.NativeAOT.targets | 3 ++- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.AssemblyResolution.targets b/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.AssemblyResolution.targets index dd9d74553f0..00179fd0b38 100644 --- a/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.AssemblyResolution.targets +++ b/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.AssemblyResolution.targets @@ -116,7 +116,6 @@ _ResolveAssemblies MSBuild target. are never taken into consideration in any context. --> diff --git a/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.NativeAOT.targets b/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.NativeAOT.targets index a1c08760901..be672824e4f 100644 --- a/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.NativeAOT.targets +++ b/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.NativeAOT.targets @@ -272,11 +272,12 @@ This eliminates the NDK dependency for NativeAOT builds. We need to run ProcessRuntimePackLibraryDirectories here because this code runs in an inner build and `_RuntimePackLibraryDirectory` items aren't carried over from the outer build. This discovers sysroot library directories (containing libc.so, liblog.so, etc.) from the runtime pack. + Note: Removal of sysroot libraries from ResolvedFileToPublish happens in the outer build's + _ResolveAssemblies target, not here. --> - From 2a742d0ca879808f2b7bf7489713df5e8c0852d2 Mon Sep 17 00:00:00 2001 From: Sven Boemer Date: Mon, 2 Feb 2026 16:19:35 -0800 Subject: [PATCH 5/9] Copy CRT object files to NativeAOT runtime pack The _CopyToPackDirs target was only copying *.a files for NativeAOT, but crtbegin_so.o and crtend_so.o are required for linking. Add *.o glob pattern to NativeAOT section, matching CoreCLR behavior. --- src/native/native.targets | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/native/native.targets b/src/native/native.targets index 9cd05f59b3a..c2d73b1fda2 100644 --- a/src/native/native.targets +++ b/src/native/native.targets @@ -338,6 +338,10 @@ + <_RuntimePackFiles Include="$(NativeRuntimeOutputRootDir)$(_RuntimeRedistDirName)\%(AndroidSupportedTargetJitAbi.AndroidRID)\*.o" + AndroidRID="%(AndroidSupportedTargetJitAbi.AndroidRID)" + AndroidRuntime="$(CMakeRuntimeFlavor)" + RuntimePackName="$(_RuntimePackName)" /> <_RuntimePackFiles Include="$(NativeRuntimeOutputRootDir)$(_RuntimeRedistDirName)\%(AndroidSupportedTargetJitAbi.AndroidRID)\*.a" AndroidRID="%(AndroidSupportedTargetJitAbi.AndroidRID)" AndroidRuntime="$(CMakeRuntimeFlavor)" From 844cb822312bdcb1f4dd1afdb1e13fbb97b7c033 Mon Sep 17 00:00:00 2001 From: Sven Boemer Date: Thu, 12 Feb 2026 14:24:16 -0800 Subject: [PATCH 6/9] Skip runtime pack directories without DSO stub instead of throwing MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit NativeAOT runtime packs intentionally don't include libarchive-dso-stub.so since they compile to native code directly and don't need DSO wrapping. DSOWrapperGenerator.GetConfig() previously threw InvalidOperationException for any runtime pack directory missing the stub, breaking NativeAOT builds with XACNF7009. A missing stub simply means the directory doesn't participate in DSO wrapping — log and skip instead of throwing. --- .../Utilities/DSOWrapperGenerator.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/Xamarin.Android.Build.Tasks/Utilities/DSOWrapperGenerator.cs b/src/Xamarin.Android.Build.Tasks/Utilities/DSOWrapperGenerator.cs index ff584246f61..cf793828785 100644 --- a/src/Xamarin.Android.Build.Tasks/Utilities/DSOWrapperGenerator.cs +++ b/src/Xamarin.Android.Build.Tasks/Utilities/DSOWrapperGenerator.cs @@ -68,7 +68,10 @@ public static Config GetConfig (TaskLoggingHelper log, string androidBinUtilsDir string stubPath = Path.Combine (packLibDir.ItemSpec, StubFileName); if (!File.Exists (stubPath)) { - throw new InvalidOperationException ($"Internal error: archive DSO stub file '{stubPath}' does not exist in runtime pack at {packLibDir}"); + // Not all runtime packs include the DSO stub (e.g. NativeAOT packs don't + // need DSO wrapping), so just skip directories that don't participate. + log.LogDebugMessage ($"Skipping runtime pack directory '{packLibDir.ItemSpec}': no DSO stub found"); + continue; } AndroidTargetArch arch = MonoAndroidHelper.RidToArch (packRID); From fdbf2422d937edb53d1a681e92977ec7e3d1c6a9 Mon Sep 17 00:00:00 2001 From: Sven Boemer Date: Tue, 17 Feb 2026 22:36:59 -0800 Subject: [PATCH 7/9] Fix NativeAOT build failure when llvm-objcopy is not in PATH Set ObjCopyName to the llvm-objcopy shipped with our workload pack so that ILC's SetupOSSpecificProps target does not search PATH for a system-installed llvm-objcopy/objcopy. This fixes the Android.NET_Tests-NativeAOT CI failure on macOS agents where LLVM tooling is not installed system-wide. --- .../targets/Microsoft.Android.Sdk.NativeAOT.targets | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.NativeAOT.targets b/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.NativeAOT.targets index b1df8f64042..b456dc98a9c 100644 --- a/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.NativeAOT.targets +++ b/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.NativeAOT.targets @@ -106,6 +106,14 @@ This eliminates the NDK dependency for NativeAOT builds. false + + $([System.IO.Path]::Combine('$(AndroidBinUtilsDirectory)', 'llvm-objcopy')) + true From d912268fce1dce08fca917c1e920c34fd27c5fd8 Mon Sep 17 00:00:00 2001 From: Sven Boemer Date: Fri, 20 Feb 2026 12:38:45 -0800 Subject: [PATCH 8/9] Fix _TouchAndroidLinkFlag skipping on NativeAOT incremental builds Add '$(RunILLink)' != 'false' to the Condition of _TouchAndroidLinkFlag so it Condition-skips in lockstep with the ILLink target it hooks via AfterTargets. This works around an MSBuild behavior (dotnet/msbuild#13274) where AfterTargets hooks may fire before the target actually executes, leading to a stale flag. Without this fix, _TouchAndroidLinkFlag's Inputs/Outputs check sees stale timestamps on the first (skipped) ILLink invocation and skips itself, then never runs again when ILLink actually executes. This can sometimes cause link.flag to go stale, which can cause _RemoveRegisterAttribute and the AssemblyModifierPipeline to skip on incremental builds, producing wrong Java stubs. --- .../targets/Microsoft.Android.Sdk.TypeMap.LlvmIr.targets | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.TypeMap.LlvmIr.targets b/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.TypeMap.LlvmIr.targets index adca4dad466..68fd6b77705 100644 --- a/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.TypeMap.LlvmIr.targets +++ b/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.TypeMap.LlvmIr.targets @@ -243,7 +243,7 @@ From 9b4279e9e5f8f28051e219715c8de6592be1870c Mon Sep 17 00:00:00 2001 From: Sven Boemer Date: Fri, 20 Feb 2026 16:18:35 -0800 Subject: [PATCH 9/9] Exclude NativeAOT runtime pack .so shims from APK packaging The NativeAOT runtime pack ships both static (.a) and shared (.so) versions of PAL libraries (System.Native, System.IO.Compression.Native, etc.). The .a archives are statically linked into the application binary by LinkNativeAotLibrary, but the .so copies were also flowing through ResolvedFileToPublish into the APK, adding ~1.6MB of dead weight. Filter them out in _AndroidComputeIlcCompileInputs by removing ResolvedFileToPublish items with .so extension from the Microsoft.NETCore.App.Runtime.NativeAOT.* packages. --- .../targets/Microsoft.Android.Sdk.NativeAOT.targets | 10 ++++++++++ .../BuildReleaseArm64SimpleDotNet.NativeAOT.apkdesc | 8 ++++---- .../BuildReleaseArm64XFormsDotNet.NativeAOT.apkdesc | 8 ++++---- 3 files changed, 18 insertions(+), 8 deletions(-) diff --git a/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.NativeAOT.targets b/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.NativeAOT.targets index b456dc98a9c..b3fe99c0740 100644 --- a/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.NativeAOT.targets +++ b/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.NativeAOT.targets @@ -158,6 +158,16 @@ This eliminates the NDK dependency for NativeAOT builds. + + <_NativeAotSharedLibsToRemove Include="@(ResolvedFileToPublish)" + Condition=" '%(ResolvedFileToPublish.Extension)' == '.so' and $([System.String]::new('%(ResolvedFileToPublish.NuGetPackageId)').StartsWith('Microsoft.NETCore.App.Runtime.NativeAOT.')) " /> + + diff --git a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.ProjectTools/Resources/Base/BuildReleaseArm64SimpleDotNet.NativeAOT.apkdesc b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.ProjectTools/Resources/Base/BuildReleaseArm64SimpleDotNet.NativeAOT.apkdesc index ece794fa0da..94f18b0bd1f 100644 --- a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.ProjectTools/Resources/Base/BuildReleaseArm64SimpleDotNet.NativeAOT.apkdesc +++ b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.ProjectTools/Resources/Base/BuildReleaseArm64SimpleDotNet.NativeAOT.apkdesc @@ -5,13 +5,13 @@ "Size": 3124 }, "classes.dex": { - "Size": 24224 + "Size": 25320 }, "lib/arm64-v8a/libUnnamedProject.so": { - "Size": 4968680 + "Size": 5424528 }, "META-INF/BNDLTOOL.RSA": { - "Size": 1211 + "Size": 1213 }, "META-INF/BNDLTOOL.SF": { "Size": 1211 @@ -44,5 +44,5 @@ "Size": 1904 } }, - "PackageSize": 2094050 + "PackageSize": 2225122 } \ No newline at end of file diff --git a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.ProjectTools/Resources/Base/BuildReleaseArm64XFormsDotNet.NativeAOT.apkdesc b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.ProjectTools/Resources/Base/BuildReleaseArm64XFormsDotNet.NativeAOT.apkdesc index 4ce53ff43f2..367744342d5 100644 --- a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.ProjectTools/Resources/Base/BuildReleaseArm64XFormsDotNet.NativeAOT.apkdesc +++ b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.ProjectTools/Resources/Base/BuildReleaseArm64XFormsDotNet.NativeAOT.apkdesc @@ -5,7 +5,7 @@ "Size": 6740 }, "classes.dex": { - "Size": 9136980 + "Size": 9132020 }, "kotlin/annotation/annotation.kotlin_builtins": { "Size": 928 @@ -29,7 +29,7 @@ "Size": 2396 }, "lib/arm64-v8a/libUnnamedProject.so": { - "Size": 21592848 + "Size": 22047616 }, "META-INF/androidx.activity_activity.version": { "Size": 6 @@ -182,7 +182,7 @@ "Size": 6 }, "META-INF/BNDLTOOL.RSA": { - "Size": 1223 + "Size": 1213 }, "META-INF/BNDLTOOL.SF": { "Size": 89178 @@ -2252,5 +2252,5 @@ "Size": 812848 } }, - "PackageSize": 12521545 + "PackageSize": 12652617 } \ No newline at end of file