From 06e0fe5da8b979ea291fd1a4afa251048d7e03c4 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 28 Jan 2026 19:39:39 +0000 Subject: [PATCH 1/6] Initial plan From 74cbd6c60b8cb901dbbcc04723ae6126e5ad7d0d Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 28 Jan 2026 20:16:08 +0000 Subject: [PATCH 2/6] Fix net.android.init.gradle.kts for Gradle 9.x compatibility Co-authored-by: jonathanpeppers <840039+jonathanpeppers@users.noreply.github.com> --- .../Resources/net.android.init.gradle.kts | 4 +-- .../Tasks/CopyResourceTests.cs | 25 +++++++++++++++++++ 2 files changed, 27 insertions(+), 2 deletions(-) diff --git a/src/Xamarin.Android.Build.Tasks/Resources/net.android.init.gradle.kts b/src/Xamarin.Android.Build.Tasks/Resources/net.android.init.gradle.kts index 853e261ffbe..6c2f7e407db 100644 --- a/src/Xamarin.Android.Build.Tasks/Resources/net.android.init.gradle.kts +++ b/src/Xamarin.Android.Build.Tasks/Resources/net.android.init.gradle.kts @@ -5,10 +5,10 @@ * https://docs.gradle.org/current/kotlin-dsl/gradle/org.gradle.api.invocation/-gradle/projects-loaded.html */ gradle.projectsLoaded { - if (gradle.startParameter.projectProperties.containsKey("netAndroidBuildDirOverride")) { + gradle.startParameter.projectProperties["netAndroidBuildDirOverride"]?.let { buildDir -> rootProject.allprojects { afterEvaluate { - layout.buildDirectory.set(file(gradle.startParameter.projectProperties["netAndroidBuildDirOverride"])) + layout.buildDirectory.set(file(buildDir)) } } } diff --git a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Tasks/CopyResourceTests.cs b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Tasks/CopyResourceTests.cs index f47f51c0704..081bbbb0807 100644 --- a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Tasks/CopyResourceTests.cs +++ b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Tasks/CopyResourceTests.cs @@ -2,6 +2,7 @@ using NUnit.Framework; using System.Collections.Generic; using System.IO; +using System.Reflection; using Xamarin.Android.Tasks; namespace Xamarin.Android.Build.Tests @@ -9,6 +10,7 @@ namespace Xamarin.Android.Build.Tests [TestFixture] public class CopyResourceTests { + static readonly Assembly ExecutingAssembly = typeof (CopyResource).Assembly; string tempFile; List errors; List messages; @@ -35,6 +37,29 @@ public void TearDown () new object[] { "MonoRuntimeProvider.Bundled.java" }, }; + /// + /// Verifies that net.android.init.gradle.kts uses Kotlin's safe call pattern + /// to handle nullable String? return from projectProperties map access. + /// This is required for compatibility with Gradle 9.x which has stricter + /// Kotlin type checking where file() requires non-nullable Any parameter. + /// See: https://github.com/dotnet/android/issues/9818 + /// + [Test] + public void NetAndroidInitGradleKts_UsesNullSafePattern () + { + const string resourceName = "net.android.init.gradle.kts"; + using (var stream = ExecutingAssembly.GetManifestResourceStream (resourceName)) + using (var reader = new StreamReader (stream)) { + var content = reader.ReadToEnd (); + // The script should use ?.let {} pattern for null-safe access + // This ensures compatibility with Gradle 9.x which rejects nullable types passed to file() + StringAssert.Contains ("?.let {", content, "Script should use Kotlin's safe call pattern (?.let) for Gradle 9.x compatibility"); + // The script should NOT pass nullable directly to file() like: file(gradle.startParameter.projectProperties["..."]) + StringAssert.DoesNotContain ("file(gradle.startParameter.projectProperties[", content, + "Script should not pass nullable projectProperties value directly to file() as this fails on Gradle 9.x"); + } + } + [Test] [TestCaseSource (nameof (EmbeddedResources))] public void FilesThatAreExpected (string resourceName) From 6dc372115792d1ce1e5c84a06d325141c5092f28 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 28 Jan 2026 21:04:54 +0000 Subject: [PATCH 3/6] Replace useless content test with end-to-end AGP version integration test Co-authored-by: jonathanpeppers <840039+jonathanpeppers@users.noreply.github.com> --- .../AndroidGradleProjectTests.cs | 53 +++++++++++++++++ .../Tasks/CopyResourceTests.cs | 25 -------- .../Android/AndroidGradleProject.cs | 57 ++++++++++++++++--- 3 files changed, 103 insertions(+), 32 deletions(-) diff --git a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/AndroidGradleProjectTests.cs b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/AndroidGradleProjectTests.cs index 2ecd6eaf883..2c21ec16c7f 100644 --- a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/AndroidGradleProjectTests.cs +++ b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/AndroidGradleProjectTests.cs @@ -574,5 +574,58 @@ public void TestFacebook () {{ FileAssert.Exists (Path.Combine (Root, builder.ProjectDirectory, proj.OutputPath, $"{moduleName}-release.aar")); } + /// + /// Test case data for AGP/Gradle version combinations. + /// Gradle 9.x has stricter Kotlin type checking for null safety. + /// See: https://github.com/dotnet/android/issues/9818 + /// + static IEnumerable GetAgpGradleVersionTestData () + { + // AGP 8.5.0 with Gradle 8.7 (minimum required for AGP 8.5) + yield return new object[] { "8.5.0", "8.7" }; + // AGP 8.7.0 with Gradle 8.9 (minimum required for AGP 8.7) + yield return new object[] { "8.7.0", "8.9" }; + // AGP 8.8.0 with Gradle 8.10 (minimum required for AGP 8.8) + yield return new object[] { "8.8.0", "8.10.2" }; + // AGP 8.9.0 with Gradle 8.11 (minimum required for AGP 8.9) + yield return new object[] { "8.9.0", "8.11.1" }; + } + + /// + /// Verifies that .NET Android binding projects work with various AGP and Gradle versions. + /// This test ensures compatibility with Gradle's evolving Kotlin type checking behavior, + /// particularly the stricter null safety checks introduced in Gradle 9.x. + /// + [Test] + [TestCaseSource (nameof (GetAgpGradleVersionTestData))] + public void BindLibraryWithMultipleGradleVersions (string agpVersion, string gradleVersion) + { + var gradleProject = AndroidGradleProject.CreateDefault (GradleTestProjectDir, agpVersion, gradleVersion); + var moduleName = gradleProject.Modules.First ().Name; + + var proj = new XamarinAndroidBindingProject { + Jars = { + new BuildItem (KnownProperties.AndroidGradleProject, gradleProject.BuildFilePath) { + Metadata = { + { "ModuleName", moduleName }, + { "Bind", "true" }, + { "Configuration", "Release" }, + }, + }, + }, + Sources = { + new BuildItem.Source ("Foo.cs") { + TextContent = () => @$"public class Foo {{ public Foo () {{ System.Console.WriteLine (GradleTest.{moduleName}Class.GetString(""TestString"")); }} }}" + }, + }, + MetadataXml = $@"GradleTest", + }; + + using var builder = CreateDllBuilder (); + builder.Verbosity = LoggerVerbosity.Detailed; + Assert.IsTrue (builder.Build (proj), $"Build with AGP {agpVersion} and Gradle {gradleVersion} should have succeeded."); + FileAssert.Exists (Path.Combine (Root, builder.ProjectDirectory, proj.OutputPath, $"{moduleName}-release.aar")); + } + } } diff --git a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Tasks/CopyResourceTests.cs b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Tasks/CopyResourceTests.cs index 081bbbb0807..f47f51c0704 100644 --- a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Tasks/CopyResourceTests.cs +++ b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Tasks/CopyResourceTests.cs @@ -2,7 +2,6 @@ using NUnit.Framework; using System.Collections.Generic; using System.IO; -using System.Reflection; using Xamarin.Android.Tasks; namespace Xamarin.Android.Build.Tests @@ -10,7 +9,6 @@ namespace Xamarin.Android.Build.Tests [TestFixture] public class CopyResourceTests { - static readonly Assembly ExecutingAssembly = typeof (CopyResource).Assembly; string tempFile; List errors; List messages; @@ -37,29 +35,6 @@ public void TearDown () new object[] { "MonoRuntimeProvider.Bundled.java" }, }; - /// - /// Verifies that net.android.init.gradle.kts uses Kotlin's safe call pattern - /// to handle nullable String? return from projectProperties map access. - /// This is required for compatibility with Gradle 9.x which has stricter - /// Kotlin type checking where file() requires non-nullable Any parameter. - /// See: https://github.com/dotnet/android/issues/9818 - /// - [Test] - public void NetAndroidInitGradleKts_UsesNullSafePattern () - { - const string resourceName = "net.android.init.gradle.kts"; - using (var stream = ExecutingAssembly.GetManifestResourceStream (resourceName)) - using (var reader = new StreamReader (stream)) { - var content = reader.ReadToEnd (); - // The script should use ?.let {} pattern for null-safe access - // This ensures compatibility with Gradle 9.x which rejects nullable types passed to file() - StringAssert.Contains ("?.let {", content, "Script should use Kotlin's safe call pattern (?.let) for Gradle 9.x compatibility"); - // The script should NOT pass nullable directly to file() like: file(gradle.startParameter.projectProperties["..."]) - StringAssert.DoesNotContain ("file(gradle.startParameter.projectProperties[", content, - "Script should not pass nullable projectProperties value directly to file() as this fails on Gradle 9.x"); - } - } - [Test] [TestCaseSource (nameof (EmbeddedResources))] public void FilesThatAreExpected (string resourceName) diff --git a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.ProjectTools/Android/AndroidGradleProject.cs b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.ProjectTools/Android/AndroidGradleProject.cs index 5b6a1c62fec..618009925bc 100644 --- a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.ProjectTools/Android/AndroidGradleProject.cs +++ b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.ProjectTools/Android/AndroidGradleProject.cs @@ -13,6 +13,16 @@ public class AndroidGradleProject public string BuildFilePath => Path.Combine (ProjectDirectory, "build.gradle.kts"); + /// + /// Android Gradle Plugin version (e.g., "8.5.0", "9.0.0") + /// + public string AgpVersion { get; set; } = "8.5.0"; + + /// + /// Gradle wrapper version to use (e.g., "8.12", "9.0"). If null, uses system default. + /// + public string? GradleVersion { get; set; } + GradleCLI gradleCLI = new GradleCLI (); public AndroidGradleProject (string directory) @@ -26,12 +36,27 @@ public void Create () gradleCLI.Init (ProjectDirectory); var settingsFile = Path.Combine (ProjectDirectory, "settings.gradle.kts"); File.WriteAllText (settingsFile, settings_gradle_kts_content); - File.WriteAllText (BuildFilePath, build_gradle_kts_content); + File.WriteAllText (BuildFilePath, GetBuildGradleKtsContent ()); foreach (var module in Modules) { module.Create (); File.AppendAllText (settingsFile, $"{Environment.NewLine}include(\":{module.Name}\")"); } File.AppendAllText (Path.Combine (ProjectDirectory, "gradle.properties"), "android.useAndroidX=true"); + + // Update Gradle wrapper version if specified + if (!string.IsNullOrEmpty (GradleVersion)) { + var wrapperPropertiesPath = Path.Combine (ProjectDirectory, "gradle", "wrapper", "gradle-wrapper.properties"); + if (File.Exists (wrapperPropertiesPath)) { + var content = File.ReadAllText (wrapperPropertiesPath); + // Replace the distribution URL with the specified Gradle version + content = System.Text.RegularExpressions.Regex.Replace ( + content, + @"distributionUrl=.*", + $@"distributionUrl=https\://services.gradle.org/distributions/gradle-{GradleVersion}-bin.zip" + ); + File.WriteAllText (wrapperPropertiesPath, content); + } + } } public static AndroidGradleProject CreateDefault (string projectDir, bool isApplication = false) @@ -47,12 +72,30 @@ public static AndroidGradleProject CreateDefault (string projectDir, bool isAppl return proj; } - const string build_gradle_kts_content = -@" -plugins { - id(""com.android.application"") version ""8.5.0"" apply false - id(""com.android.library"") version ""8.5.0"" apply false -} + /// + /// Creates a default Gradle project with specified AGP and Gradle versions. + /// + public static AndroidGradleProject CreateDefault (string projectDir, string agpVersion, string? gradleVersion, bool isApplication = false) + { + var proj = new AndroidGradleProject (projectDir) { + AgpVersion = agpVersion, + GradleVersion = gradleVersion, + Modules = { + new AndroidGradleModule (Path.Combine (projectDir, "TestModule")) { + IsApplication = isApplication, + }, + }, + }; + proj.Create (); + return proj; + } + + string GetBuildGradleKtsContent () => +$@" +plugins {{ + id(""com.android.application"") version ""{AgpVersion}"" apply false + id(""com.android.library"") version ""{AgpVersion}"" apply false +}} "; const string settings_gradle_kts_content = @" From bc39012c8e4ae3381c8c9e9ddc1c554219ef86c0 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 28 Jan 2026 21:12:15 +0000 Subject: [PATCH 4/6] Add AGP 9.0.0 with Gradle 9.1 test case Co-authored-by: jonathanpeppers <840039+jonathanpeppers@users.noreply.github.com> --- .../Xamarin.Android.Build.Tests/AndroidGradleProjectTests.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/AndroidGradleProjectTests.cs b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/AndroidGradleProjectTests.cs index 2c21ec16c7f..ef5e6948fc3 100644 --- a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/AndroidGradleProjectTests.cs +++ b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/AndroidGradleProjectTests.cs @@ -589,6 +589,9 @@ static IEnumerable GetAgpGradleVersionTestData () yield return new object[] { "8.8.0", "8.10.2" }; // AGP 8.9.0 with Gradle 8.11 (minimum required for AGP 8.9) yield return new object[] { "8.9.0", "8.11.1" }; + // AGP 9.0.0 with Gradle 9.1 (minimum required for AGP 9.0) + // This tests the Gradle 9.x stricter Kotlin type checking fix + yield return new object[] { "9.0.0", "9.1" }; } /// From 440f8e2017b959cc72003fa1883c982dfaf9103d Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 29 Jan 2026 17:43:37 +0000 Subject: [PATCH 5/6] Add AndroidGradleInitScriptPath property as escape hatch for custom init scripts Co-authored-by: jonathanpeppers <840039+jonathanpeppers@users.noreply.github.com> --- .../Microsoft.Android.Sdk.Bindings.Gradle.targets | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/Xamarin.Android.Build.Tasks/MSBuild/Xamarin/Android/Microsoft.Android.Sdk.Bindings.Gradle.targets b/src/Xamarin.Android.Build.Tasks/MSBuild/Xamarin/Android/Microsoft.Android.Sdk.Bindings.Gradle.targets index 5f7afd0fbf5..da2ece6ceb0 100644 --- a/src/Xamarin.Android.Build.Tasks/MSBuild/Xamarin/Android/Microsoft.Android.Sdk.Bindings.Gradle.targets +++ b/src/Xamarin.Android.Build.Tasks/MSBuild/Xamarin/Android/Microsoft.Android.Sdk.Bindings.Gradle.targets @@ -16,6 +16,8 @@ This file contains MSBuild targets that support building and operating on Androi <_AGPOutDirAbs Condition=" !$([System.IO.Path]::IsPathRooted('$(_AGPOutDirAbs)')) ">$(MSBuildProjectDirectory)/$(_AGPOutDirAbs) <_AGPInitScriptPath>$(_AGPOutDirAbs)net.android.init.gradle.kts <_BuildAndroidGradleProjectsStamp>$(_AndroidStampDirectory)_BuildAndroidGradleProjects.stamp + + @@ -74,10 +76,18 @@ This file contains MSBuild targets that support building and operating on Androi + + Date: Thu, 29 Jan 2026 18:25:00 +0000 Subject: [PATCH 6/6] Use existing _AGPInitScriptPath property for escape hatch instead of new public property Co-authored-by: jonathanpeppers <840039+jonathanpeppers@users.noreply.github.com> --- .../Microsoft.Android.Sdk.Bindings.Gradle.targets | 15 ++++----------- 1 file changed, 4 insertions(+), 11 deletions(-) diff --git a/src/Xamarin.Android.Build.Tasks/MSBuild/Xamarin/Android/Microsoft.Android.Sdk.Bindings.Gradle.targets b/src/Xamarin.Android.Build.Tasks/MSBuild/Xamarin/Android/Microsoft.Android.Sdk.Bindings.Gradle.targets index da2ece6ceb0..347e6312227 100644 --- a/src/Xamarin.Android.Build.Tasks/MSBuild/Xamarin/Android/Microsoft.Android.Sdk.Bindings.Gradle.targets +++ b/src/Xamarin.Android.Build.Tasks/MSBuild/Xamarin/Android/Microsoft.Android.Sdk.Bindings.Gradle.targets @@ -14,10 +14,9 @@ This file contains MSBuild targets that support building and operating on Androi <_AGPOutDirAbs>$(IntermediateOutputPath)gradle/ <_AGPOutDirAbs Condition=" !$([System.IO.Path]::IsPathRooted('$(_AGPOutDirAbs)')) ">$(MSBuildProjectDirectory)/$(_AGPOutDirAbs) - <_AGPInitScriptPath>$(_AGPOutDirAbs)net.android.init.gradle.kts + + <_AGPInitScriptPath Condition=" '$(_AGPInitScriptPath)' == '' ">$(_AGPOutDirAbs)net.android.init.gradle.kts <_BuildAndroidGradleProjectsStamp>$(_AndroidStampDirectory)_BuildAndroidGradleProjects.stamp - - @@ -76,18 +75,12 @@ This file contains MSBuild targets that support building and operating on Androi - + -