Skip to content

Commit e8265c0

Browse files
sbomerCopilot
andauthored
Add Android NativeAOT integration tests (dotnet#33756)
- Add android-arm64 and android-x64 test cases to PublishNativeAOT and PublishNativeAOTRootAllMauiAssemblies tests - Add PrepareNativeAotBuildPropsAndroid() with Android-specific build properties including ANDROID_NDK_ROOT support - Add ExpectedNativeAOTWarningsAndroid baseline (XA1040 + IL3050 warnings) - Use OnlyAndroid() helper on Linux to avoid iOS/macCatalyst workload issues --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
1 parent 538463e commit e8265c0

5 files changed

Lines changed: 167 additions & 16 deletions

File tree

eng/pipelines/ci.yml

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -281,7 +281,25 @@ stages:
281281
timeout: 120
282282
testCategory: MultiProject
283283

284-
# TODO: macOSTemplates and AOT template categories
284+
# TODO: macOSTemplates category
285+
286+
- name: win_aot_tests
287+
${{ if eq(variables['Build.DefinitionName'], 'maui-pr') }}:
288+
pool: ${{ parameters.WindowsPool.public }}
289+
runAsPublic: true
290+
${{ else }}:
291+
pool: ${{ parameters.WindowsPool.internal }}
292+
runAsPublic: false
293+
timeout: 120
294+
testCategory: AOT
295+
- name: mac_aot_tests
296+
${{ if eq(variables['Build.DefinitionName'], 'maui-pr') }}:
297+
pool: ${{ parameters.MacOSPool.public }}
298+
${{ else }}:
299+
pool: ${{ parameters.MacOSPool.internal }}
300+
timeout: 240
301+
testCategory: AOT
302+
285303
- name: mac_runandroid_tests
286304
${{ if eq(variables['Build.DefinitionName'], 'maui-pr') }}:
287305
pool: ${{ parameters.AndroidPoolLinux }}

src/Controls/src/Build.Tasks/nuget/buildTransitive/netstandard2.0/Microsoft.Maui.Controls.targets

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -358,6 +358,20 @@
358358
</ItemGroup>
359359
</Target>
360360

361+
<!--
362+
Workaround for Android SDK bug: Microsoft.Android.Sdk.ILLink.targets uses %(RootMode) without
363+
fully qualifying it as %(TrimmerRootAssembly.RootMode), which causes MSB4096 when user-defined
364+
TrimmerRootAssembly items don't have the RootMode metadata.
365+
See: https://github.com/dotnet/android/issues/10758
366+
-->
367+
<Target Name="_MauiFixTrimmerRootAssemblyMetadata"
368+
BeforeTargets="PrepareForILLink"
369+
Condition="'$(UsingAndroidNETSdk)' == 'true'">
370+
<ItemGroup>
371+
<TrimmerRootAssembly Update="@(TrimmerRootAssembly)" Condition="'%(TrimmerRootAssembly.RootMode)' == ''" RootMode="All" />
372+
</ItemGroup>
373+
</Target>
374+
361375
<Target Name="_MauiSetWinUIDefaultsForPublishAot" BeforeTargets="PrepareForBuild" Condition="'$(PublishAot)' == 'true' and '$(_MauiTargetPlatformIsWindows)' == 'True'">
362376
<PropertyGroup>
363377
<DebugSymbols Condition="'$(DebugSymbols)' == ''">false</DebugSymbols>

src/Controls/src/Core/Platform/Android/MultiPageFragmentStateAdapter.cs

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,7 @@
77

88
namespace Microsoft.Maui.Controls.Platform
99
{
10-
internal class MultiPageFragmentStateAdapter<[DynamicallyAccessedMembers(BindableProperty.DeclaringTypeMembers
11-
#if NET8_0 // IL2091
12-
| BindableProperty.ReturnTypeMembers
13-
#endif
14-
)] T> : FragmentStateAdapter where T : Page
10+
internal class MultiPageFragmentStateAdapter<[DynamicallyAccessedMembers(BindableProperty.DeclaringTypeMembers | BindableProperty.ReturnTypeMembers)] T> : FragmentStateAdapter where T : Page
1511
{
1612
MultiPage<T> _page;
1713
readonly IMauiContext _context;

src/TestUtils/src/Microsoft.Maui.IntegrationTests/AOTTemplateTest.cs

Lines changed: 89 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
1-
namespace Microsoft.Maui.IntegrationTests;
1+
namespace Microsoft.Maui.IntegrationTests;
22

33
[Trait("Category", "AOT")]
44
public class AOTTemplateTest : BaseTemplateTests
55
{
66
public AOTTemplateTest(IntegrationTestFixture fixture, ITestOutputHelper output) : base(fixture, output) { }
77

88
[Theory]
9+
[InlineData("maui", $"{DotNetCurrent}-android", "android-arm64")]
10+
[InlineData("maui", $"{DotNetCurrent}-android", "android-x64")]
911
[InlineData("maui", $"{DotNetCurrent}-ios", "ios-arm64")]
1012
[InlineData("maui", $"{DotNetCurrent}-ios", "iossimulator-arm64")]
1113
[InlineData("maui", $"{DotNetCurrent}-ios", "iossimulator-x64")]
@@ -18,6 +20,7 @@ public void PublishNativeAOT(string id, string framework, string runtimeIdentifi
1820
SetTestIdentifier(id, framework, runtimeIdentifier);
1921
bool isWindowsFramework = framework.Contains("windows", StringComparison.OrdinalIgnoreCase);
2022
bool isApplePlatform = framework.Contains("ios", StringComparison.OrdinalIgnoreCase) || framework.Contains("maccatalyst", StringComparison.OrdinalIgnoreCase);
23+
bool isAndroidPlatform = framework.Contains("android", StringComparison.OrdinalIgnoreCase);
2124

2225
if (isApplePlatform && !TestEnvironment.IsMacOS)
2326
if (true) return; // Skip: "Publishing a MAUI iOS/macOS app with NativeAOT is only supported on a host MacOS system."
@@ -31,17 +34,41 @@ public void PublishNativeAOT(string id, string framework, string runtimeIdentifi
3134
Assert.True(DotnetInternal.New(id, projectDir, DotNetCurrent, output: _output),
3235
$"Unable to create template {id}. Check test output for errors.");
3336

34-
var extendedBuildProps = isWindowsFramework ? PrepareNativeAotBuildPropsWindows(runtimeIdentifier) : PrepareNativeAotBuildProps();
37+
// For Android-only builds on Linux, modify the csproj to only target Android
38+
// This avoids restore failures due to missing iOS/macCatalyst workloads
39+
if (isAndroidPlatform && !TestEnvironment.IsMacOS && !TestEnvironment.IsWindows)
40+
{
41+
OnlyAndroid(projectFile);
42+
}
43+
44+
var extendedBuildProps = isWindowsFramework
45+
? PrepareNativeAotBuildPropsWindows(runtimeIdentifier)
46+
: isAndroidPlatform
47+
? PrepareNativeAotBuildPropsAndroid()
48+
: PrepareNativeAotBuildProps();
49+
50+
// Disable code signing for Apple platforms (no signing certificate available in CI)
51+
if (isApplePlatform)
52+
{
53+
AddNoCodeSigningProps(extendedBuildProps);
54+
}
3555

3656
string binLogFilePath = $"publish-{DateTime.UtcNow.ToFileTimeUtc()}.binlog";
3757
Assert.True(DotnetInternal.Build(projectFile, "Release", framework: framework, properties: extendedBuildProps, runtimeIdentifier: runtimeIdentifier, binlogPath: binLogFilePath, output: _output),
3858
$"Project {Path.GetFileName(projectFile)} failed to build. Check test output/attachments for errors.");
3959

4060
var actualWarnings = BuildWarningsUtilities.ReadNativeAOTWarningsFromBinLog(binLogFilePath);
41-
actualWarnings.AssertNoWarnings();
61+
var expectedWarnings = isAndroidPlatform
62+
? BuildWarningsUtilities.ExpectedNativeAOTWarningsAndroid
63+
: isWindowsFramework
64+
? BuildWarningsUtilities.ExpectedNativeAOTWarningsWindows
65+
: BuildWarningsUtilities.ExpectedNativeAOTWarnings;
66+
actualWarnings.AssertWarnings(expectedWarnings);
4267
}
4368

4469
[Theory]
70+
[InlineData("maui", $"{DotNetCurrent}-android", "android-arm64")]
71+
[InlineData("maui", $"{DotNetCurrent}-android", "android-x64")]
4572
[InlineData("maui", $"{DotNetCurrent}-ios", "ios-arm64")]
4673
[InlineData("maui", $"{DotNetCurrent}-ios", "iossimulator-arm64")]
4774
[InlineData("maui", $"{DotNetCurrent}-ios", "iossimulator-x64")]
@@ -54,6 +81,7 @@ public void PublishNativeAOTRootAllMauiAssemblies(string id, string framework, s
5481
// This test follows the following guide: https://devblogs.microsoft.com/dotnet/creating-aot-compatible-libraries/#publishing-a-test-application-for-aot
5582
bool isWindowsFramework = framework.Contains("windows", StringComparison.OrdinalIgnoreCase);
5683
bool isApplePlatform = framework.Contains("ios", StringComparison.OrdinalIgnoreCase) || framework.Contains("maccatalyst", StringComparison.OrdinalIgnoreCase);
84+
bool isAndroidPlatform = framework.Contains("android", StringComparison.OrdinalIgnoreCase);
5785

5886
if (isApplePlatform && !TestEnvironment.IsMacOS)
5987
if (true) return; // Skip: "Publishing a MAUI iOS/macOS app with NativeAOT is only supported on a host MacOS system."
@@ -67,7 +95,25 @@ public void PublishNativeAOTRootAllMauiAssemblies(string id, string framework, s
6795
Assert.True(DotnetInternal.New(id, projectDir, DotNetCurrent, output: _output),
6896
$"Unable to create template {id}. Check test output for errors.");
6997

70-
var extendedBuildProps = isWindowsFramework ? PrepareNativeAotBuildPropsWindows(runtimeIdentifier) : PrepareNativeAotBuildProps();
98+
// For Android-only builds on Linux, modify the csproj to only target Android
99+
// This avoids restore failures due to missing iOS/macCatalyst workloads
100+
if (isAndroidPlatform && !TestEnvironment.IsMacOS && !TestEnvironment.IsWindows)
101+
{
102+
OnlyAndroid(projectFile);
103+
}
104+
105+
var extendedBuildProps = isWindowsFramework
106+
? PrepareNativeAotBuildPropsWindows(runtimeIdentifier)
107+
: isAndroidPlatform
108+
? PrepareNativeAotBuildPropsAndroid()
109+
: PrepareNativeAotBuildProps();
110+
111+
// Disable code signing for Apple platforms (no signing certificate available in CI)
112+
if (isApplePlatform)
113+
{
114+
AddNoCodeSigningProps(extendedBuildProps);
115+
}
116+
71117
FileUtilities.ReplaceInFile(projectFile,
72118
"</Project>",
73119
"""
@@ -95,9 +141,11 @@ public void PublishNativeAOTRootAllMauiAssemblies(string id, string framework, s
95141
$"Project {Path.GetFileName(projectFile)} failed to build. Check test output/attachments for errors.");
96142

97143
var actualWarnings = BuildWarningsUtilities.ReadNativeAOTWarningsFromBinLog(binLogFilePath);
98-
var expectedWarnings = isWindowsFramework && BuildWarningsUtilities.ExpectedNativeAOTWarningsWindows != null
99-
? BuildWarningsUtilities.ExpectedNativeAOTWarningsWindows
100-
: BuildWarningsUtilities.ExpectedNativeAOTWarnings;
144+
var expectedWarnings = isAndroidPlatform
145+
? BuildWarningsUtilities.ExpectedNativeAOTWarningsAndroid
146+
: isWindowsFramework
147+
? BuildWarningsUtilities.ExpectedNativeAOTWarningsWindows
148+
: BuildWarningsUtilities.ExpectedNativeAOTWarnings;
101149
actualWarnings.AssertWarnings(expectedWarnings);
102150
}
103151

@@ -109,8 +157,7 @@ private List<string> PrepareNativeAotBuildProps()
109157
"PublishAotUsingRuntimePack=true", // TODO: This parameter will become obsolete https://github.com/dotnet/runtime/issues/87060 in net9
110158
"_IsPublishing=true", // This makes 'dotnet build -r iossimulator-x64' equivalent to 'dotnet publish -r iossimulator-x64'
111159
"IlcTreatWarningsAsErrors=false",
112-
"TrimmerSingleWarn=false",
113-
"_RequireCodeSigning=false" // This is required to build the iOS app without a signing key
160+
"TrimmerSingleWarn=false"
114161
};
115162
return extendedBuildProps;
116163
}
@@ -132,4 +179,37 @@ private List<string> PrepareNativeAotBuildPropsWindows(string runtimeIdentifier)
132179
};
133180
return extendedBuildProps;
134181
}
182+
183+
private List<string> PrepareNativeAotBuildPropsAndroid()
184+
{
185+
var extendedBuildProps = new List<string>(BuildProps)
186+
{
187+
"PublishAot=true",
188+
"PublishAotUsingRuntimePack=true",
189+
"_IsPublishing=true",
190+
"IlcTreatWarningsAsErrors=false",
191+
"TrimmerSingleWarn=false"
192+
};
193+
194+
var ndkRoot = Environment.GetEnvironmentVariable("ANDROID_NDK_ROOT");
195+
if (!string.IsNullOrEmpty(ndkRoot))
196+
{
197+
// Quote and escape the NDK path to avoid argument splitting when it contains spaces.
198+
var ndkRootEscaped = ndkRoot.Replace("\"", "\\\"", StringComparison.Ordinal);
199+
extendedBuildProps.Add($"AndroidNdkDirectory=\"{ndkRootEscaped}\"");
200+
}
201+
202+
return extendedBuildProps;
203+
}
204+
205+
/// <summary>
206+
/// Adds properties to disable code signing for Apple platforms.
207+
/// This is required when building without a signing certificate (e.g., in CI environments).
208+
/// </summary>
209+
private static void AddNoCodeSigningProps(List<string> buildProps)
210+
{
211+
buildProps.Add("EnableCodeSigning=false");
212+
buildProps.Add("_RequireCodeSigning=false");
213+
}
214+
135215
}

src/TestUtils/src/Microsoft.Maui.IntegrationTests/Utilities/BuildWarningsUtilities.cs

Lines changed: 44 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
using System.IO;
1+
using System.IO;
22
using System.Linq;
33
using Microsoft.Build.Framework;
44
using Microsoft.Build.Logging.StructuredLogger;
@@ -172,6 +172,44 @@ public static void AssertWarnings(this List<WarningsPerFile> actualWarnings, Lis
172172
// These might be different from iOS/Mac warnings due to platform-specific implementations
173173
private static readonly List<WarningsPerFile> expectedNativeAOTWarningsWindows = new();
174174

175+
// Android baseline warnings to ensure no new warnings are introduced
176+
private static readonly List<WarningsPerFile> expectedNativeAOTWarningsAndroid = new()
177+
{
178+
new WarningsPerFile
179+
{
180+
File = "Xamarin.Android.Common.targets",
181+
WarningsPerCode = new List<WarningsPerCode>
182+
{
183+
new WarningsPerCode
184+
{
185+
Code = "XA1040",
186+
Messages = new List<string>
187+
{
188+
"The NativeAOT runtime on Android is an experimental feature and not yet suitable for production use. File issues at: https://github.com/dotnet/android/issues",
189+
}
190+
},
191+
}
192+
},
193+
new WarningsPerFile
194+
{
195+
File = "ILC",
196+
WarningsPerCode = new List<WarningsPerCode>
197+
{
198+
new WarningsPerCode
199+
{
200+
Code = "IL3050",
201+
Messages = new List<string>
202+
{
203+
"Microsoft.Android.Runtime.ManagedTypeManager.<GetInvokerTypeCore>g__MakeGenericType|4_1(Type,Type[]): Using member 'System.Type.MakeGenericType(Type[])' which has 'RequiresDynamicCodeAttribute' can break functionality when AOT compiling. The native code for this instantiation might not be available at runtime.",
204+
"Android.Runtime.JNIEnv.MakeArrayType(Type): Using member 'System.Type.MakeArrayType()' which has 'RequiresDynamicCodeAttribute' can break functionality when AOT compiling. The code for an array of the specified type might not be available.",
205+
"Android.Runtime.JNINativeWrapper.CreateDelegate(Delegate): Using member 'System.Reflection.Emit.DynamicMethod.DynamicMethod(String,Type,Type[],Type,Boolean)' which has 'RequiresDynamicCodeAttribute' can break functionality when AOT compiling. Creating a DynamicMethod requires dynamic code.",
206+
"Java.Interop.JavaConvert.<GetJniHandleConverter>g__MakeGenericType|2_0(Type,Type[]): Using member 'System.Type.MakeGenericType(Type[])' which has 'RequiresDynamicCodeAttribute' can break functionality when AOT compiling. The native code for this instantiation might not be available at runtime.",
207+
}
208+
},
209+
}
210+
},
211+
};
212+
175213
public static List<WarningsPerFile> ExpectedNativeAOTWarnings
176214
{
177215
get => expectedNativeAOTWarnings;
@@ -182,6 +220,11 @@ public static List<WarningsPerFile> ExpectedNativeAOTWarningsWindows
182220
get => expectedNativeAOTWarningsWindows;
183221
}
184222

223+
public static List<WarningsPerFile> ExpectedNativeAOTWarningsAndroid
224+
{
225+
get => expectedNativeAOTWarningsAndroid;
226+
}
227+
185228
#region Utility methods for generating the list of expected warnings
186229

187230
// Use this method to regenerate warnings found in a .binlog file at 'binLogFilePath'.

0 commit comments

Comments
 (0)