Conversation
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
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.
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.
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.
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.
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.
# Conflicts: # src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.NativeAOT.targets
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.
# Conflicts: # src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.NativeAOT.targets
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.
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.
…, error handling - Use conventional ELF soname (full filename) instead of stripping lib prefix - Use ABI-prefixed NDK clang++ wrapper with existing ApiLevel properties - Restrict DSOWrapperGenerator silent skip to NativeAOT packs only - Add HasLoggedErrors early-return before invoking linker
…C LinkNative Since NativeCompilationDuringPublish=false and NativeLib=Static are set unconditionally, ILC's LinkNative target never runs. The legacy NDK path now invokes clang++ directly via Exec with a response file, collecting the same ILC outputs (_NativeAotObjectFiles, _NativeAotArchives) and linking them with the NDK's ABI-prefixed clang++ wrapper. Key changes: - DependsOnTargets changed to _CollectNativeAotLinkInputs (which transitively includes _GenerateNativeAotAndroidAppAssemblerSources) - Use ABI-prefixed clang++ wrapper with full path and .cmd extension support for Windows - Pass -nostdlib++ to prevent dynamic libc++ auto-linking - Pass all linker flags via -Wl, prefix (matching new path's flags) - Register output library via ResolvedFileToPublish
# Conflicts: # src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.NativeAOT.targets
Add test infrastructure and CI configuration to validate NativeAOT builds using the workload pack linker (ld.lld) instead of the NDK. This ensures the NDK-free linking path is tested in CI where the NDK is always present on disk. Changes: - Add _ValidateNativeAotLinkerChoice diagnostic target that logs which linker path was used (workload pack vs NDK clang) - Add SetPublishAot(bool) overload that doesn't inject AndroidNdkDirectory - Add useNdkForNativeAot parameter to SetRuntime extension method - Add SuppressNdkInjection property to Builder and DotNetCLI to prevent automatic /p:AndroidNdkDirectory injection - Add NativeAOT_WorkloadPack MSBuild test that verifies NDK-free builds - Add NativeAOTWorkload on-device test flavor in stage-package-tests.yaml
There was a problem hiding this comment.
Pull request overview
This WIP PR aims to validate NativeAOT Android builds without requiring the Android NDK by switching NativeAOT linking to use android-native-tools (workload-pack LLVM/ld.lld) and by extending runtime packs to include the required sysroot/link inputs.
Changes:
- Add a new
LinkNativeAotLibraryMSBuild task and wire NativeAOT targets to link.ooutputs into the final.sovia workload-packld.lld(with an NDK fallback). - Adjust runtime pack processing/creation to include and discover sysroot/linking assets needed for NativeAOT linking without the NDK.
- Add tests/CI coverage to assert the workload-pack linker path is used and outputs are packaged as expected.
Reviewed changes
Copilot reviewed 16 out of 16 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
| src/Xamarin.Android.Build.Tasks/Utilities/NativeLinker.cs | Adds NativeAOT-specific linker behavior and improves soname argument quoting. |
| src/Xamarin.Android.Build.Tasks/Utilities/DSOWrapperGenerator.cs | Skips DSO-stub requirement for NativeAOT runtime packs. |
| src/Xamarin.Android.Build.Tasks/Tests/Xamarin.ProjectTools/Resources/Base/BuildReleaseArm64XFormsDotNet.NativeAOT.apkdesc | Updates expected APK contents/sizes for NativeAOT outputs. |
| src/Xamarin.Android.Build.Tasks/Tests/Xamarin.ProjectTools/Resources/Base/BuildReleaseArm64SimpleDotNet.NativeAOT.apkdesc | Updates expected APK contents/sizes for NativeAOT outputs. |
| src/Xamarin.Android.Build.Tasks/Tests/Xamarin.ProjectTools/Common/DotNetCLI.cs | Adds an option to suppress NDK property injection in test builds. |
| src/Xamarin.Android.Build.Tasks/Tests/Xamarin.ProjectTools/Common/Builder.cs | Adds an option to suppress NDK property injection in test builds. |
| src/Xamarin.Android.Build.Tasks/Tests/Xamarin.ProjectTools/Android/XamarinAndroidApplicationProject.cs | Adds SetPublishAot(bool) overload for NDK-free NativeAOT scenarios. |
| src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Utilities/ProjectExtensions.cs | Adds a useNdkForNativeAot switch when configuring NativeAOT tests. |
| src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/BuildTest2.cs | Adds a test validating NativeAOT workload-pack linking and APK contents. |
| src/Xamarin.Android.Build.Tasks/Tasks/ProcessRuntimePackLibraryDirectories.cs | Treats NativeAOT runtime packs as supported inputs for runtime-pack discovery. |
| src/Xamarin.Android.Build.Tasks/Tasks/LinkNativeAotLibrary.cs | New task that performs NativeAOT .so linking via workload-pack ld.lld. |
| src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.NativeAOT.targets | Reworks NativeAOT pipeline to compile with ILC and link via custom task (NDK optional). |
| src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.AssemblyResolution.targets | Updates RID build/AOT hook points and runtime pack directory processing behavior. |
| src/native/native.targets | Includes .o files in NativeAOT runtime pack inputs. |
| build-tools/create-packs/Microsoft.Android.Runtime.proj | Adds sysroot/C++/compiler-rt assets to the NativeAOT runtime packs. |
| build-tools/automation/yaml-templates/stage-package-tests.yaml | Adds a pipeline test configuration for NativeAOT workload-pack builds. |
| string GetAbiFromRuntimeIdentifier (string rid) | ||
| { | ||
| return rid switch { | ||
| "android-arm64" => "arm64-v8a", | ||
| "android-x64" => "x86_64", | ||
| _ => throw new NotSupportedException ($"Unsupported RuntimeIdentifier for NativeAOT: {rid}") | ||
| }; |
There was a problem hiding this comment.
In this MSBuild task, unsupported RuntimeIdentifier values currently throw NotSupportedException. Throwing here will surface as an unhandled task exception (stack trace) instead of a normal build error. Consider logging a coded error (XA####) and returning false (or mapping through the existing RuntimeIdentifierToAbi logic) so the build fails cleanly.
| string GetClangArchFromRuntimeIdentifier (string rid) | ||
| { | ||
| return rid switch { | ||
| "android-arm64" => "aarch64", | ||
| "android-x64" => "x86_64", | ||
| _ => throw new NotSupportedException ($"Unsupported RuntimeIdentifier for NativeAOT: {rid}") | ||
| }; |
There was a problem hiding this comment.
Same as above: throwing NotSupportedException for an unexpected RID will crash the task rather than producing a normal MSBuild error. Please log an error (preferably coded/localized) and return false so unsupported RIDs are reported gracefully.
| // 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"); |
There was a problem hiding this comment.
This task logs several user-visible failures via Log.LogError with hard-coded English strings. In this repo, customer-facing build errors are typically emitted via Log.LogCodedError with XA#### codes and localized strings from Properties.Resources. Please switch these errors (sysroot missing, missing CRT/libs, link failure) to coded/localized errors for consistency and localization support.
| Log.LogError ("Could not find sysroot directory containing C++ runtime libraries in runtime pack"); | |
| Log.LogCodedError ("XA5300", "Could not find sysroot directory containing C++ runtime libraries in runtime pack"); |
| // NativeAOT runtime packs don't include the DSO stub because they don't need | ||
| // DSO wrapping. Skip them gracefully instead of failing the build. | ||
| string? packageId = packLibDir.GetMetadata ("NuGetPackageId"); | ||
| if (!String.IsNullOrEmpty (packageId) && packageId.StartsWith ("Microsoft.Android.Runtime.NativeAOT.", StringComparison.OrdinalIgnoreCase)) { |
There was a problem hiding this comment.
Minor consistency: this method already uses the repo's IsNullOrEmpty() extension (e.g., packRID.IsNullOrEmpty()), but the new packageId check uses String.IsNullOrEmpty(). Consider switching to packageId.IsNullOrEmpty() for consistent style and nullable handling.
| if (!String.IsNullOrEmpty (packageId) && packageId.StartsWith ("Microsoft.Android.Runtime.NativeAOT.", StringComparison.OrdinalIgnoreCase)) { | |
| if (!packageId.IsNullOrEmpty () && packageId.StartsWith ("Microsoft.Android.Runtime.NativeAOT.", StringComparison.OrdinalIgnoreCase)) { |
Using the workload pack approach (WIP) from #10704.