diff --git a/build-tools/automation/yaml-templates/stage-package-tests.yaml b/build-tools/automation/yaml-templates/stage-package-tests.yaml index 869ac45cee1..50e1102f255 100644 --- a/build-tools/automation/yaml-templates/stage-package-tests.yaml +++ b/build-tools/automation/yaml-templates/stage-package-tests.yaml @@ -213,6 +213,19 @@ stages: artifactSource: bin/Test$(XA.Build.Configuration)/$(DotNetTargetFramework)-android/Mono.Android.NET_Tests-Signed.aab artifactFolder: $(DotNetTargetFramework)-NativeAOT + - template: /build-tools/automation/yaml-templates/apk-instrumentation.yaml + parameters: + configuration: $(XA.Build.Configuration) + testName: Mono.Android.NET_Tests-NativeAOTWorkload + project: tests/Mono.Android-Tests/Mono.Android-Tests/Mono.Android.NET-Tests.csproj + testResultsFiles: TestResult-Mono.Android.NET_Tests-$(XA.Build.Configuration)NativeAOTWorkload.xml + extraBuildArgs: >- + -p:TestsFlavor=NativeAOTWorkload + -p:PublishAot=true + -p:AndroidNativeAotUseNdk=false + artifactSource: bin/Test$(XA.Build.Configuration)/$(DotNetTargetFramework)-android/Mono.Android.NET_Tests-Signed.aab + artifactFolder: $(DotNetTargetFramework)-NativeAOTWorkload + - template: /build-tools/automation/yaml-templates/apk-instrumentation.yaml parameters: configuration: $(XA.Build.Configuration) 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/build-tools/xaprepare/xaprepare/Application/AndroidToolchainComponent.cs b/build-tools/xaprepare/xaprepare/Application/AndroidToolchainComponent.cs index a03baee3342..57e00cd55d5 100644 --- a/build-tools/xaprepare/xaprepare/Application/AndroidToolchainComponent.cs +++ b/build-tools/xaprepare/xaprepare/Application/AndroidToolchainComponent.cs @@ -63,7 +63,8 @@ enum AndroidToolchainComponentType CoreDependency = 0, BuildDependency = 1 << 0, EmulatorDependency = 1 << 1, - All = CoreDependency | BuildDependency | EmulatorDependency, + NdkDependency = 1 << 2, + All = CoreDependency | BuildDependency | EmulatorDependency | NdkDependency, } } diff --git a/build-tools/xaprepare/xaprepare/ConfigAndData/Dependencies/AndroidToolchain.cs b/build-tools/xaprepare/xaprepare/ConfigAndData/Dependencies/AndroidToolchain.cs index 1baf6711b26..fbc40e68cd3 100644 --- a/build-tools/xaprepare/xaprepare/ConfigAndData/Dependencies/AndroidToolchain.cs +++ b/build-tools/xaprepare/xaprepare/ConfigAndData/Dependencies/AndroidToolchain.cs @@ -104,6 +104,7 @@ public AndroidToolchain () new AndroidToolchainComponent ($"android-ndk-r{AndroidNdkVersion}-{osTag}", destDir: AndroidNdkDirectory, pkgRevision: AndroidPkgRevision, + dependencyType: AndroidToolchainComponentType.NdkDependency, buildToolName: $"android-ndk-r{AndroidNdkVersion}", buildToolVersion: AndroidPkgRevision ), diff --git a/build-tools/xaprepare/xaprepare/Steps/Step_Android_SDK_NDK.cs b/build-tools/xaprepare/xaprepare/Steps/Step_Android_SDK_NDK.cs index d987b23c878..667a10599ff 100644 --- a/build-tools/xaprepare/xaprepare/Steps/Step_Android_SDK_NDK.cs +++ b/build-tools/xaprepare/xaprepare/Steps/Step_Android_SDK_NDK.cs @@ -171,6 +171,11 @@ bool AcceptLicenses (Context context, string sdkRoot) bool GatherNDKInfo (Context context) { + if (!DependencyTypeToInstall.HasFlag (AndroidToolchainComponentType.NdkDependency)) { + Log.StatusLine ("NDK not requested, skipping NDK info gathering"); + return true; + } + if (!CopyRedistributableFiles (context)) { return false; } 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 0d0a212b938..27e6e2a693d 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 @@ -31,8 +31,11 @@ _ResolveAssemblies MSBuild target. - - <_RunAotMaybe Condition=" '$(_AndroidUseMarshalMethods)' != 'True' ">_AndroidAot + + <_RunAotMaybe Condition=" '$(_AndroidUseMarshalMethods)' != 'True' and '$(_AndroidRuntime)' != 'NativeAOT' ">_AndroidAot 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 b4ecd0f5fe6..740e0aae21f 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 @@ -3,6 +3,13 @@ Microsoft.Android.Sdk.NativeAOT.targets This file contains the NativeAOT-specific MSBuild logic for .NET for Android. + +We follow an approach similar to xamarin-macios: +1. Set NativeCompilationDuringPublish=false to skip ILC SDK's native linking +2. Set NativeLib=Static so ILC produces only object files +3. Use our own LinkNativeAotLibrary task with android-native-tools (ld.lld) + +This eliminates the NDK dependency for NativeAOT builds. *********************************************************************************************** --> @@ -10,6 +17,8 @@ This file contains the NativeAOT-specific MSBuild logic for .NET for Android. + + @@ -25,6 +34,38 @@ This file contains the NativeAOT-specific MSBuild logic for .NET for Android. <_GenerateProguardAfterTargets>_AndroidComputeIlcCompileInputs + + + false + + + true + + + true + + + false + + + <_RunAotMaybe Condition=" '$(_AndroidUseMarshalMethods)' != 'True' ">_AndroidNativeAotCompileAndLink @@ -46,73 +87,47 @@ This file contains the NativeAOT-specific MSBuild logic for .NET for Android. /> - + _AndroidBeforeIlcCompile; + Compile; SetupOSSpecificProps; + _AndroidAfterSetupOSSpecificProps; PrepareForILLink; ILLink; ComputeIlcCompileInputs; _AndroidComputeIlcCompileInputs; - $(IlcCompileDependsOn) - - <_NdkAbi Condition=" '$(RuntimeIdentifier)' == 'android-arm64' ">aarch64 - <_NdkAbi Condition=" '$(RuntimeIdentifier)' == 'android-x64' ">x86_64 - <_NDKApiLevel Condition=" '$(RuntimeIdentifier)' == 'android-arm64' ">$(AndroidNdkApiLevel_Arm64) - <_NDKApiLevel Condition=" '$(RuntimeIdentifier)' == 'android-x64' ">$(AndroidNdkAPiLevel_X64) - <_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 - - - <_NdkWrapperScriptExt Condition=" $([MSBuild]::IsOSPlatform('windows')) ">.cmd - <_NdkSysrootDir>$(_AndroidNdkDirectory)toolchains/llvm/prebuilt/$(_NdkPrebuiltAbi)/sysroot/usr/lib/$(_NdkSysrootAbi)/ - <_NdkBinDir>$(_AndroidNdkDirectory)toolchains/llvm/prebuilt/$(_NdkPrebuiltAbi)/bin/ - - $(_NdkAbi)-linux-android$(_NDKApiLevel)-clang$(_NdkWrapperScriptExt) - $(_NdkAbi)-linux-android$(_NDKApiLevel)-clang$(_NdkWrapperScriptExt) - llvm-objcopy + Static false - - true + + <_AndroidNativeAotStripSymbols Condition=" '$(_AndroidNativeAotStripSymbols)' == '' ">$(StripSymbols) + false + <_ExtraTrimmerArgs>$(_ExtraTrimmerArgs) --notrimwarn + - + + + + $(_AndroidNativeAotStripSymbols) + @@ -132,11 +155,6 @@ 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)" /> + - + + + + + + <_NativeAotSharedLibsToRemove Include="@(ResolvedFileToPublish)" + Condition=" '%(ResolvedFileToPublish.Extension)' == '.so' and $([System.String]::new('%(ResolvedFileToPublish.NuGetPackageId)').StartsWith('Microsoft.NETCore.App.Runtime.NativeAOT.')) " /> + + - <_NdkLibs Include="@(RuntimePackAsset->WithMetadataValue('Filename', 'libnaot-android.$(Configuration.ToLower())-static-$(Configuration.ToLower())'))" /> - - - <_NdkLibs Include="$(_NdkSysrootDir)libc++_static.a" /> - <_NdkLibs Include="$(_NdkSysrootDir)libc++abi.a" /> - - - @@ -243,19 +273,219 @@ 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="_CollectNativeAotLinkInputs" + 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 + <_NdkApiLevel Condition=" '$(RuntimeIdentifier)' == 'android-arm64' ">$(AndroidNdkApiLevel_Arm64) + <_NdkApiLevel Condition=" '$(RuntimeIdentifier)' == 'android-x64' ">$(AndroidNdkApiLevel_X64) + <_NdkSysrootDir>$(_AndroidNdkDirectory)toolchains/llvm/prebuilt/$(_NdkPrebuiltAbi)/sysroot/usr/lib/$(_NdkSysrootAbi)/ + <_NdkBinDir>$(_AndroidNdkDirectory)toolchains/llvm/prebuilt/$(_NdkPrebuiltAbi)/bin/ + + <_NdkWrapperScriptExt Condition=" $([MSBuild]::IsOSPlatform('windows')) ">.cmd + + <_NdkClang>$(_NdkBinDir)$(_NdkSysrootAbi)$(_NdkApiLevel)-clang++$(_NdkWrapperScriptExt) + <_NativeAotOutputLibrary>$(NativeOutputPath)lib$(TargetName).so + <_NdkLinkRspFile>$(NativeIntermediateOutputPath)ndk-link.rsp + + + + + <_NdkLibs Include="$(_NdkSysrootDir)libc++_static.a" /> + <_NdkLibs Include="$(_NdkSysrootDir)libc++abi.a" /> + + + + + <_NdkLinkerArgs Include="-shared" /> + + <_NdkLinkerArgs Include="-nostdlib++" /> + <_NdkLinkerArgs Include="-o "$(_NativeAotOutputLibrary)"" /> + <_NdkLinkerArgs Include="-Wl,-soname,$([System.IO.Path]::GetFileName('$(_NativeAotOutputLibrary)'))" /> + <_NdkLinkerArgs Include="-Wl,--build-id=sha1" /> + <_NdkLinkerArgs Include="-Wl,--eh-frame-hdr" /> + <_NdkLinkerArgs Include="-Wl,-z,relro" /> + <_NdkLinkerArgs Include="-Wl,-z,now" /> + <_NdkLinkerArgs Include="-Wl,-z,max-page-size=16384" /> + <_NdkLinkerArgs Include="-Wl,-z,nostart-stop-gc" /> + <_NdkLinkerArgs Include="-Wl,--gc-sections" /> + <_NdkLinkerArgs Include="-Wl,--as-needed" /> + <_NdkLinkerArgs Include="-Wl,--no-undefined" /> + <_NdkLinkerArgs Include="-Wl,-Bsymbolic" /> + <_NdkLinkerArgs Condition=" '$(StripSymbols)' == 'true' " Include="-Wl,-s" /> + <_NdkLinkerArgs Condition=" '$(NativeDebugSymbols)' == 'true' and '$(StripSymbols)' != 'true' " Include="-g" /> + + + <_NdkLinkerArgs Include=""$(NativeObject)"" /> + + <_NdkLinkerArgs Include="@(_NativeAotObjectFiles->'"%(Identity)"')" /> + + <_NdkLinkerArgs Include="@(_NativeAotArchives->'"%(Identity)"')" /> + + <_NdkLinkerArgs Include="@(_NdkLibs->'"%(Identity)"')" /> + + <_NdkLinkerArgs Include="-llog" /> + <_NdkLinkerArgs Include="-lz" /> + <_NdkLinkerArgs Include="-lm" /> + <_NdkLinkerArgs Include="-ldl" /> + + + + + + + + + + + + lib$(TargetName).so + lib$(TargetName).so + PreserveNewest + + + + + + + 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..47661122627 --- /dev/null +++ b/src/Xamarin.Android.Build.Tasks/Tasks/LinkNativeAotLibrary.cs @@ -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; + +/// +/// 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); + + // 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 linkItems = OrganizeCommandLineItems (abi, sysrootDir, clangArch); + List linkStartFiles = GetCrtStartFiles (abi, sysrootDir); + List 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}") + }; + } + + /// + /// 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/Tests/Xamarin.Android.Build.Tests/BuildTest2.cs b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/BuildTest2.cs index 759d6a2f855..27dd590f6ab 100644 --- a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/BuildTest2.cs +++ b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/BuildTest2.cs @@ -277,6 +277,42 @@ void AssertTypeMap(string javaName, string managedName) } } + [Test] + public void NativeAOT_WorkloadPack () + { + var proj = new XamarinAndroidApplicationProject { + IsRelease = true, + ProjectName = "HelloNdkFree", + }; + proj.SetRuntime (AndroidRuntime.NativeAOT, useNdkForNativeAot: false); + + using var b = CreateApkBuilder (); + b.SuppressNdkInjection = true; + Assert.IsTrue (b.Build (proj), "Build should have succeeded."); + + // Verify the workload pack linker was used, not the NDK + Assert.IsTrue (StringAssertEx.ContainsText (b.LastBuildOutput, + "NativeAOT linking used workload pack"), + "Build output should confirm workload pack linker was used"); + Assert.IsFalse (StringAssertEx.ContainsText (b.LastBuildOutput, + "NativeAOT linking used NDK clang"), + "Build output should NOT mention NDK clang linker"); + + // Verify NativeAOT .so files are in the APK + string [] nativeaot_files = [ + $"lib/arm64-v8a/lib{proj.ProjectName}.so", + $"lib/x86_64/lib{proj.ProjectName}.so", + ]; + + var output = Path.Combine (Root, b.ProjectDirectory, proj.OutputPath); + var apkFile = Path.Combine (output, $"{proj.PackageName}-Signed.apk"); + FileAssert.Exists (apkFile); + using var zip = ZipHelper.OpenZip (apkFile); + foreach (var nativeaot_file in nativeaot_files) { + Assert.IsTrue (zip.ContainsEntry (nativeaot_file, caseSensitive: true), $"APK must contain `{nativeaot_file}`."); + } + } + [Test] public void BuildBasicApplicationThenMoveIt ([Values] bool isRelease, [Values] AndroidRuntime runtime) { diff --git a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Utilities/ProjectExtensions.cs b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Utilities/ProjectExtensions.cs index a3cac4affef..a4ca6aeb76e 100644 --- a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Utilities/ProjectExtensions.cs +++ b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Utilities/ProjectExtensions.cs @@ -18,12 +18,22 @@ public static void SetRuntime (this XamarinProject project, AndroidRuntime runti } public static void SetRuntime (this XamarinAndroidApplicationProject project, AndroidRuntime runtime) + { + SetRuntime (project, runtime, useNdkForNativeAot: true); + } + + public static void SetRuntime (this XamarinAndroidApplicationProject project, AndroidRuntime runtime, bool useNdkForNativeAot) { if (runtime != AndroidRuntime.NativeAOT) { DoSetRuntime (project, runtime); return; } - project.SetPublishAot (true, BaseTest.AndroidNdkPath); + if (useNdkForNativeAot) { + project.SetPublishAot (true, BaseTest.AndroidNdkPath); + } else { + project.SetPublishAot (true); + project.SetProperty ("AndroidNativeAotUseNdk", "false"); + } EnablePreviewFeaturesIfNeeded (project, runtime); } diff --git a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.ProjectTools/Android/XamarinAndroidApplicationProject.cs b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.ProjectTools/Android/XamarinAndroidApplicationProject.cs index 67d982fb0d9..4c5331cb52e 100644 --- a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.ProjectTools/Android/XamarinAndroidApplicationProject.cs +++ b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.ProjectTools/Android/XamarinAndroidApplicationProject.cs @@ -207,6 +207,17 @@ public void SetPublishAot (bool value, string androidNdkPath) SetProperty ("AndroidNdkDirectory", androidNdkPath); } + /// + /// Sets properties required for $(PublishAot)=true without requiring the NDK. + /// Used for NativeAOT builds that use the workload pack instead of the NDK. + /// + public void SetPublishAot (bool value) + { + if (value) + IsRelease = true; + PublishAot = value; + } + public string AndroidManifest { get; set; } public string LayoutMain { get; set; } public string MainActivity { get; set; } diff --git a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.ProjectTools/Common/Builder.cs b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.ProjectTools/Common/Builder.cs index 753f6d7a9fa..a3a1b266999 100644 --- a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.ProjectTools/Common/Builder.cs +++ b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.ProjectTools/Common/Builder.cs @@ -104,6 +104,12 @@ public IEnumerable LastBuildOutput { /// public bool AutomaticNuGetRestore { get; set; } = true; + /// + /// When true, suppresses the automatic injection of /p:AndroidNdkDirectory into MSBuild invocations. + /// Used for NativeAOT workload pack tests that must not use the NDK. + /// + public bool SuppressNdkInjection { get; set; } + /// /// Checks whether cross-compilers are available for the specified Android ABIs. /// @@ -276,7 +282,7 @@ protected bool BuildInternal (string projectOrSolution, string target, string [] sw.WriteLine ("/p:AndroidSdkDirectory=\"{0}\"", sdkPath); } string ndkPath = AndroidSdkResolver.GetAndroidNdkPath (); - if (Directory.Exists (ndkPath)) { + if (!SuppressNdkInjection && Directory.Exists (ndkPath)) { sw.WriteLine ("/p:AndroidNdkDirectory=\"{0}\"", ndkPath); } string jdkPath = AndroidSdkResolver.GetJavaSdkPath (); diff --git a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.ProjectTools/Common/DotNetCLI.cs b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.ProjectTools/Common/DotNetCLI.cs index 9f4ebc9a1e0..bd7944bdcf1 100644 --- a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.ProjectTools/Common/DotNetCLI.cs +++ b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.ProjectTools/Common/DotNetCLI.cs @@ -19,6 +19,12 @@ public class DotNetCLI public string JavaSdkPath { get; set; } = AndroidSdkResolver.GetJavaSdkPath (); public string ProjectDirectory { get; set; } + /// + /// When true, suppresses the automatic injection of /p:AndroidNdkDirectory into dotnet build invocations. + /// Used for NativeAOT workload pack tests that must not use the NDK. + /// + public bool SuppressNdkInjection { get; set; } + readonly string projectOrSolution; public DotNetCLI (string projectOrSolution) @@ -236,7 +242,7 @@ List GetDefaultCommandLineArgs (string verb, string target = null, strin if (Directory.Exists (AndroidSdkPath)) { arguments.Add ($"/p:AndroidSdkDirectory=\"{AndroidSdkPath.TrimEnd('\\')}\""); } - if (Directory.Exists (AndroidNdkPath)) { + if (!SuppressNdkInjection && Directory.Exists (AndroidNdkPath)) { arguments.Add ($"/p:AndroidNdkDirectory=\"{AndroidNdkPath.TrimEnd('\\')}\""); } if (Directory.Exists (JavaSdkPath)) { 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 diff --git a/src/Xamarin.Android.Build.Tasks/Utilities/DSOWrapperGenerator.cs b/src/Xamarin.Android.Build.Tasks/Utilities/DSOWrapperGenerator.cs index ff584246f61..fc10969a56c 100644 --- a/src/Xamarin.Android.Build.Tasks/Utilities/DSOWrapperGenerator.cs +++ b/src/Xamarin.Android.Build.Tasks/Utilities/DSOWrapperGenerator.cs @@ -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}"); } diff --git a/src/Xamarin.Android.Build.Tasks/Utilities/NativeLinker.cs b/src/Xamarin.Android.Build.Tasks/Utilities/NativeLinker.cs index ac53cfebd11..8b5c04d767a 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; @@ -65,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; @@ -155,14 +156,20 @@ public bool Link (ITaskItem outputLibraryPath, List linkItems, List + <_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)"