Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@ This file contains MSBuild targets that support building and operating on Androi
<PropertyGroup>
<_AGPOutDirAbs>$(IntermediateOutputPath)gradle/</_AGPOutDirAbs>
<_AGPOutDirAbs Condition=" !$([System.IO.Path]::IsPathRooted('$(_AGPOutDirAbs)')) ">$(MSBuildProjectDirectory)/$(_AGPOutDirAbs)</_AGPOutDirAbs>
<_AGPInitScriptPath>$(_AGPOutDirAbs)net.android.init.gradle.kts</_AGPInitScriptPath>
<!-- Users can override _AGPInitScriptPath to point to their own init.gradle.kts file as an escape hatch -->
<_AGPInitScriptPath Condition=" '$(_AGPInitScriptPath)' == '' ">$(_AGPOutDirAbs)net.android.init.gradle.kts</_AGPInitScriptPath>
<_BuildAndroidGradleProjectsStamp>$(_AndroidStampDirectory)_BuildAndroidGradleProjects.stamp</_BuildAndroidGradleProjectsStamp>
</PropertyGroup>
Comment thread
jonathanpeppers marked this conversation as resolved.

Expand Down Expand Up @@ -74,7 +75,9 @@ This file contains MSBuild targets that support building and operating on Androi
<RemoveDir Directories="@(AndroidGradleProject->'%(OutputPath)outputs')" />

<!-- Create the net.android.init.gradle.kts script used to override the Gradle project output directory -->
<!-- Skip CopyResource if user has overridden _AGPInitScriptPath to point to their own file -->
<CopyResource
Condition=" $([System.String]::Copy('$(_AGPInitScriptPath)').StartsWith('$(_AGPOutDirAbs)')) "
Comment thread
jonathanpeppers marked this conversation as resolved.
Outdated
ResourceName="net.android.init.gradle.kts"
OutputPath="$(_AGPInitScriptPath)"
/>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,10 @@
* https://docs.gradle.org/current/kotlin-dsl/gradle/org.gradle.api.invocation/-gradle/projects-loaded.html
*/
gradle.projectsLoaded {
Comment thread
jonathanpeppers marked this conversation as resolved.
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))
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -574,5 +574,61 @@ public void TestFacebook () {{
FileAssert.Exists (Path.Combine (Root, builder.ProjectDirectory, proj.OutputPath, $"{moduleName}-release.aar"));
}

/// <summary>
/// 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
/// </summary>
static IEnumerable<object[]> 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" };
}
Comment thread
jonathanpeppers marked this conversation as resolved.

/// <summary>
/// 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.
/// </summary>
[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 = $@"<metadata><attr path=""/api/package[@name='{gradleProject.Modules.First ().PackageName}']"" name=""managedName"">GradleTest</attr></metadata>",
};

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"));
}

}
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,16 @@ public class AndroidGradleProject

public string BuildFilePath => Path.Combine (ProjectDirectory, "build.gradle.kts");

/// <summary>
/// Android Gradle Plugin version (e.g., "8.5.0", "9.0.0")
/// </summary>
public string AgpVersion { get; set; } = "8.5.0";

/// <summary>
/// Gradle wrapper version to use (e.g., "8.12", "9.0"). If null, uses system default.
Comment thread
jonathanpeppers marked this conversation as resolved.
Outdated
/// </summary>
public string? GradleVersion { get; set; }

GradleCLI gradleCLI = new GradleCLI ();

public AndroidGradleProject (string directory)
Expand All @@ -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)
Expand All @@ -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
}
/// <summary>
/// Creates a default Gradle project with specified AGP and Gradle versions.
/// </summary>
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 =
@"
Expand Down
Loading