Skip to content

[WIP] Validate NativeAOT build without NDK#11077

Open
sbomer wants to merge 20 commits intomainfrom
dev/sbomer/workload-aot-validation
Open

[WIP] Validate NativeAOT build without NDK#11077
sbomer wants to merge 20 commits intomainfrom
dev/sbomer/workload-aot-validation

Conversation

@sbomer
Copy link
Copy Markdown
Member

@sbomer sbomer commented Apr 3, 2026

Using the workload pack approach (WIP) from #10704.

sbomer added 20 commits January 16, 2026 21:11
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
Copilot AI review requested due to automatic review settings April 3, 2026 23:08
@sbomer sbomer added the do-not-merge PR should not be merged. label Apr 3, 2026
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

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 LinkNativeAotLibrary MSBuild task and wire NativeAOT targets to link .o outputs into the final .so via workload-pack ld.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.

Comment on lines +106 to +112
string GetAbiFromRuntimeIdentifier (string rid)
{
return rid switch {
"android-arm64" => "arm64-v8a",
"android-x64" => "x86_64",
_ => throw new NotSupportedException ($"Unsupported RuntimeIdentifier for NativeAOT: {rid}")
};
Copy link

Copilot AI Apr 3, 2026

Choose a reason for hiding this comment

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

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.

Copilot uses AI. Check for mistakes.
Comment on lines +115 to +121
string GetClangArchFromRuntimeIdentifier (string rid)
{
return rid switch {
"android-arm64" => "aarch64",
"android-x64" => "x86_64",
_ => throw new NotSupportedException ($"Unsupported RuntimeIdentifier for NativeAOT: {rid}")
};
Copy link

Copilot AI Apr 3, 2026

Choose a reason for hiding this comment

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

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.

Copilot uses AI. Check for mistakes.
// 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");
Copy link

Copilot AI Apr 3, 2026

Choose a reason for hiding this comment

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

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.

Suggested change
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");

Copilot uses AI. Check for mistakes.
// 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)) {
Copy link

Copilot AI Apr 3, 2026

Choose a reason for hiding this comment

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

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.

Suggested change
if (!String.IsNullOrEmpty (packageId) && packageId.StartsWith ("Microsoft.Android.Runtime.NativeAOT.", StringComparison.OrdinalIgnoreCase)) {
if (!packageId.IsNullOrEmpty () && packageId.StartsWith ("Microsoft.Android.Runtime.NativeAOT.", StringComparison.OrdinalIgnoreCase)) {

Copilot uses AI. Check for mistakes.
@sbomer sbomer self-assigned this Apr 7, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

do-not-merge PR should not be merged.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants