Skip to content
Draft
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
a1c7ecc
Use android-native-tools for NativeAOT linking instead of Android NDK
sbomer Jan 17, 2026
b9bb3ce
Add --eh-frame-hdr linker flag for NativeAOT builds
sbomer Jan 20, 2026
2395d8f
Fix NativeAOT linking for projects with spaces in names
sbomer Jan 21, 2026
d2c6f45
Remove sysroot libraries from NativeAOT publish output
sbomer Feb 2, 2026
27b43fa
Merge origin/main into workload-aot
sbomer Feb 2, 2026
2a742d0
Copy CRT object files to NativeAOT runtime pack
sbomer Feb 3, 2026
844cb82
Skip runtime pack directories without DSO stub instead of throwing
sbomer Feb 12, 2026
1559887
Merge remote-tracking branch 'origin/main' into workload-aot
sbomer Feb 17, 2026
fdbf242
Fix NativeAOT build failure when llvm-objcopy is not in PATH
sbomer Feb 18, 2026
885a2aa
Merge remote-tracking branch 'origin/main' into workload-aot
sbomer Feb 18, 2026
d912268
Fix _TouchAndroidLinkFlag skipping on NativeAOT incremental builds
sbomer Feb 20, 2026
9b4279e
Exclude NativeAOT runtime pack .so shims from APK packaging
sbomer Feb 21, 2026
35053c5
Merge branch 'main' into dev/sbomer/workload-aot
sbomer Mar 5, 2026
faa097c
Fix NativeAOT build failure on Windows due to llvm-objcopy probe
sbomer Mar 6, 2026
0231cc5
Fix review issues in NativeAOT linking: soname, NDK clang, DSOWrapper…
sbomer Mar 8, 2026
56c7793
Merge remote-tracking branch 'origin/main' into workload-aot
sbomer Mar 11, 2026
16c44e9
Fix legacy NDK path: invoke clang++ directly instead of relying on IL…
sbomer Mar 11, 2026
dbe5b72
Merge remote-tracking branch 'origin/main' into workload-aot
sbomer Mar 19, 2026
3d86857
Merge branch 'main' into dev/sbomer/workload-aot
sbomer Mar 20, 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
14 changes: 14 additions & 0 deletions build-tools/create-packs/Microsoft.Android.Runtime.proj
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,20 @@ projects that use the Microsoft.Android.Runtimes framework in .NET 6+.
</ItemGroup>

<ItemGroup Condition=" '$(AndroidRuntime)' == 'NativeAOT' ">
<!-- Include sysroot libraries for NativeAOT linking -->
<NativeRuntimeAsset Include="$(NativeRuntimeOutputRootDir)$(_RuntimeRedistDirName)\$(AndroidRID)\libc.so" />
<NativeRuntimeAsset Include="$(NativeRuntimeOutputRootDir)$(_RuntimeRedistDirName)\$(AndroidRID)\libdl.so" />
<NativeRuntimeAsset Include="$(NativeRuntimeOutputRootDir)$(_RuntimeRedistDirName)\$(AndroidRID)\liblog.so" />
<NativeRuntimeAsset Include="$(NativeRuntimeOutputRootDir)$(_RuntimeRedistDirName)\$(AndroidRID)\libm.so" />
<NativeRuntimeAsset Include="$(NativeRuntimeOutputRootDir)$(_RuntimeRedistDirName)\$(AndroidRID)\libz.so" />
<!-- C++ runtime and compiler builtins for NativeAOT -->
<NativeRuntimeAsset Include="$(NativeRuntimeOutputRootDir)$(_RuntimeRedistDirName)\$(AndroidRID)\crtbegin_so.o" />
<NativeRuntimeAsset Include="$(NativeRuntimeOutputRootDir)$(_RuntimeRedistDirName)\$(AndroidRID)\crtend_so.o" />
<NativeRuntimeAsset Include="$(NativeRuntimeOutputRootDir)$(_RuntimeRedistDirName)\$(AndroidRID)\libc++_static.a" />
<NativeRuntimeAsset Include="$(NativeRuntimeOutputRootDir)$(_RuntimeRedistDirName)\$(AndroidRID)\libc++abi.a" />
<NativeRuntimeAsset Include="$(NativeRuntimeOutputRootDir)$(_RuntimeRedistDirName)\$(AndroidRID)\libclang_rt.builtins-$(_ClangArch)-android.a" />
<NativeRuntimeAsset Include="$(NativeRuntimeOutputRootDir)$(_RuntimeRedistDirName)\$(AndroidRID)\libunwind.a" />
<!-- NativeAOT runtime libraries -->
<NativeRuntimeAsset Condition=" Exists('$(NativeRuntimeOutputRootDir)$(_RuntimeFlavorDirName)\$(AndroidRID)\libnaot-android.debug-static-debug.a') " Include="$(NativeRuntimeOutputRootDir)$(_RuntimeFlavorDirName)\$(AndroidRID)\libnaot-android.debug-static-debug.a" />
<NativeRuntimeAsset Condition=" Exists('$(NativeRuntimeOutputRootDir)$(_RuntimeFlavorDirName)\$(AndroidRID)\libnaot-android.release-static-release.a') " Include="$(NativeRuntimeOutputRootDir)$(_RuntimeFlavorDirName)\$(AndroidRID)\libnaot-android.release-static-release.a" />
<_AndroidRuntimePackAssemblies Include="$(_MonoAndroidNETOutputRoot)$(AndroidLatestStableApiLevel)\Microsoft.Android.Runtime.NativeAOT.dll" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,11 @@ _ResolveAssemblies MSBuild target.
</PropertyGroup>

<PropertyGroup>
<!-- When marshal methods are enabled, AOT needs to run after the GenerateJavaStubs task -->
<_RunAotMaybe Condition=" '$(_AndroidUseMarshalMethods)' != 'True' ">_AndroidAot</_RunAotMaybe>
<!--
When marshal methods are enabled, AOT needs to run after the GenerateJavaStubs task.
For NativeAOT, _RunAotMaybe is set in Microsoft.Android.Sdk.NativeAOT.targets.
-->
<_RunAotMaybe Condition=" '$(_AndroidUseMarshalMethods)' != 'True' and '$(_AndroidRuntime)' != 'NativeAOT' ">_AndroidAot</_RunAotMaybe>
</PropertyGroup>

<Target Name="_ComputeFilesToPublishForRuntimeIdentifiers"
Expand Down Expand Up @@ -123,7 +126,6 @@ _ResolveAssemblies MSBuild target.
are never taken into consideration in any context.
-->
<ProcessRuntimePackLibraryDirectories
Condition=" '$(_AndroidRuntime)' != 'NativeAOT' "
ResolvedFilesToPublish="@(ResolvedFileToPublish)">
<Output TaskParameter="RuntimePackLibraryDirectories" ItemName="_RuntimePackLibraryDirectory" />
<Output TaskParameter="NativeLibrariesToRemove" ItemName="_NativeLibraryToRemove" />
Expand Down

Large diffs are not rendered by default.

242 changes: 242 additions & 0 deletions src/Xamarin.Android.Build.Tasks/Tasks/LinkNativeAotLibrary.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,242 @@
#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;

/// <summary>
/// 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.
/// </summary>
public class LinkNativeAotLibrary : AndroidTask
{
public override string TaskPrefix => "LNA";

[Required]
public string AndroidBinUtilsDirectory { get; set; } = "";

[Required]
public string IntermediateOutputPath { get; set; } = "";

/// <summary>
/// The main object file produced by ILC ($(NativeObject)).
/// </summary>
[Required]
public string NativeObject { get; set; } = "";

/// <summary>
/// Additional object files (e.g., generated assembler sources).
/// </summary>
public ITaskItem[] NativeObjectFiles { get; set; } = [];

/// <summary>
/// Static archives from ILC SDK and runtime packs.
/// </summary>
[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);

// Use the full filename as soname (e.g., "libMyApp.so") following ELF/Android conventions
string soname = Path.GetFileName (OutputLibrary);

// 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 uses its own runtime, not CoreCLR
UseSymbolic = true,
IsNativeAOT = true, // Enable NativeAOT-specific linker flags
};

List<ITaskItem> linkItems = OrganizeCommandLineItems (abi, sysrootDir, clangArch);
List<ITaskItem> linkStartFiles = GetCrtStartFiles (abi, sysrootDir);
List<ITaskItem> linkEndFiles = GetCrtEndFiles (abi, sysrootDir);

// If required files were missing (errors logged above), don't invoke the linker
// with incomplete inputs — it would produce confusing secondary errors.
if (Log.HasLoggedErrors) {
return false;
}

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}")
};
}

/// <summary>
/// Finds the sysroot directory containing C++ runtime libraries.
/// </summary>
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;
}

/// <summary>
/// Get CRT start files (crtbegin_so.o).
/// </summary>
List<ITaskItem> GetCrtStartFiles (string abi, string sysrootDir)
{
var items = new List<ITaskItem> ();
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;
}

/// <summary>
/// Get CRT end files (crtend_so.o).
/// </summary>
List<ITaskItem> GetCrtEndFiles (string abi, string sysrootDir)
{
var items = new List<ITaskItem> ();
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;
}

/// <summary>
/// Organizes link items in the correct order for the native linker.
/// Order matters for static linking!
/// </summary>
List<ITaskItem> OrganizeCommandLineItems (string abi, string sysrootDir, string clangArch)
{
var items = new List<ITaskItem> ();

// 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;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -44,5 +44,5 @@
"Size": 1904
}
},
"PackageSize": 2094050
"PackageSize": 2225122
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
"Size": 6740
},
"classes.dex": {
"Size": 9136980
"Size": 9132020
},
"kotlin/annotation/annotation.kotlin_builtins": {
"Size": 928
Expand All @@ -29,7 +29,7 @@
"Size": 2396
},
"lib/arm64-v8a/libUnnamedProject.so": {
"Size": 21592848
"Size": 22047616
},
"META-INF/androidx.activity_activity.version": {
"Size": 6
Expand Down Expand Up @@ -182,7 +182,7 @@
"Size": 6
},
"META-INF/BNDLTOOL.RSA": {
"Size": 1223
"Size": 1213
},
"META-INF/BNDLTOOL.SF": {
"Size": 89178
Expand Down Expand Up @@ -2252,5 +2252,5 @@
"Size": 812848
}
},
"PackageSize": 12521545
"PackageSize": 12652617
}
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,14 @@ public static Config GetConfig (TaskLoggingHelper log, string androidBinUtilsDir

string stubPath = Path.Combine (packLibDir.ItemSpec, StubFileName);
if (!File.Exists (stubPath)) {
// 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)) {
log.LogDebugMessage ($"Skipping NativeAOT runtime pack directory '{packLibDir.ItemSpec}': no DSO stub needed");
continue;
}

throw new InvalidOperationException ($"Internal error: archive DSO stub file '{stubPath}' does not exist in runtime pack at {packLibDir}");
}

Expand Down
Loading