From e2c5f69132b879b021e6d5c984240c0b2ba3c8e1 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Wed, 19 Aug 2026 14:32:27 +0200 Subject: [PATCH 1/5] [build] Pass class files directly to D8 and R8 Avoid creating an intermediate classes.zip after javac. D8 and R8 accept individual .class inputs through their response files, eliminating redundant archive I/O. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Android/Xamarin.Android.Javac.targets | 4 +--- src/Xamarin.Android.Build.Tasks/Tasks/D8.cs | 24 ++++++++++--------- .../Tasks/D8Tests.cs | 6 +++++ .../Xamarin.Android.Common.targets | 3 +-- .../Xamarin.Android.D8.targets | 5 ++-- 5 files changed, 24 insertions(+), 18 deletions(-) diff --git a/src/Xamarin.Android.Build.Tasks/MSBuild/Xamarin/Android/Xamarin.Android.Javac.targets b/src/Xamarin.Android.Build.Tasks/MSBuild/Xamarin/Android/Xamarin.Android.Javac.targets index b7867c60764..04b17fe3b6b 100644 --- a/src/Xamarin.Android.Build.Tasks/MSBuild/Xamarin/Android/Xamarin.Android.Javac.targets +++ b/src/Xamarin.Android.Build.Tasks/MSBuild/Xamarin/Android/Xamarin.Android.Javac.targets @@ -154,15 +154,13 @@ It is shared between "legacy" binding projects and .NET 7+ projects. Inputs="@(_AndroidMSBuildAllProjects);$(_AndroidCompileJavaFileList);$(MonoPlatformJarPath);@(_JavaStubFiles);@(_JavaSource)" Outputs="$(_AndroidCompileJavaStampFile)"> - + - (); + // --lib and program inputs + var inputs = new List (); var libjars = new List (); + if (ClassFiles != null) { + foreach (var classFile in ClassFiles) { + inputs.Add (classFile.ItemSpec); + } + } if (AlternativeJarLibrariesToEmbed?.Length > 0) { Log.LogDebugMessage (" processing AlternativeJarLibrariesToEmbed..."); foreach (var jar in AlternativeJarLibrariesToEmbed) { - injars.Add (jar.ItemSpec); + inputs.Add (jar.ItemSpec); } } else if (JavaLibrariesToEmbed != null) { - Log.LogDebugMessage (" processing ClassesZip, JavaLibrariesToEmbed..."); - if (!ClassesZip.IsNullOrEmpty () && File.Exists (ClassesZip)) { - injars.Add (ClassesZip); - } + Log.LogDebugMessage (" processing JavaLibrariesToEmbed..."); foreach (var jar in JavaLibrariesToEmbed) { - injars.Add (jar.ItemSpec); + inputs.Add (jar.ItemSpec); } } libjars.Add (JavaPlatformJarPath); @@ -163,8 +165,8 @@ protected virtual string CreateResponseFile () WriteArg (response, "--lib"); WriteArg (response, jar); } - foreach (var jar in injars) { - WriteArg (response, jar); + foreach (var input in inputs) { + WriteArg (response, input); } return responseFile; 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 index 7ed141a91f0..5146ab04bde 100644 --- 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 @@ -46,10 +46,12 @@ public void ResponseFileContainsLibAndInputJars () var inputJar1 = Path.Combine (tempDir, "input1.jar"); var inputJar2 = Path.Combine (tempDir, "input2.jar"); var libJar1 = Path.Combine (tempDir, "lib1.jar"); + var classFile = Path.Combine (tempDir, "Example.class"); File.WriteAllText (platformJar, "mock"); File.WriteAllText (inputJar1, "mock"); File.WriteAllText (inputJar2, "mock"); File.WriteAllText (libJar1, "mock"); + File.WriteAllText (classFile, "mock"); var d8Task = new D8TestTask { BuildEngine = engine, @@ -60,6 +62,9 @@ public void ResponseFileContainsLibAndInputJars () new TaskItem (inputJar1), new TaskItem (inputJar2), }, + ClassFiles = new ITaskItem [] { + new TaskItem (classFile), + }, JavaLibrariesToReference = new ITaskItem [] { new TaskItem (libJar1), }, @@ -87,6 +92,7 @@ public void ResponseFileContainsLibAndInputJars () // 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"); + Assert.IsTrue (responseFileContent.Any (line => line.Contains ("Example.class")), "Response file should contain class files"); } finally { // Clean up response file diff --git a/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Common.targets b/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Common.targets index 57fa1a57afc..bf3311e2a85 100644 --- a/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Common.targets +++ b/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Common.targets @@ -921,7 +921,6 @@ because xbuild doesn't support framework reference assemblies. <_AndroidIntermediateJavaSourceDirectory>$(IntermediateOutputPath)android\src\ <_AndroidIntermediateDexOutputDirectory>$(IntermediateOutputPath)android\bin\ <_AndroidIntermediateJavaClassDirectory>$(IntermediateOutputPath)android\bin\classes\ - <_AndroidIntermediateClassesZip>$(IntermediateOutputPath)android\bin\classes.zip <_AndroidAotBinDirectory>$(IntermediateOutputPath)aot <_AndroidResgenFlagFile>$(IntermediateOutputPath)R.cs.flag <_AndroidResFlagFile>$(IntermediateOutputPath)res.flag @@ -1969,7 +1968,7 @@ because xbuild doesn't support framework reference assemblies. @(_AndroidMSBuildAllProjects) ;@(_JavaLibrariesToCompileForApp) ;@(AndroidExternalJavaLibrary) - ;$(_AndroidIntermediateClassesZip) + ;$(_AndroidCompileJavaStampFile) ;@(ProguardConfiguration) ;@(MultiDexMainDexList) ;$(_AndroidBuildPropertiesCache) diff --git a/src/Xamarin.Android.Build.Tasks/Xamarin.Android.D8.targets b/src/Xamarin.Android.Build.Tasks/Xamarin.Android.D8.targets index f66903d0449..1b6c320f845 100644 --- a/src/Xamarin.Android.Build.Tasks/Xamarin.Android.D8.targets +++ b/src/Xamarin.Android.Build.Tasks/Xamarin.Android.D8.targets @@ -51,6 +51,7 @@ Copyright (C) 2018 Xamarin. All rights reserved. <_AndroidD8MapDiagnostics Condition=" '$(AndroidD8IgnoreWarnings)' == 'true' " Include="warning" To="info" /> <_AndroidR8MapDiagnostics Condition=" '$(AndroidR8IgnoreWarnings)' == 'true' " Include="warning" To="info" /> + <_JavaClassFile Include="$(_AndroidIntermediateJavaClassDirectory)**\*.class" /> <_R8KeepJavaSource Include="@(AndroidJavaSource)" Condition=" '%(AndroidJavaSource.Bind)' != 'True' " /> @@ -66,7 +67,7 @@ Copyright (C) 2018 Xamarin. All rights reserved. OutputDirectory="$(_AndroidIntermediateDexOutputDirectory)" Debug="$(AndroidIncludeDebugSymbols)" JavaPlatformJarPath="$(JavaPlatformJarPath)" - ClassesZip="$(_AndroidIntermediateClassesZip)" + ClassFiles="@(_JavaClassFile)" JavaLibrariesToEmbed="@(_JavaLibrariesToCompileForApp);" JavaLibrariesToReference="@(AndroidExternalJavaLibrary)" EnableDesugar="$(AndroidEnableDesugar)" @@ -99,7 +100,7 @@ Copyright (C) 2018 Xamarin. All rights reserved. OutputDirectory="$(_AndroidIntermediateDexOutputDirectory)" Debug="$(AndroidIncludeDebugSymbols)" JavaPlatformJarPath="$(JavaPlatformJarPath)" - ClassesZip="$(_AndroidIntermediateClassesZip)" + ClassFiles="@(_JavaClassFile)" JavaLibrariesToEmbed="@(_JavaLibrariesToCompileForApp)" AlternativeJarLibrariesToEmbed="@(_AlternativeJarForAppD8)" JavaLibrariesToReference="@(AndroidExternalJavaLibrary)" From 6951a1c004f5563e49f279d380446697822794c6 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Wed, 19 Aug 2026 14:43:01 +0200 Subject: [PATCH 2/5] [tests] Check loose javac class outputs Update BuildIncrementingClassName to validate the class output directory now that app builds no longer create classes.zip. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Xamarin.Android.Build.Tests/BuildTest2.cs | 33 ++++++++----------- 1 file changed, 14 insertions(+), 19 deletions(-) diff --git a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/BuildTest2.cs b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/BuildTest2.cs index 48cd2eb9d58..cc229e477b4 100644 --- a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/BuildTest2.cs +++ b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/BuildTest2.cs @@ -1432,35 +1432,30 @@ public void BuildIncrementingClassName ([Values (AndroidRuntime.CoreCLR, Android using (var b = CreateApkBuilder ()) { Assert.IsTrue (b.Build (proj), "Build should have succeeded."); - var classesZipPath = Path.Combine (Root, b.ProjectDirectory, proj.IntermediateOutputPath, "android", "bin", "classes.zip"); - FileAssert.Exists (classesZipPath); - var expectedBuilder = new StringBuilder (); - using (var zip = ZipHelper.OpenZip (classesZipPath)) { - foreach (var file in zip) { - expectedBuilder.AppendLine (file.FullName); - } - } - var expectedZip = expectedBuilder.ToString (); + var classesDirectory = Path.Combine (Root, b.ProjectDirectory, proj.IntermediateOutputPath, "android", "bin", "classes"); + DirectoryAssert.Exists (classesDirectory); + var expectedClasses = Directory.GetFiles (classesDirectory, "*.class", SearchOption.AllDirectories) + .Select (path => Path.GetRelativePath (classesDirectory, path)) + .OrderBy (path => path) + .ToArray (); + Assert.IsNotEmpty (expectedClasses); source.Timestamp = null; //Force the file to re-save w/ new Timestamp Assert.IsTrue (b.Build (proj), "Second build should have succeeded."); - var actualBuilder = new StringBuilder (); - using (var zip = ZipHelper.OpenZip (classesZipPath)) { - foreach (var file in zip) { - actualBuilder.AppendLine (file.FullName); - } - } - var actualZip = actualBuilder.ToString (); - Assert.AreNotEqual (expectedZip, actualZip); + var actualClasses = Directory.GetFiles (classesDirectory, "*.class", SearchOption.AllDirectories) + .Select (path => Path.GetRelativePath (classesDirectory, path)) + .OrderBy (path => path) + .ToArray (); + CollectionAssert.AreNotEqual (expectedClasses, actualClasses); //Build with no changes Assert.IsTrue (b.Build (proj), "Third build should have succeeded."); - FileAssert.Exists (classesZipPath); + DirectoryAssert.Exists (classesDirectory); //Clean Assert.IsTrue (b.Clean (proj), "Clean should have succeeded."); - FileAssert.DoesNotExist (classesZipPath); + DirectoryAssert.DoesNotExist (classesDirectory); } } From 3d7d113585bef7598ee621da9f5a788eb92add13 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Thu, 20 Aug 2026 14:17:08 +0200 Subject: [PATCH 3/5] [build] Separate binding JAR creation from Javac Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Xamarin/Android/Xamarin.Android.Javac.targets | 8 ++++++-- src/Xamarin.Android.Build.Tasks/Tasks/Javac.cs | 15 +-------------- 2 files changed, 7 insertions(+), 16 deletions(-) diff --git a/src/Xamarin.Android.Build.Tasks/MSBuild/Xamarin/Android/Xamarin.Android.Javac.targets b/src/Xamarin.Android.Build.Tasks/MSBuild/Xamarin/Android/Xamarin.Android.Javac.targets index 04b17fe3b6b..3d49494448f 100644 --- a/src/Xamarin.Android.Build.Tasks/MSBuild/Xamarin/Android/Xamarin.Android.Javac.targets +++ b/src/Xamarin.Android.Build.Tasks/MSBuild/Xamarin/Android/Xamarin.Android.Javac.targets @@ -106,7 +106,7 @@ It is shared between "legacy" binding projects and .NET 7+ projects. Inputs="@(_AndroidMSBuildAllProjects);$(_AndroidCompileBindingJavaFileList);$(MonoPlatformJarPath);@(_JavaBindingSource)" Outputs="$(_AndroidCompileBindingJavaStampFile)"> - + @@ -114,7 +114,6 @@ It is shared between "legacy" binding projects and .NET 7+ projects. Condition=" '@(_JavaBindingSource->Count())' != '0' " JavaPlatformJarPath="$(JavaPlatformJarPath)" ClassesOutputDirectory="$(_AndroidIntermediateBindingJavaClassDirectory)" - ClassesZip="$(_AndroidIntermediateBindingClassesZip)" StubSourceDirectory="$(_AndroidIntermediateBindingJavaSourceDirectory)" JavaSourceFiles="@(_JavaBindingSource)" ToolPath="$(JavacToolPath)" @@ -126,6 +125,11 @@ It is shared between "legacy" binding projects and .NET 7+ projects. IntermediateOutputPath="$(IntermediateOutputPath)" AssemblyIdentityMapFile="$(_AndroidLibrayProjectAssemblyMapFile)" /> + Date: Thu, 20 Aug 2026 14:36:33 +0200 Subject: [PATCH 4/5] [build] Avoid binding JARs in app builds Pass bound Java class files directly to class-parse, javac, and D8/R8. Retain JAR creation for library packaging and legacy jar2xml builds. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Xamarin.Android.Bindings.ClassParse.targets | 4 ++-- .../Xamarin/Android/Xamarin.Android.Javac.targets | 11 ++++++++++- .../Tests/Xamarin.Android.Build.Tests/BuildTest.cs | 8 ++++++++ .../Xamarin.Android.Common.targets | 1 + .../Xamarin.Android.D8.targets | 1 + 5 files changed, 22 insertions(+), 3 deletions(-) diff --git a/src/Xamarin.Android.Build.Tasks/MSBuild/Xamarin/Android/Xamarin.Android.Bindings.ClassParse.targets b/src/Xamarin.Android.Build.Tasks/MSBuild/Xamarin/Android/Xamarin.Android.Bindings.ClassParse.targets index 4e1b6e05063..5a9e9a29c69 100644 --- a/src/Xamarin.Android.Build.Tasks/MSBuild/Xamarin/Android/Xamarin.Android.Bindings.ClassParse.targets +++ b/src/Xamarin.Android.Build.Tasks/MSBuild/Xamarin/Android/Xamarin.Android.Bindings.ClassParse.targets @@ -31,7 +31,7 @@ This file is only used by binding projects. @@ -50,7 +50,7 @@ This file is only used by binding projects. + <_JavaBindingClassDirectory + Condition=" '$(AndroidApplication)' == 'true' and '$(AndroidClassParser)' != 'jar2xml' " + Include="$(_AndroidIntermediateBindingJavaClassDirectory)" + /> + <_JavaBindingClassFile + Condition=" '$(AndroidApplication)' == 'true' and '$(AndroidClassParser)' != 'jar2xml' " + Include="$(_AndroidIntermediateBindingJavaClassDirectory)**\*.class" + /> @@ -170,7 +179,7 @@ It is shared between "legacy" binding projects and .NET 7+ projects. JavaSourceFiles="@(_JavaSource)" ToolPath="$(JavacToolPath)" ToolExe="$(JavacToolExe)" - Jars="@(_JavaLibrariesToCompile);@(_ReferenceJavaLibs)" + Jars="@(_JavaBindingClassDirectory);@(_JavaLibrariesToCompile);@(_ReferenceJavaLibs)" JavacTargetVersion="$(JavacTargetVersion)" JavacSourceVersion="$(JavacSourceVersion)" JdkVersion="$(_JdkVersion)" diff --git a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/BuildTest.cs b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/BuildTest.cs index 63a575f42a2..417b0494689 100644 --- a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/BuildTest.cs +++ b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/BuildTest.cs @@ -2162,6 +2162,14 @@ public static Toolbar GetToolbar (Context context) { b.ThrowOnBuildFailure = false; Assert.IsTrue (b.Build (proj), "Build should have succeeded"); + var intermediate = Path.Combine (Root, b.ProjectDirectory, proj.IntermediateOutputPath); + var bindingJar = Path.Combine (intermediate, "binding", "bin", $"{proj.ProjectName}.jar"); + FileAssert.DoesNotExist (bindingJar, "Application builds should pass bound Java class files directly to D8/R8."); + var dexFile = Path.Combine (intermediate, "android", "bin", "classes.dex"); + FileAssert.Exists (dexFile); + Assert.IsTrue (DexUtils.ContainsClass ("Lcom/unnamedproject/unnamedproject/ToolbarEx;", dexFile, AndroidSdkPath), + $"`{dexFile}` should contain the bound Java source."); + Assert.IsTrue (b.Clean (proj), "Clean should have succeeded."); } } diff --git a/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Common.targets b/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Common.targets index bf3311e2a85..3be0e3b6793 100644 --- a/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Common.targets +++ b/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Common.targets @@ -1967,6 +1967,7 @@ because xbuild doesn't support framework reference assemblies. <_CompileToDalvikInputs> @(_AndroidMSBuildAllProjects) ;@(_JavaLibrariesToCompileForApp) + ;@(_JavaBindingClassFile) ;@(AndroidExternalJavaLibrary) ;$(_AndroidCompileJavaStampFile) ;@(ProguardConfiguration) diff --git a/src/Xamarin.Android.Build.Tasks/Xamarin.Android.D8.targets b/src/Xamarin.Android.Build.Tasks/Xamarin.Android.D8.targets index 1b6c320f845..9690a9ebf57 100644 --- a/src/Xamarin.Android.Build.Tasks/Xamarin.Android.D8.targets +++ b/src/Xamarin.Android.Build.Tasks/Xamarin.Android.D8.targets @@ -52,6 +52,7 @@ Copyright (C) 2018 Xamarin. All rights reserved. <_AndroidD8MapDiagnostics Condition=" '$(AndroidD8IgnoreWarnings)' == 'true' " Include="warning" To="info" /> <_AndroidR8MapDiagnostics Condition=" '$(AndroidR8IgnoreWarnings)' == 'true' " Include="warning" To="info" /> <_JavaClassFile Include="$(_AndroidIntermediateJavaClassDirectory)**\*.class" /> + <_JavaClassFile Include="@(_JavaBindingClassFile)" /> <_R8KeepJavaSource Include="@(AndroidJavaSource)" Condition=" '%(AndroidJavaSource.Bind)' != 'True' " /> From 5815d1fc798960bde1e1286657fc69a28a0811d3 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Thu, 20 Aug 2026 16:30:55 +0200 Subject: [PATCH 5/5] [tests] Exercise bound Java source in app builds Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Xamarin.Android.Build.Tests/BuildTest.cs | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/BuildTest.cs b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/BuildTest.cs index 417b0494689..5025442ac33 100644 --- a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/BuildTest.cs +++ b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/BuildTest.cs @@ -2152,9 +2152,23 @@ public static Toolbar GetToolbar (Context context) { } } ", - Encoding = Encoding.ASCII + Encoding = Encoding.ASCII, + Metadata = { { "Bind", "True" } }, }, - } + }, + Sources = { + new BuildItem.Source ("UseToolbarEx.cs") { + TextContent = () => """ +using Android.Content; + +class UseToolbarEx +{ + public AndroidX.AppCompat.Widget.Toolbar GetToolbar (Context context) => + Com.Unnamedproject.Unnamedproject.ToolbarEx.GetToolbar (context); +} +""", + }, + }, }; proj.SetRuntime (runtime); proj.PackageReferences.Add (KnownPackages.AndroidXAppCompat);