From a58aca9ddb5e336eb6da7ce15055b4af9384f80f Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Wed, 11 Nov 2026 01:11:00 +0100 Subject: [PATCH] [Xamarin.Android.Build.Tasks] Move GetAndroidDefineConstants to Microsoft.Android.Build.Tasks Context: #13460 `` turns `$(MonoAndroidVersion)` into a `__XAMARIN_ANDROID_*__` define by replacing every non-alphanumeric character via `Regex.Replace (ProductVersion, "[^A-Za-z0-9]", "_")`. Xamarin.Android.Build.Tasks targets netstandard2.0, so that pattern is parsed and interpreted at runtime on every build. Following #13460, move the task into Microsoft.Android.Build.Tasks (net12.0) and the `Microsoft.Android.Tasks` namespace, so the fixed pattern can use a source-generated `[GeneratedRegex]` partial property. The pattern text is unchanged and uses no `RegexOptions`, so matching semantics are identical. The task keeps its existing `MultithreadedAndroidTask` base. The task is registered through the same Full/Core `` pair as the other Microsoft.Android.Build.Tasks tasks; the unqualified `` invocation in `Xamarin.Android.Common.targets` is unchanged. The old `Xamarin.Android.Tasks.GetAndroidDefineConstants` type is removed. `MonoAndroidHelper.TryParseApiLevel()` moves from `MonoAndroidHelper.cs` to `MonoAndroidHelper.Basic.cs`, which is already compile-linked into Microsoft.Android.Build.Tasks. As that file is nullable-enabled, the method is now annotated `(string? apiLevel, [NotNullWhen (true)] out Version? version)`; `Aapt2Link` uses `out var` to match. Behavior is unchanged. The existing tests move to Microsoft.Android.Build.Tasks.Tests, plus a test which loads the registrations from `Xamarin.Android.Common.targets` and runs both `` and `` through `dotnet msbuild`. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../Tasks/GetAndroidDefineConstants.cs | 10 ++- .../Tasks/GetAndroidDefineConstantsTests.cs | 89 +++++++++++++++++++ .../Tasks/Aapt2Link.cs | 4 +- .../Tasks/GetAndroidDefineConstantsTests.cs | 48 ---------- .../Utilities/MonoAndroidHelper.Basic.cs | 15 ++++ .../Utilities/MonoAndroidHelper.cs | 14 --- .../Xamarin.Android.Common.targets | 3 +- 7 files changed, 115 insertions(+), 68 deletions(-) rename src/{Xamarin.Android.Build.Tasks => Microsoft.Android.Build.Tasks}/Tasks/GetAndroidDefineConstants.cs (85%) create mode 100644 src/Microsoft.Android.Build.Tasks/Tests/Microsoft.Android.Build.Tasks.Tests/Tasks/GetAndroidDefineConstantsTests.cs delete mode 100644 src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Tasks/GetAndroidDefineConstantsTests.cs diff --git a/src/Xamarin.Android.Build.Tasks/Tasks/GetAndroidDefineConstants.cs b/src/Microsoft.Android.Build.Tasks/Tasks/GetAndroidDefineConstants.cs similarity index 85% rename from src/Xamarin.Android.Build.Tasks/Tasks/GetAndroidDefineConstants.cs rename to src/Microsoft.Android.Build.Tasks/Tasks/GetAndroidDefineConstants.cs index 58925c4cc3e..7943af5d638 100644 --- a/src/Xamarin.Android.Build.Tasks/Tasks/GetAndroidDefineConstants.cs +++ b/src/Microsoft.Android.Build.Tasks/Tasks/GetAndroidDefineConstants.cs @@ -7,11 +7,12 @@ using Microsoft.Build.Framework; using Microsoft.Build.Utilities; using Microsoft.Android.Build.Tasks; +using Xamarin.Android.Tasks; -namespace Xamarin.Android.Tasks +namespace Microsoft.Android.Tasks { [MSBuildMultiThreadableTask] - public class GetAndroidDefineConstants : MultithreadedAndroidTask + public partial class GetAndroidDefineConstants : MultithreadedAndroidTask { public override string TaskPrefix => "GAD"; @@ -23,12 +24,15 @@ public class GetAndroidDefineConstants : MultithreadedAndroidTask [Output] public ITaskItem[]? AndroidDefineConstants { get; set; } + [GeneratedRegex ("[^A-Za-z0-9]")] + private static partial Regex NonAlphanumericRegex { get; } + public override bool RunTask () { var constants = new List (); if (!ProductVersion.IsNullOrEmpty ()) { - var version = Regex.Replace (ProductVersion, "[^A-Za-z0-9]", "_"); + var version = NonAlphanumericRegex.Replace (ProductVersion, "_"); constants.Add (new TaskItem ($"__XAMARIN_ANDROID_{version}__")); } diff --git a/src/Microsoft.Android.Build.Tasks/Tests/Microsoft.Android.Build.Tasks.Tests/Tasks/GetAndroidDefineConstantsTests.cs b/src/Microsoft.Android.Build.Tasks/Tests/Microsoft.Android.Build.Tasks.Tests/Tasks/GetAndroidDefineConstantsTests.cs new file mode 100644 index 00000000000..24d8c742f52 --- /dev/null +++ b/src/Microsoft.Android.Build.Tasks/Tests/Microsoft.Android.Build.Tasks.Tests/Tasks/GetAndroidDefineConstantsTests.cs @@ -0,0 +1,89 @@ +#nullable enable +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Xml.Linq; +using Microsoft.Build.Framework; +using NUnit.Framework; +using Microsoft.Android.Tasks; + +namespace Xamarin.Android.Build.Tests +{ + [TestFixture] + public class GetAndroidDefineConstantsTests : BaseTest + { + [TestCase ("36", null, 36, "")] + [TestCase ("36.1", null, 36, "__ANDROID_36_1__")] + [TestCase ("38.2", "11.0-rc+test", 38, "__ANDROID_36_1__;__ANDROID_37_1__;__ANDROID_37_2__;__ANDROID_37_3__;__ANDROID_37_4__;__ANDROID_38_1__;__ANDROID_38_2__")] + public void Constants (string apiLevel, string? productVersion, int major, string minorConstants) + { + var task = new GetAndroidDefineConstants { + AndroidApiLevel = apiLevel, + ProductVersion = productVersion, + }; + Assert.IsTrue (task.Execute ()); + var constants = task.AndroidDefineConstants ?? throw new InvalidOperationException ("No constants returned."); + var expected = new List (); + if (productVersion != null) + expected.Add ("__XAMARIN_ANDROID_11_0_rc_test__"); + expected.AddRange (["__MOBILE__", "__ANDROID__"]); + expected.AddRange (Enumerable.Range (1, major).Select (i => $"__ANDROID_{i}__")); + expected.AddRange (minorConstants.Split (';', StringSplitOptions.RemoveEmptyEntries)); + CollectionAssert.AreEqual (expected, constants.Select (i => i.ItemSpec)); + } + + [TestCase ("")] + [TestCase ("invalid")] + public void InvalidApiLevel (string apiLevel) + { + var errors = new List (); + var task = new GetAndroidDefineConstants { + AndroidApiLevel = apiLevel, + BuildEngine = new MockBuildEngine (TestContext.Out, errors: errors), + }; + Assert.IsFalse (task.Execute ()); + Assert.IsNull (task.AndroidDefineConstants); + Assert.IsEmpty (errors); + } + + [TestCase ("GetAndroidDefineConstants")] + [TestCase ("Microsoft.Android.Tasks.GetAndroidDefineConstants")] + public void RegisteredTaskNamesExecute (string taskName) + { + string directory = Path.Combine (Root, "temp", TestName); + Directory.CreateDirectory (directory); + string commonTargets = Path.Combine (NativeAotBootstrapTestTools.FindRepositoryRoot (), + "src", "Xamarin.Android.Build.Tasks", "Xamarin.Android.Common.targets"); + var registrations = XDocument.Load (commonTargets).Descendants () + .Where (e => e.Name.LocalName == "UsingTask" && (string?) e.Attribute ("TaskName") == "Microsoft.Android.Tasks.GetAndroidDefineConstants") + .Select (e => new XElement (e) { Name = "UsingTask" }) + .ToArray (); + Assert.AreEqual (2, registrations.Length); + + string project = Path.Combine (directory, "constants.proj"); + string output = Path.Combine (directory, "constants.txt"); + new XDocument (new XElement ("Project", + new XElement ("PropertyGroup", + new XElement ("_MicrosoftAndroidBuildTasksAssembly", typeof (GetAndroidDefineConstants).Assembly.Location)), + registrations, + new XElement ("Target", new XAttribute ("Name", "Build"), + new XElement (taskName, + new XAttribute ("AndroidApiLevel", "36.1"), + new XAttribute ("ProductVersion", "11.0-rc+test"), + new XElement ("Output", new XAttribute ("TaskParameter", "AndroidDefineConstants"), new XAttribute ("ItemName", "_Constants"))), + new XElement ("WriteLinesToFile", + new XAttribute ("File", output), + new XAttribute ("Lines", "@(_Constants)"), + new XAttribute ("Overwrite", "true"))))) + .Save (project); + + string dotnet = Environment.GetEnvironmentVariable ("DOTNET_HOST_PATH") ?? "dotnet"; + var result = NativeAotBootstrapTestTools.Run (dotnet, "msbuild", project, "-t:Build", "-v:minimal", "-nr:false"); + Assert.AreEqual (0, result.ExitCode, result.Output + result.Error); + var lines = File.ReadAllLines (output); + Assert.AreEqual ("__XAMARIN_ANDROID_11_0_rc_test__", lines [0]); + Assert.AreEqual ("__ANDROID_36_1__", lines [^1]); + } + } +} diff --git a/src/Xamarin.Android.Build.Tasks/Tasks/Aapt2Link.cs b/src/Xamarin.Android.Build.Tasks/Tasks/Aapt2Link.cs index c966306119e..79ae66ce443 100644 --- a/src/Xamarin.Android.Build.Tasks/Tasks/Aapt2Link.cs +++ b/src/Xamarin.Android.Build.Tasks/Tasks/Aapt2Link.cs @@ -162,7 +162,7 @@ string [] GenerateCommandLineCommands (string ManifestFile, string? currentAbi, Directory.CreateDirectory (manifestDir); string manifestFile = Path.Combine (manifestDir, Path.GetFileName (ManifestFile)); string targetSdkVersion = AndroidApiLevel; - if (MonoAndroidHelper.TryParseApiLevel (targetSdkVersion, out Version v)) { + if (MonoAndroidHelper.TryParseApiLevel (targetSdkVersion, out var v)) { targetSdkVersion = v.Major.ToString (CultureInfo.InvariantCulture); } ManifestDocument manifest = new ManifestDocument (ManifestFile, versions); @@ -341,7 +341,7 @@ string [] GenerateCommandLineCommands (string ManifestFile, string? currentAbi, } } // Use $(SupportedOSPlatformVersion) if minSdkVersion was not found in the manifest - if (minSdkVersion.IsNullOrEmpty () && MonoAndroidHelper.TryParseApiLevel (SupportedOSPlatformVersion, out Version version)) { + if (minSdkVersion.IsNullOrEmpty () && MonoAndroidHelper.TryParseApiLevel (SupportedOSPlatformVersion, out var version)) { minSdkVersion = version.Major.ToString (CultureInfo.InvariantCulture); } if (!minSdkVersion.IsNullOrEmpty ()) { diff --git a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Tasks/GetAndroidDefineConstantsTests.cs b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Tasks/GetAndroidDefineConstantsTests.cs deleted file mode 100644 index bd7a35200fd..00000000000 --- a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Tasks/GetAndroidDefineConstantsTests.cs +++ /dev/null @@ -1,48 +0,0 @@ -#nullable enable -using System; -using System.Collections.Generic; -using System.Linq; -using Microsoft.Build.Framework; -using NUnit.Framework; -using Xamarin.Android.Tasks; - -namespace Xamarin.Android.Build.Tests -{ - [TestFixture] - public class GetAndroidDefineConstantsTests - { - [TestCase ("36", null, 36, "")] - [TestCase ("36.1", null, 36, "__ANDROID_36_1__")] - [TestCase ("38.2", "11.0-rc+test", 38, "__ANDROID_36_1__;__ANDROID_37_1__;__ANDROID_37_2__;__ANDROID_37_3__;__ANDROID_37_4__;__ANDROID_38_1__;__ANDROID_38_2__")] - public void Constants (string apiLevel, string? productVersion, int major, string minorConstants) - { - var task = new GetAndroidDefineConstants { - AndroidApiLevel = apiLevel, - ProductVersion = productVersion, - }; - Assert.IsTrue (task.Execute ()); - var constants = task.AndroidDefineConstants ?? throw new InvalidOperationException ("No constants returned."); - var expected = new List (); - if (productVersion != null) - expected.Add ("__XAMARIN_ANDROID_11_0_rc_test__"); - expected.AddRange (["__MOBILE__", "__ANDROID__"]); - expected.AddRange (Enumerable.Range (1, major).Select (i => $"__ANDROID_{i}__")); - expected.AddRange (minorConstants.Split (';', StringSplitOptions.RemoveEmptyEntries)); - CollectionAssert.AreEqual (expected, constants.Select (i => i.ItemSpec)); - } - - [TestCase ("")] - [TestCase ("invalid")] - public void InvalidApiLevel (string apiLevel) - { - var errors = new List (); - var task = new GetAndroidDefineConstants { - AndroidApiLevel = apiLevel, - BuildEngine = new MockBuildEngine (TestContext.Out, errors: errors), - }; - Assert.IsFalse (task.Execute ()); - Assert.IsNull (task.AndroidDefineConstants); - Assert.IsEmpty (errors); - } - } -} diff --git a/src/Xamarin.Android.Build.Tasks/Utilities/MonoAndroidHelper.Basic.cs b/src/Xamarin.Android.Build.Tasks/Utilities/MonoAndroidHelper.Basic.cs index b42f34daaf5..d11757a3f9c 100644 --- a/src/Xamarin.Android.Build.Tasks/Utilities/MonoAndroidHelper.Basic.cs +++ b/src/Xamarin.Android.Build.Tasks/Utilities/MonoAndroidHelper.Basic.cs @@ -1,6 +1,7 @@ #nullable enable using System; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.Globalization; using System.Text; @@ -222,4 +223,18 @@ public static bool StringEquals (string? value1, string? value2, StringCompariso } return value1.Equals (value2, comparisonType); } + + public static bool TryParseApiLevel (string? apiLevel, [NotNullWhen (true)] out Version? version) + { + if (Version.TryParse (apiLevel, out var v)) { + version = v; + return true; + } + if (int.TryParse (apiLevel, out var major)) { + version = new Version (major, 0); + return true; + } + version = null; + return false; + } } diff --git a/src/Xamarin.Android.Build.Tasks/Utilities/MonoAndroidHelper.cs b/src/Xamarin.Android.Build.Tasks/Utilities/MonoAndroidHelper.cs index df5269a8a98..09d03d222b2 100644 --- a/src/Xamarin.Android.Build.Tasks/Utilities/MonoAndroidHelper.cs +++ b/src/Xamarin.Android.Build.Tasks/Utilities/MonoAndroidHelper.cs @@ -619,20 +619,6 @@ public static int ConvertSupportedOSPlatformVersionToApiLevel (string version) return apiLevel; } - public static bool TryParseApiLevel (string apiLevel, out Version version) - { - if (Version.TryParse (apiLevel, out var v)) { - version = v; - return true; - } - if (int.TryParse (apiLevel, out var major)) { - version = new Version (major, 0); - return true; - } - version = null; - return false; - } - #if MSBUILD public static string GetAssemblyRid (ITaskItem asmItem) { diff --git a/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Common.targets b/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Common.targets index 8eb56d64fc7..3e1650fbc79 100644 --- a/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Common.targets +++ b/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Common.targets @@ -71,7 +71,8 @@ Copyright (C) 2011-2012 Xamarin. All rights reserved. - + +