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..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,7 +14,8 @@ 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 @@ -74,7 +75,9 @@ This file contains MSBuild targets that support building and operating on Androi + 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/AndroidGradleProjectTests.cs b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/AndroidGradleProjectTests.cs index 2ecd6eaf883..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 @@ -574,5 +574,61 @@ 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" }; + // 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" }; + } + + /// + /// 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.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 = @"