diff --git a/src/Xamarin.Android.Build.Tasks/Tasks/D8.cs b/src/Xamarin.Android.Build.Tasks/Tasks/D8.cs
index c7c15fcf065..797d63e657c 100644
--- a/src/Xamarin.Android.Build.Tasks/Tasks/D8.cs
+++ b/src/Xamarin.Android.Build.Tasks/Tasks/D8.cs
@@ -16,6 +16,8 @@ public class D8 : JavaToolTask
{
public override string TaskPrefix => "DX8";
+ string? responseFilePath;
+
[Required]
public string JarPath { get; set; } = "";
@@ -44,6 +46,17 @@ public class D8 : JavaToolTask
public string? ExtraArguments { get; set; }
+ public override bool RunTask ()
+ {
+ try {
+ return base.RunTask ();
+ } finally {
+ if (!responseFilePath.IsNullOrEmpty () && File.Exists (responseFilePath)) {
+ File.Delete (responseFilePath);
+ }
+ }
+ }
+
protected override string GenerateCommandLineCommands ()
{
return GetCommandLineBuilder ().ToString ();
@@ -57,32 +70,72 @@ protected virtual CommandLineBuilder GetCommandLineBuilder ()
{
var cmd = new CommandLineBuilder ();
- if (JavaOptions is { Length: > 0 }) {
+ // Only JVM arguments go on the command line, everything else goes in the response file
+ if (!JavaOptions.IsNullOrEmpty ()) {
cmd.AppendSwitch (JavaOptions);
}
cmd.AppendSwitchIfNotNull ("-Xmx", JavaMaximumHeapSize);
cmd.AppendSwitchIfNotNull ("-classpath ", JarPath);
cmd.AppendSwitch (MainClass);
- if (ExtraArguments is { Length: > 0 })
- cmd.AppendSwitch (ExtraArguments); // it should contain "--dex".
+ // Create response file with all D8/R8 arguments to avoid command line length limits
+ responseFilePath = CreateResponseFile ();
+ cmd.AppendSwitch ($"@{responseFilePath}");
+
+ return cmd;
+ }
+
+ ///
+ /// Creates a response file containing all D8/R8 arguments.
+ /// This avoids command line length limits that can occur with many jar libraries.
+ ///
+ protected virtual string CreateResponseFile ()
+ {
+ var responseFile = Path.GetTempFileName ();
+ Log.LogDebugMessage ($"[{MainClass}] response file: {responseFile}");
+
+ using var response = new StreamWriter (responseFile, append: false, encoding: Files.UTF8withoutBOM);
+
+ // D8/R8 switches
+ if (!ExtraArguments.IsNullOrEmpty ())
+ WriteArg (response, ExtraArguments); // it should contain "--dex".
if (Debug)
- cmd.AppendSwitch ("--debug");
+ WriteArg (response, "--debug");
else
- cmd.AppendSwitch ("--release");
+ WriteArg (response, "--release");
//NOTE: if this is blank, we can omit --min-api in this call
- if (AndroidManifestFile is { Length: > 0 }) {
+ if (!AndroidManifestFile.IsNullOrEmpty ()) {
var doc = AndroidAppManifest.Load (AndroidManifestFile, MonoAndroidHelper.SupportedVersions);
if (doc.MinSdkVersion.HasValue) {
MinSdkVersion = doc.MinSdkVersion.Value;
- cmd.AppendSwitchIfNotNull ("--min-api ", MinSdkVersion.ToString ());
+ WriteArg (response, "--min-api");
+ WriteArg (response, MinSdkVersion.ToString ());
}
}
if (!EnableDesugar)
- cmd.AppendSwitch ("--no-desugaring");
+ WriteArg (response, "--no-desugaring");
+ if (!OutputDirectory.IsNullOrEmpty ()) {
+ WriteArg (response, "--output");
+ WriteArg (response, OutputDirectory);
+ }
+
+ // --map-diagnostics
+ if (MapDiagnostics != null) {
+ foreach (var diagnostic in MapDiagnostics) {
+ var from = diagnostic.ItemSpec;
+ var to = diagnostic.GetMetadata ("To");
+ if (from.IsNullOrEmpty () || to.IsNullOrEmpty ())
+ continue;
+ WriteArg (response, "--map-diagnostics");
+ WriteArg (response, from);
+ WriteArg (response, to);
+ }
+ }
+
+ // --lib and input jars
var injars = new List ();
var libjars = new List ();
if (AlternativeJarLibrariesToEmbed?.Length > 0) {
@@ -92,7 +145,7 @@ protected virtual CommandLineBuilder GetCommandLineBuilder ()
}
} else if (JavaLibrariesToEmbed != null) {
Log.LogDebugMessage (" processing ClassesZip, JavaLibrariesToEmbed...");
- if (ClassesZip is { Length: > 0 } && File.Exists (ClassesZip)) {
+ if (!ClassesZip.IsNullOrEmpty () && File.Exists (ClassesZip)) {
injars.Add (ClassesZip);
}
foreach (var jar in JavaLibrariesToEmbed) {
@@ -106,25 +159,25 @@ protected virtual CommandLineBuilder GetCommandLineBuilder ()
}
}
- cmd.AppendSwitchIfNotNull ("--output ", OutputDirectory);
- foreach (var jar in libjars)
- cmd.AppendSwitchIfNotNull ("--lib ", jar);
- foreach (var jar in injars)
- cmd.AppendFileNameIfNotNull (jar);
-
- if (MapDiagnostics != null) {
- foreach (var diagnostic in MapDiagnostics) {
- var from = diagnostic.ItemSpec;
- var to = diagnostic.GetMetadata ("To");
- if (from is not { Length: > 0 } || to is not { Length: > 0 })
- continue;
- cmd.AppendSwitch ("--map-diagnostics");
- cmd.AppendSwitch (from);
- cmd.AppendSwitch (to);
- }
+ foreach (var jar in libjars) {
+ WriteArg (response, "--lib");
+ WriteArg (response, jar);
+ }
+ foreach (var jar in injars) {
+ WriteArg (response, jar);
}
- return cmd;
+ return responseFile;
+ }
+
+ ///
+ /// Writes a single argument to the response file.
+ /// R8/D8 response files treat each line as a complete argument, so no quoting is needed.
+ ///
+ protected void WriteArg (StreamWriter writer, string arg)
+ {
+ writer.WriteLine (arg);
+ Log.LogDebugMessage ($" {arg}");
}
// Note: We do not want to call the base.LogEventsFromTextOutput as it will incorrectly identify
diff --git a/src/Xamarin.Android.Build.Tasks/Tasks/R8.cs b/src/Xamarin.Android.Build.Tasks/Tasks/R8.cs
index f2dee391d4b..fbf66e7bf48 100644
--- a/src/Xamarin.Android.Build.Tasks/Tasks/R8.cs
+++ b/src/Xamarin.Android.Build.Tasks/Tasks/R8.cs
@@ -51,9 +51,17 @@ public override bool RunTask ()
}
}
- protected override CommandLineBuilder GetCommandLineBuilder ()
+ ///
+ /// Override CreateResponseFile to add R8-specific arguments to the response file.
+ /// This ensures all arguments are passed via response file to avoid command line length limits.
+ ///
+ protected override string CreateResponseFile ()
{
- var cmd = base.GetCommandLineBuilder ();
+ // First, get the base response file path and write base D8 arguments
+ var responseFile = base.CreateResponseFile ();
+
+ // Now append R8-specific arguments to the response file
+ using var response = new StreamWriter (responseFile, append: true, encoding: Files.UTF8withoutBOM);
if (EnableMultiDex) {
if (MinSdkVersion >= 21) {
@@ -77,9 +85,12 @@ protected override CommandLineBuilder GetCommandLineBuilder ()
}
File.WriteAllText (temp, string.Concat (content));
- cmd.AppendSwitchIfNotNull ("--main-dex-list ", temp);
- cmd.AppendSwitchIfNotNull ("--main-dex-rules ", Path.Combine (AndroidSdkBuildToolsPath, "mainDexClasses.rules"));
- cmd.AppendSwitchIfNotNull ("--main-dex-list-output ", MultiDexMainDexListFile);
+ WriteArg (response, "--main-dex-list");
+ WriteArg (response, temp);
+ WriteArg (response, "--main-dex-rules");
+ WriteArg (response, Path.Combine (AndroidSdkBuildToolsPath, "mainDexClasses.rules"));
+ WriteArg (response, "--main-dex-list-output");
+ WriteArg (response, MultiDexMainDexListFile);
}
}
@@ -112,8 +123,8 @@ protected override CommandLineBuilder GetCommandLineBuilder ()
}
} else {
//NOTE: we may be calling r8 *only* for multi-dex, and all shrinking is disabled
- cmd.AppendSwitch ("--no-tree-shaking");
- cmd.AppendSwitch ("--no-minification");
+ WriteArg (response, "--no-tree-shaking");
+ WriteArg (response, "--no-minification");
// Rules to turn off optimizations
var temp = Path.GetTempFileName ();
var lines = new List {
@@ -131,18 +142,21 @@ protected override CommandLineBuilder GetCommandLineBuilder ()
}
File.WriteAllLines (temp, lines);
tempFiles.Add (temp);
- cmd.AppendSwitchIfNotNull ("--pg-conf ", temp);
+ WriteArg (response, "--pg-conf");
+ WriteArg (response, temp);
}
if (ProguardConfigurationFiles != null) {
foreach (var file in ProguardConfigurationFiles) {
- if (File.Exists (file))
- cmd.AppendSwitchIfNotNull ("--pg-conf ", file);
- else
+ if (File.Exists (file)) {
+ WriteArg (response, "--pg-conf");
+ WriteArg (response, file);
+ } else {
Log.LogCodedWarning ("XA4304", file, 0, Properties.Resources.XA4304, file);
+ }
}
}
- return cmd;
+ return responseFile;
}
// Note: We do not want to call the base.LogEventsFromTextOutput as it will incorrectly identify
diff --git a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Tasks/D8Tests.cs b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Tasks/D8Tests.cs
new file mode 100644
index 00000000000..f86c0b4f9d4
--- /dev/null
+++ b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Tasks/D8Tests.cs
@@ -0,0 +1,179 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using Microsoft.Build.Framework;
+using Microsoft.Build.Utilities;
+using NUnit.Framework;
+using Xamarin.Android.Tasks;
+
+namespace Xamarin.Android.Build.Tests
+{
+ [TestFixture]
+ public class D8Tests
+ {
+ MockBuildEngine engine;
+ List messages;
+ string tempDir;
+
+ [SetUp]
+ public void Setup ()
+ {
+ engine = new MockBuildEngine (TestContext.Out,
+ messages: messages = new List ());
+ tempDir = Path.Combine (Path.GetTempPath (), "D8Tests_" + Guid.NewGuid ().ToString ("N"));
+ Directory.CreateDirectory (tempDir);
+ }
+
+ [TearDown]
+ public void TearDown ()
+ {
+ if (Directory.Exists (tempDir)) {
+ Directory.Delete (tempDir, recursive: true);
+ }
+ }
+
+ ///
+ /// Tests that the D8 task creates a response file with the expected content.
+ /// This test uses a test subclass to avoid actually running Java.
+ ///
+ [Test]
+ public void ResponseFileContainsLibAndInputJars ()
+ {
+ // Create mock jar files for testing
+ var platformJar = Path.Combine (tempDir, "android.jar");
+ var inputJar1 = Path.Combine (tempDir, "input1.jar");
+ var inputJar2 = Path.Combine (tempDir, "input2.jar");
+ var libJar1 = Path.Combine (tempDir, "lib1.jar");
+ File.WriteAllText (platformJar, "mock");
+ File.WriteAllText (inputJar1, "mock");
+ File.WriteAllText (inputJar2, "mock");
+ File.WriteAllText (libJar1, "mock");
+
+ var d8Task = new D8TestTask {
+ BuildEngine = engine,
+ JarPath = "d8.jar",
+ JavaPlatformJarPath = platformJar,
+ OutputDirectory = tempDir,
+ JavaLibrariesToEmbed = new ITaskItem [] {
+ new TaskItem (inputJar1),
+ new TaskItem (inputJar2),
+ },
+ JavaLibrariesToReference = new ITaskItem [] {
+ new TaskItem (libJar1),
+ },
+ };
+
+ string commandLine = d8Task.TestGenerateCommandLineCommands ();
+ string responseFilePath = d8Task.ResponseFilePath;
+
+ try {
+ // Verify response file was created
+ Assert.IsNotNull (responseFilePath, "Response file path should not be null");
+ FileAssert.Exists (responseFilePath, "Response file should exist");
+
+ // Verify the response file is referenced in the command line
+ Assert.IsTrue (commandLine.Contains ($"@{responseFilePath}"), "Command line should reference the response file");
+
+ // Read and verify response file content
+ string [] responseFileContent = File.ReadAllLines (responseFilePath);
+
+ // Should contain --lib entries (written as separate lines: --lib on one line, path on next)
+ Assert.IsTrue (responseFileContent.Any (line => line == "--lib"), "Response file should contain --lib switch");
+ Assert.IsTrue (responseFileContent.Any (line => line.Contains ("android.jar")), "Response file should contain android.jar");
+ Assert.IsTrue (responseFileContent.Any (line => line.Contains ("lib1.jar")), "Response file should contain lib1.jar");
+
+ // Should contain input jars as direct arguments (no --lib prefix)
+ Assert.IsTrue (responseFileContent.Any (line => line.Contains ("input1.jar")), "Response file should contain input1.jar");
+ Assert.IsTrue (responseFileContent.Any (line => line.Contains ("input2.jar")), "Response file should contain input2.jar");
+
+ } finally {
+ // Clean up response file
+ if (responseFilePath != null && File.Exists (responseFilePath)) {
+ File.Delete (responseFilePath);
+ }
+ }
+ }
+
+ ///
+ /// Tests that paths with spaces are written correctly to the response file.
+ /// R8/D8 response files treat each line as a complete argument, so no quoting is needed.
+ ///
+ [Test]
+ public void ResponseFileHandlesPathsWithSpaces ()
+ {
+ var pathWithSpaces = Path.Combine (tempDir, "path with spaces");
+ Directory.CreateDirectory (pathWithSpaces);
+
+ // Create mock jar files for testing
+ var platformJar = Path.Combine (pathWithSpaces, "android.jar");
+ var inputJar = Path.Combine (pathWithSpaces, "input.jar");
+ File.WriteAllText (platformJar, "mock");
+ File.WriteAllText (inputJar, "mock");
+
+ var d8Task = new D8TestTask {
+ BuildEngine = engine,
+ JarPath = "d8.jar",
+ JavaPlatformJarPath = platformJar,
+ OutputDirectory = tempDir,
+ JavaLibrariesToEmbed = new ITaskItem [] {
+ new TaskItem (inputJar),
+ },
+ };
+
+ d8Task.TestGenerateCommandLineCommands ();
+ string responseFilePath = d8Task.ResponseFilePath;
+
+ try {
+ FileAssert.Exists (responseFilePath, "Response file should exist");
+ string responseFileContent = File.ReadAllText (responseFilePath);
+
+ // Paths with spaces should NOT be quoted (R8/D8 treats each line as a complete argument)
+ Assert.IsFalse (responseFileContent.Contains ("\""), "Response file should not contain quoted paths");
+ Assert.IsTrue (responseFileContent.Contains ("path with spaces"), "Response file should contain the path with spaces");
+
+ } finally {
+ if (responseFilePath != null && File.Exists (responseFilePath)) {
+ File.Delete (responseFilePath);
+ }
+ }
+ }
+ }
+
+ ///
+ /// Test subclass of D8 that exposes internal methods for testing without needing Java.
+ ///
+ internal class D8TestTask : D8
+ {
+ ///
+ /// The path to the response file created by the last call to TestGenerateCommandLineCommands.
+ ///
+ public string ResponseFilePath { get; private set; }
+
+ ///
+ /// Test method that generates command line without actually running the task.
+ ///
+ public string TestGenerateCommandLineCommands ()
+ {
+ var cmd = GetCommandLineBuilder ();
+ // Capture the response file path after command line generation
+ ResponseFilePath = GetResponseFilePathFromCommandLine (cmd.ToString ());
+ return cmd.ToString ();
+ }
+
+ private static string GetResponseFilePathFromCommandLine (string commandLine)
+ {
+ // Find the @filepath argument
+ var startIndex = commandLine.IndexOf ("@", StringComparison.Ordinal);
+ if (startIndex < 0) return null;
+
+ // Check if '@' is the last character
+ if (startIndex + 1 >= commandLine.Length) return null;
+
+ var endIndex = commandLine.IndexOf (" ", startIndex, StringComparison.Ordinal);
+ if (endIndex < 0) endIndex = commandLine.Length;
+
+ return commandLine.Substring (startIndex + 1, endIndex - startIndex - 1);
+ }
+ }
+}