Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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>

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

/// <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.
/// </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