From 22a6f833a55fa6fa5af8e28928e164b01c5b17fa Mon Sep 17 00:00:00 2001 From: Jonathan Peppers Date: Thu, 13 Aug 2026 16:51:31 -0500 Subject: [PATCH 1/3] Fix Bind=false binding dependencies Treat Bind=false JAR and AAR inputs as reference-only dependencies so their types resolve during binding generation without generating managed bindings for them. Preserve packaging metadata and process source and reference inputs through single task invocations. Fixes #10481 Fixes #10668 Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 5e7acc46-f300-4f95-802f-96c02ef45817 --- .../Java.Interop/tools/class-parse/Program.cs | 65 ++++++++++--- .../tools/generator/CodeGenerator.cs | 13 ++- .../tools/generator/CodeGeneratorOptions.cs | 5 + .../JavaTypeResolutionFixups.cs | 8 +- .../Xamarin.Android.AvailableItems.targets | 2 + ...amarin.Android.Bindings.ClassParse.targets | 10 ++ .../Xamarin.Android.Bindings.Core.targets | 4 + ...osoft.Android.Sdk.AndroidLibraries.targets | 6 +- .../Tasks/ClassParse.cs | 11 +++ .../Tasks/ExtractJarsFromAar.cs | 43 +++++--- .../Tasks/Generator.cs | 4 + .../BindingBuildTest.cs | 97 ++++++++++++++++--- .../Tasks/AndroidDotnetToolTests.cs | 34 +++++++ .../Tasks/ExtractJarsFromAarTests.cs | 36 ++++++- 14 files changed, 290 insertions(+), 48 deletions(-) diff --git a/external/Java.Interop/tools/class-parse/Program.cs b/external/Java.Interop/tools/class-parse/Program.cs index bf3b2f17813..89335bf35c8 100644 --- a/external/Java.Interop/tools/class-parse/Program.cs +++ b/external/Java.Interop/tools/class-parse/Program.cs @@ -23,8 +23,10 @@ public static void Main (string[] args) int verbosity = 0; bool autorename = false; var outputFile = (string) null; + var referenceOutputFile = (string) null; string platform = null; var docsPaths = new List (); + var referenceFiles = new List (); var p = new OptionSet () { "usage: class-dump [-dump] FILES [@RESPONSE-FILES]", "", @@ -37,6 +39,12 @@ public static void Main (string[] args) { "o=", "Write output to {PATH}.", v => outputFile = v }, + { "reference=", + "Reference .class or .jar {FILE}.", + v => referenceFiles.Add (v) }, + { "reference-output=", + "Write the reference API to {PATH}.", + v => referenceOutputFile = v }, { "docspath=", "Documentation {PATH} for parameter fixup", doc => docsPaths.Add (doc) }, @@ -67,18 +75,32 @@ public static void Main (string[] args) } if (docsType) Console.WriteLine ("class-parse: --docstype is obsolete and no longer a valid option."); - var output = outputFile == null - ? Console.Out - : (TextWriter) new StreamWriter (outputFile, append: false, encoding: new UTF8Encoding (encoderShouldEmitUTF8Identifier: false)); Log.OnLog = (t, v, m, a) => { Console.Error.WriteLine(m, a); }; - var globalClassPath = CreateClassPath (platform, docsPaths, autorename); + var globalClassPath = LoadClassPath (files, platform, docsPaths, autorename, dump, verbosity); + WriteOutput (globalClassPath, outputFile, dump); + if (referenceFiles.Count > 0) { + if (referenceOutputFile == null) { + Console.Error.WriteLine ("class-parse: --reference-output is required when using --reference."); + Environment.ExitCode = 1; + return; + } + var referenceClassPath = LoadClassPath (referenceFiles, platform, new List (), autoRename: false, dump: false, verbosity: verbosity); + WriteOutput (referenceClassPath, referenceOutputFile, dump: false); + } else if (referenceOutputFile != null && File.Exists (referenceOutputFile)) { + File.Delete (referenceOutputFile); + } + } + + static ClassPath LoadClassPath (IEnumerable files, string platform, List docsPaths, bool autoRename, bool dump, int verbosity) + { + var globalClassPath = CreateClassPath (platform, docsPaths, autoRename); var classPaths = new List (); foreach (var file in files) { try { if (ClassPath.IsJmodFile (file) || ClassPath.IsJarFile (file)) { - var cp = CreateClassPath (platform, docsPaths, autorename); + var cp = CreateClassPath (platform, docsPaths, autoRename); cp.Load (file); classPaths.Add (cp); continue; @@ -102,20 +124,31 @@ public static void Main (string[] args) foreach (var cp in classPaths) { globalClassPath.Add (cp, removeModules: !dump); } - if (!dump) { - globalClassPath.SaveXmlDescription (output); - } else { - bool first = true; - foreach (var c in globalClassPath.GetClassFiles ()) { - if (!first) { - output.WriteLine (); + return globalClassPath; + } + + static void WriteOutput (ClassPath classPath, string outputFile, bool dump) + { + var output = outputFile == null + ? Console.Out + : (TextWriter) new StreamWriter (outputFile, append: false, encoding: new UTF8Encoding (encoderShouldEmitUTF8Identifier: false)); + try { + if (!dump) { + classPath.SaveXmlDescription (output); + } else { + bool first = true; + foreach (var c in classPath.GetClassFiles ()) { + if (!first) { + output.WriteLine (); + } + first = false; + DumpClassFile (c, output); } - first = false; - DumpClassFile (c, output); } + } finally { + if (outputFile != null) + output.Close (); } - if (outputFile != null) - output.Close (); } static ClassPath CreateClassPath (string platform, List docsPaths, bool autoRename) diff --git a/external/Java.Interop/tools/generator/CodeGenerator.cs b/external/Java.Interop/tools/generator/CodeGenerator.cs index fc04215f5ca..d21c4c163b6 100644 --- a/external/Java.Interop/tools/generator/CodeGenerator.cs +++ b/external/Java.Interop/tools/generator/CodeGenerator.cs @@ -110,7 +110,7 @@ static void Run (CodeGeneratorOptions options, DirectoryAssemblyResolver resolve // Resolve types using Java.Interop.Tools.JavaTypeSystem if (is_classparse) { var output_xml = api_xml_adjuster_output ?? Path.Combine (Path.GetDirectoryName (filename), Path.GetFileName (filename) + ".adjusted"); - JavaTypeResolutionFixups.Fixup (filename, output_xml, resolver, references.Distinct ().ToArray (), resolverCache, options); + JavaTypeResolutionFixups.Fixup (filename, output_xml, resolver, references.Distinct ().ToArray (), options.JavaReferenceApiXml.ToArray (), resolverCache, options); if (only_xml_adjuster) return; @@ -120,6 +120,17 @@ static void Run (CodeGeneratorOptions options, DirectoryAssemblyResolver resolve apiXmlFile = filename; } + foreach (var javaReference in options.JavaReferenceApiXml) { + var referenceApi = ApiXmlDocument.Load (javaReference, api_level, product_version); + if (referenceApi is null) + continue; + var referenceGens = XmlApiImporter.Parse (referenceApi.ApiDocument, opt); + if (referenceGens is null) + continue; + foreach (var referenceGen in referenceGens) + AddTypeToTable (opt, referenceGen); + } + foreach (var reference in references.Distinct ()) { try { Report.Verbose (0, "resolving assembly {0}.", reference); diff --git a/external/Java.Interop/tools/generator/CodeGeneratorOptions.cs b/external/Java.Interop/tools/generator/CodeGeneratorOptions.cs index 3995279ef6c..b6a564df0c7 100644 --- a/external/Java.Interop/tools/generator/CodeGeneratorOptions.cs +++ b/external/Java.Interop/tools/generator/CodeGeneratorOptions.cs @@ -20,6 +20,7 @@ public CodeGeneratorOptions () LibraryPaths = new Collection (); AnnotationsZipFiles = new Collection (); JavadocXmlFiles = new Collection (); + JavaReferenceApiXml = new Collection (); } public string ApiLevel {get; set;} @@ -30,6 +31,7 @@ public CodeGeneratorOptions () public Collection FixupFiles {get; private set;} public Collection LibraryPaths {get; private set;} public Collection JavadocXmlFiles {get; private set;} + public Collection JavaReferenceApiXml {get; private set;} public bool GlobalTypeNames {get; set;} public bool OnlyBindPublicTypes {get; set;} public string ApiDescriptionFile {get; set;} @@ -100,6 +102,9 @@ public static CodeGeneratorOptions Parse (string[] args) { "r|ref=", "{ASSEMBLY} to reference.", v => opts.AssemblyReferences.Add (v) }, + { "java-reference=", + "Java API XML {FILE} containing reference-only types.", + v => opts.JavaReferenceApiXml.Add (v) }, { "sdk-platform|api-level=", "SDK Platform {VERSION}/API level.", v => opts.ApiLevel = v }, diff --git a/external/Java.Interop/tools/generator/Java.Interop.Tools.Generator.Transformation/JavaTypeResolutionFixups.cs b/external/Java.Interop/tools/generator/Java.Interop.Tools.Generator.Transformation/JavaTypeResolutionFixups.cs index bf830145777..e8a3e59f433 100644 --- a/external/Java.Interop/tools/generator/Java.Interop.Tools.Generator.Transformation/JavaTypeResolutionFixups.cs +++ b/external/Java.Interop/tools/generator/Java.Interop.Tools.Generator.Transformation/JavaTypeResolutionFixups.cs @@ -15,12 +15,18 @@ public static class JavaTypeResolutionFixups // This fixup ensures all referenced Java types can be resolved, and // removes types and members that rely on unresolvable Java types. - public static void Fixup (string xmlFile, string outputXmlFile, DirectoryAssemblyResolver resolver, string [] references, TypeDefinitionCache cache, CodeGeneratorOptions opt) + public static void Fixup (string xmlFile, string outputXmlFile, DirectoryAssemblyResolver resolver, string [] references, string [] javaReferences, TypeDefinitionCache cache, CodeGeneratorOptions opt) { // Parse api.xml var type_collection = JavaXmlApiImporter.Parse (xmlFile); var options = new ApiImporterOptions (); + foreach (var javaReference in javaReferences) { + var referenceTypes = JavaXmlApiImporter.Parse (javaReference); + foreach (var type in referenceTypes.TypesFlattened.Values) + type_collection.AddReferencedType (type); + } + if (opt.CodeGenerationTarget == CodeGenerationTarget.JavaInterop1) { options.SupportedTypeMapAttributes.Clear (); options.SupportedTypeMapAttributes.Add ("Java.Interop.JniTypeSignatureAttribute"); diff --git a/src/Xamarin.Android.Build.Tasks/MSBuild/Xamarin/Android/Xamarin.Android.AvailableItems.targets b/src/Xamarin.Android.Build.Tasks/MSBuild/Xamarin/Android/Xamarin.Android.AvailableItems.targets index 484356c5fd8..220a5f8ca3f 100644 --- a/src/Xamarin.Android.Build.Tasks/MSBuild/Xamarin/Android/Xamarin.Android.AvailableItems.targets +++ b/src/Xamarin.Android.Build.Tasks/MSBuild/Xamarin/Android/Xamarin.Android.AvailableItems.targets @@ -74,7 +74,9 @@ This item group populates the Build Action drop-down in IDEs. + <_AndroidReferenceLibraryProjectZip Include="@(AndroidLibrary)" Condition=" '%(AndroidLibrary.Extension)' == '.aar' and '%(AndroidLibrary.Bind)' != 'true' " /> + 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 604dc1ea93b..7fc84c8dde9 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 @@ -35,17 +35,26 @@ This file is only used by binding projects. + + <_JavaReferenceApiXml + Include="$(ApiOutputFile).reference.class-parse" + Condition=" ('@(EmbeddedReferenceJar->Count())' != '0' or '@(ReferenceJar->Count())' != '0') and Exists('$(ApiOutputFile).reference.class-parse') " + /> + + diff --git a/src/Xamarin.Android.Build.Tasks/MSBuild/Xamarin/Android/Xamarin.Android.Bindings.Core.targets b/src/Xamarin.Android.Build.Tasks/MSBuild/Xamarin/Android/Xamarin.Android.Bindings.Core.targets index e627ff9c408..37047bc3904 100644 --- a/src/Xamarin.Android.Build.Tasks/MSBuild/Xamarin/Android/Xamarin.Android.Bindings.Core.targets +++ b/src/Xamarin.Android.Build.Tasks/MSBuild/Xamarin/Android/Xamarin.Android.Bindings.Core.targets @@ -102,6 +102,7 @@ It is shared between "legacy" binding projects and .NET 5 projects. CodegenTarget="$(AndroidCodegenTarget)" AndroidApiLevel="$(_AndroidApiLevel)" ApiXmlInput="$(ApiOutputFile)" + JavaReferenceApiXml="@(_JavaReferenceApiXml)" AnnotationsZipFiles="@(AnnotationsZip)" AssemblyName="$(AssemblyName)" JavadocVerbosity="$(AndroidJavadocVerbosity)" @@ -154,6 +155,7 @@ It is shared between "legacy" binding projects and .NET 5 projects. + @@ -181,6 +183,8 @@ It is shared between "legacy" binding projects and .NET 5 projects. + + diff --git a/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.AndroidLibraries.targets b/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.AndroidLibraries.targets index 6d2d016dabe..676ad51b157 100644 --- a/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.AndroidLibraries.targets +++ b/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.AndroidLibraries.targets @@ -110,12 +110,16 @@ projects. diff --git a/src/Xamarin.Android.Build.Tasks/Tasks/ClassParse.cs b/src/Xamarin.Android.Build.Tasks/Tasks/ClassParse.cs index ece680ed7a6..68038af1f4d 100644 --- a/src/Xamarin.Android.Build.Tasks/Tasks/ClassParse.cs +++ b/src/Xamarin.Android.Build.Tasks/Tasks/ClassParse.cs @@ -18,6 +18,10 @@ public class ClassParse : AndroidDotnetToolTask [Required] public ITaskItem[] SourceJars { get; set; } = []; + public string? ReferenceOutputFile { get; set; } + + public ITaskItem []? ReferenceJars { get; set; } + public ITaskItem []? DocumentationPaths { get; set; } protected override string GenerateCommandLineCommands () @@ -34,6 +38,13 @@ protected override string GenerateCommandLineCommands () foreach (var doc in DocumentationPaths) WriteLine (sw, $"--docspath=\"{doc}\""); + if (!ReferenceOutputFile.IsNullOrEmpty ()) + WriteLine (sw, $"--reference-output=\"{ReferenceOutputFile}\""); + + if (ReferenceJars != null) + foreach (var reference in ReferenceJars) + WriteLine (sw, $"--reference=\"{reference}\""); + foreach (var doc in SourceJars) WriteLine (sw, $"\"{doc}\""); } diff --git a/src/Xamarin.Android.Build.Tasks/Tasks/ExtractJarsFromAar.cs b/src/Xamarin.Android.Build.Tasks/Tasks/ExtractJarsFromAar.cs index f5fc112a8a0..cf33fb9298f 100644 --- a/src/Xamarin.Android.Build.Tasks/Tasks/ExtractJarsFromAar.cs +++ b/src/Xamarin.Android.Build.Tasks/Tasks/ExtractJarsFromAar.cs @@ -24,19 +24,38 @@ public class ExtractJarsFromAar : AndroidTask public string []? Libraries { get; set; } + public string? OutputReferenceJarsDirectory { get; set; } + + public string? OutputReferenceAnnotationsDirectory { get; set; } + + public string []? ReferenceLibraries { get; set; } + public override bool RunTask () { - if (Libraries == null || Libraries.Length == 0) - return true; - var memoryStream = MemoryStreamPool.Shared.Rent (); try { - var jars = new HashSet (StringComparer.OrdinalIgnoreCase); - var annotations = new HashSet (StringComparer.OrdinalIgnoreCase); - foreach (var library in Libraries) { + ExtractLibraries (Libraries, OutputJarsDirectory, OutputAnnotationsDirectory, memoryStream); + if (!OutputReferenceJarsDirectory.IsNullOrEmpty () && !OutputReferenceAnnotationsDirectory.IsNullOrEmpty ()) { + ExtractLibraries (ReferenceLibraries, OutputReferenceJarsDirectory, OutputReferenceAnnotationsDirectory, memoryStream); + } else if (ReferenceLibraries?.Length > 0) { + Log.LogError ("Reference output directories are required when reference libraries are specified."); + } + } finally { + MemoryStreamPool.Shared.Return (memoryStream); + } + + return !Log.HasLoggedErrors; + } + + void ExtractLibraries (string []? libraries, string outputJarsDirectory, string outputAnnotationsDirectory, MemoryStream memoryStream) + { + var jars = new HashSet (StringComparer.OrdinalIgnoreCase); + var annotations = new HashSet (StringComparer.OrdinalIgnoreCase); + if (libraries != null) { + foreach (var library in libraries) { bool isAar = library.EndsWith (".aar", StringComparison.OrdinalIgnoreCase); - var jarOutputDirectory = Path.Combine (OutputJarsDirectory, Path.GetFileName (library)); - var annotationOutputDirectory = Path.Combine (OutputAnnotationsDirectory, Path.GetFileName (library)); + var jarOutputDirectory = Path.Combine (outputJarsDirectory, Path.GetFileName (library)); + var annotationOutputDirectory = Path.Combine (outputAnnotationsDirectory, Path.GetFileName (library)); using (var zip = MonoAndroidHelper.ReadZipFile (library)) { foreach (var entry in zip) { if (entry.IsDirectory) @@ -63,13 +82,9 @@ public override bool RunTask () } } } - DeleteUnknownFiles (OutputJarsDirectory, jars); - DeleteUnknownFiles (OutputAnnotationsDirectory, annotations); - } finally { - MemoryStreamPool.Shared.Return (memoryStream); } - - return !Log.HasLoggedErrors; + DeleteUnknownFiles (outputJarsDirectory, jars); + DeleteUnknownFiles (outputAnnotationsDirectory, annotations); } bool IsUnderDirectory (string resolvedPath, string targetDirectory, string entryName, string archivePath) diff --git a/src/Xamarin.Android.Build.Tasks/Tasks/Generator.cs b/src/Xamarin.Android.Build.Tasks/Tasks/Generator.cs index e2bc343b358..64594d09ffa 100644 --- a/src/Xamarin.Android.Build.Tasks/Tasks/Generator.cs +++ b/src/Xamarin.Android.Build.Tasks/Tasks/Generator.cs @@ -64,6 +64,7 @@ public class BindingsGenerator : AndroidDotnetToolTask public ITaskItem[]? JavadocXml { get; set; } public string? JavadocVerbosity { get; set; } + public ITaskItem []? JavaReferenceApiXml { get; set; } private List> transform_files = new List> (); @@ -187,6 +188,9 @@ protected override string GenerateCommandLineCommands () if (ReferencedManagedLibraries != null) foreach (var lib in ReferencedManagedLibraries) WriteLine (sw, $"--ref=\"{Path.GetFullPath (lib.ItemSpec)}\""); + if (JavaReferenceApiXml != null) + foreach (var reference in JavaReferenceApiXml) + WriteLine (sw, $"--java-reference=\"{Path.GetFullPath (reference.ItemSpec)}\""); if (AnnotationsZipFiles != null) foreach (var zip in AnnotationsZipFiles) WriteLine (sw, $"--annotations=\"{Path.GetFullPath (zip.ItemSpec)}\""); diff --git a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/BindingBuildTest.cs b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/BindingBuildTest.cs index 436c2852a8b..615cda6eaa8 100644 --- a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/BindingBuildTest.cs +++ b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/BindingBuildTest.cs @@ -520,7 +520,7 @@ public void OnUpdate (Java.Lang.Object p0) } [Test] - public void AndroidLibraryPackFalseExcludesJarFromAar ([Values (AndroidRuntime.CoreCLR, AndroidRuntime.NativeAOT)] AndroidRuntime runtime) + public void AndroidLibraryPackMetadataIsPreservedInNuGet ([Values (AndroidRuntime.CoreCLR, AndroidRuntime.NativeAOT)] AndroidRuntime runtime) { const bool isRelease = true; if (IgnoreUnsupportedConfiguration (runtime, release: isRelease)) { @@ -542,23 +542,92 @@ public void AndroidLibraryPackFalseExcludesJarFromAar ([Values (AndroidRuntime.C }); using (var bindingBuilder = CreateDllBuilder ()) { - Assert.IsTrue (bindingBuilder.Build (binding), "binding build should have succeeded"); + bindingBuilder.Target = "Pack"; + Assert.IsTrue (bindingBuilder.Build (binding), "`Pack` should succeed"); + + var nupkgPath = Path.Combine (Root, bindingBuilder.ProjectDirectory, binding.OutputPath, "UnnamedProject.1.0.0.nupkg"); + FileAssert.Exists (nupkgPath); + using (var nupkg = ZipArchive.Open (nupkgPath, FileMode.Open)) { + var aarEntry = nupkg.Single (entry => entry.FullName.EndsWith ("/UnnamedProject.aar", StringComparison.Ordinal)); + using var aarStream = new MemoryStream (); + aarEntry.Extract (aarStream); + aarStream.Position = 0; + using var aar = ZipArchive.Open (aarStream); + Assert.AreEqual (1, aar.Count (entry => entry.FullName.StartsWith ("libs/", StringComparison.Ordinal) && entry.FullName.EndsWith (".jar", StringComparison.Ordinal)), + "The generated AAR should contain only the Bind='false', Pack='true' JAR."); + } + } + } - // Check that the AAR file was created - var aarPath = Path.Combine (Root, bindingBuilder.ProjectDirectory, binding.OutputPath, "UnnamedProject.aar"); - FileAssert.Exists (aarPath); + [Test] + public void BindFalseAndroidLibraryResolvesGeneratorTypes ( + [Values (".jar", ".aar")] string extension, + [Values (AndroidRuntime.CoreCLR, AndroidRuntime.NativeAOT)] AndroidRuntime runtime) + { + const bool isRelease = true; + if (IgnoreUnsupportedConfiguration (runtime, release: isRelease)) { + return; + } - // Extract and examine AAR contents - using (var aar = ZipArchive.Open (aarPath, FileMode.Open)) { - // test-pack-false.jar should NOT be in the AAR because Pack='false' - var packFalseEntry = aar.Where (e => e.FullName.Contains ("test-pack-false")).FirstOrDefault (); - Assert.IsNull (packFalseEntry, "Jar with Pack='false' should not be included in AAR"); + var dependency = extension == ".aar" ? CreateAar (ResourceData.JavaSourceJarTestJar) : ResourceData.JavaSourceJarTestJar; + var binding = new XamarinAndroidBindingProject { + IsRelease = isRelease, + AndroidClassParser = "class-parse", + Sources = { + new BuildItem.Source ("JavaSourceJarTest.cs") { + TextContent = () => @" +namespace Com.Xamarin.Android.Test.Msbuildtest { + [global::Android.Runtime.Register (""com/xamarin/android/test/msbuildtest/JavaSourceJarTest"", DoNotGenerateAcw = true)] + public class JavaSourceJarTest : global::Java.Lang.Object { + } +} +", + }, + }, + AndroidJavaSources = { + new AndroidItem.AndroidJavaSource ("UsesDependency.java") { + Encoding = Encoding.ASCII, + TextContent = () => @" +package com.example; - // test-pack-true.jar should be in the AAR (default Pack='true') - var packTrueEntry = aar.Where (e => e.FullName.Contains ("test-pack-true") || e.FullName.StartsWith ("libs/")).FirstOrDefault (); - Assert.IsNotNull (packTrueEntry, "Jar with Pack='true' (default) should be included in AAR"); - } +import com.xamarin.android.test.msbuildtest.JavaSourceJarTest; + +public class UsesDependency { + public JavaSourceJarTest echo (JavaSourceJarTest value) { + return value; + } +} +", + Metadata = { { "Bind", "true" } }, + }, + }, + }; + binding.SetRuntime (runtime); + binding.OtherBuildItems.Add (new AndroidItem.AndroidLibrary ($"dependency{extension}") { + BinaryContent = () => dependency, + MetadataValues = "Bind=false", + }); + using var builder = CreateDllBuilder (); + Assert.IsTrue (builder.Build (binding), "Binding build should have succeeded."); + var apiXml = builder.Output.GetIntermediaryPath ("api.xml"); + FileAssert.Exists (apiXml); + StringAssert.Contains ("com.xamarin.android.test.msbuildtest.JavaSourceJarTest", File.ReadAllText (apiXml), + "Bind='false' dependency types should be available while resolving the generated API."); + var generatedSourceDirectory = Path.Combine (Root, builder.ProjectDirectory, binding.IntermediateOutputPath, "generated", "src"); + var generatedSources = Directory.EnumerateFiles (generatedSourceDirectory, "*.cs", SearchOption.AllDirectories).ToArray (); + Assert.IsTrue (generatedSources.Any (source => File.ReadAllText (source).Contains (" Echo (", StringComparison.Ordinal)), + "Members that use Bind='false' dependency types should generate managed bindings."); + Assert.IsFalse (generatedSources.Any (source => Path.GetFileName (source).Contains ("JavaSourceJarTest", StringComparison.Ordinal)), + "Bind='false' dependency types should not generate managed bindings."); + } + + static byte [] CreateAar (byte [] classesJar) + { + using var stream = new MemoryStream (); + using (var aar = ZipArchive.Open (stream)) { + aar.AddStream (new MemoryStream (classesJar), "classes.jar"); } + return stream.ToArray (); } [Test] diff --git a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Tasks/AndroidDotnetToolTests.cs b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Tasks/AndroidDotnetToolTests.cs index d6d03315ad2..adab324a77e 100644 --- a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Tasks/AndroidDotnetToolTests.cs +++ b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Tasks/AndroidDotnetToolTests.cs @@ -43,6 +43,32 @@ public void ShouldUseFullToolPath () Assert.True (classParseTask.Execute (), "Task should have succeeded."); Assert.IsTrue (messages.Any (m => m.Message.StartsWith (dotnetPath)), "Task did not use expected tool path."); } + + [Test] + public void ClassParseWritesReferenceArguments () + { + var outputDirectory = Path.Combine (Root, "temp", TestName); + Directory.CreateDirectory (outputDirectory); + TestOutputDirectories [TestContext.CurrentContext.Test.ID] = outputDirectory; + var output = Path.Combine (outputDirectory, "api.xml"); + var referenceOutput = Path.Combine (outputDirectory, "reference-api.xml"); + var sourceJar = Path.Combine (outputDirectory, "source.jar"); + var referenceJar = Path.Combine (outputDirectory, "reference.jar"); + var task = new ClassParseArgumentsTestTask { + BuildEngine = engine, + OutputFile = output, + ReferenceOutputFile = referenceOutput, + ReferenceJars = [new TaskItem (referenceJar)], + SourceJars = [new TaskItem (sourceJar)], + }; + + task.GenerateArguments (); + + var responseFile = File.ReadAllLines (Path.Combine (outputDirectory, "class-parse.rsp")); + Assert.That (responseFile, Does.Contain ($"--reference-output=\"{referenceOutput}\"")); + Assert.That (responseFile, Does.Contain ($"--reference=\"{referenceJar}\"")); + Assert.That (responseFile, Does.Contain ($"\"{sourceJar}\"")); + } } public class ClassParseTestTask : AndroidDotnetToolTask @@ -53,4 +79,12 @@ protected override string GenerateCommandLineCommands () return GetCommandLineBuilder ().ToString (); } } + + public class ClassParseArgumentsTestTask : ClassParse + { + public string GenerateArguments () + { + return GenerateCommandLineCommands (); + } + } } diff --git a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Tasks/ExtractJarsFromAarTests.cs b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Tasks/ExtractJarsFromAarTests.cs index e089ccd9dc4..0c88a8f1de3 100644 --- a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Tasks/ExtractJarsFromAarTests.cs +++ b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Tasks/ExtractJarsFromAarTests.cs @@ -92,9 +92,43 @@ public void ValidJarEntry () Assert.IsEmpty (errors, "No errors should be logged."); } + [Test] + public void ExtractsBoundAndReferenceLibraries () + { + var boundAar = CreateAarWithEntry ("bound.aar", "classes.jar"); + var referenceAar = CreateAarWithEntry ("reference.aar", "libs/helper.jar"); + var jarOutputDir = Path.Combine (path, "jars"); + var annotationOutputDir = Path.Combine (path, "annotations"); + var referenceJarOutputDir = Path.Combine (path, "reference-jars"); + var referenceAnnotationOutputDir = Path.Combine (path, "reference-annotations"); + var task = new ExtractJarsFromAar { + BuildEngine = engine, + OutputJarsDirectory = jarOutputDir, + OutputAnnotationsDirectory = annotationOutputDir, + Libraries = [boundAar], + OutputReferenceJarsDirectory = referenceJarOutputDir, + OutputReferenceAnnotationsDirectory = referenceAnnotationOutputDir, + ReferenceLibraries = [referenceAar], + }; + + Assert.IsTrue (task.Execute (), "Task should extract both library categories."); + Assert.IsTrue (File.Exists (Path.Combine (jarOutputDir, "bound.aar", "classes.jar"))); + var extractedReference = Path.Combine (referenceJarOutputDir, "reference.aar", "libs", "helper.jar"); + Assert.IsTrue (File.Exists (extractedReference)); + + task.ReferenceLibraries = []; + Assert.IsTrue (task.Execute (), "Task should clean an empty reference category."); + Assert.IsFalse (File.Exists (extractedReference), "Stale reference JAR should be deleted."); + } + string CreateAarWithEntry (string entryName) { - var aarPath = Path.Combine (path, "test.aar"); + return CreateAarWithEntry ("test.aar", entryName); + } + + string CreateAarWithEntry (string fileName, string entryName) + { + var aarPath = Path.Combine (path, fileName); using (var stream = new FileStream (aarPath, FileMode.Create)) using (var archive = new ZipArchive (stream, ZipArchiveMode.Create)) { var entry = archive.CreateEntry (entryName); From 0f028b760c0e587f71bcc2484a3fd1be6e9614b5 Mon Sep 17 00:00:00 2001 From: Jonathan Peppers Date: Thu, 13 Aug 2026 17:21:17 -0500 Subject: [PATCH 2/3] Address binding dependency review feedback Resolve relative class-parse output paths safely and include the conditional reference API in incremental target outputs. Cover regeneration when the reference API file is missing. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 5e7acc46-f300-4f95-802f-96c02ef45817 --- .../Xamarin.Android.Bindings.ClassParse.targets | 15 +++++++++++++-- .../Tasks/ClassParse.cs | 2 +- .../BindingBuildTest.cs | 5 +++++ 3 files changed, 19 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 7fc84c8dde9..16fb0db732e 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 @@ -13,10 +13,21 @@ This file is only used by binding projects. + + + <_ExportJarToXmlOutputs Remove="@(_ExportJarToXmlOutputs)" /> + <_ExportJarToXmlOutputs Include="$(ApiOutputFile)" /> + <_ExportJarToXmlOutputs + Include="$(ApiOutputFile).reference.class-parse" + Condition=" '@(EmbeddedReferenceJar->Count())' != '0' or '@(ReferenceJar->Count())' != '0' " + /> + + + + Outputs="@(_ExportJarToXmlOutputs)"> diff --git a/src/Xamarin.Android.Build.Tasks/Tasks/ClassParse.cs b/src/Xamarin.Android.Build.Tasks/Tasks/ClassParse.cs index 68038af1f4d..f8375eff8f2 100644 --- a/src/Xamarin.Android.Build.Tasks/Tasks/ClassParse.cs +++ b/src/Xamarin.Android.Build.Tasks/Tasks/ClassParse.cs @@ -28,7 +28,7 @@ protected override string GenerateCommandLineCommands () { var cmd = GetCommandLineBuilder (); - var responseFile = Path.Combine (Path.GetDirectoryName (OutputFile), "class-parse.rsp"); + var responseFile = Path.Combine (Path.GetDirectoryName (Path.GetFullPath (OutputFile)) ?? "", "class-parse.rsp"); Log.LogDebugMessage ("[class-parse] response file: {0}", responseFile); using (var sw = new StreamWriter (responseFile, append: false, encoding: Files.UTF8withoutBOM)) { diff --git a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/BindingBuildTest.cs b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/BindingBuildTest.cs index 615cda6eaa8..b3b965a027e 100644 --- a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/BindingBuildTest.cs +++ b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/BindingBuildTest.cs @@ -611,6 +611,11 @@ public JavaSourceJarTest echo (JavaSourceJarTest value) { Assert.IsTrue (builder.Build (binding), "Binding build should have succeeded."); var apiXml = builder.Output.GetIntermediaryPath ("api.xml"); FileAssert.Exists (apiXml); + var referenceApiXml = $"{apiXml}.reference.class-parse"; + FileAssert.Exists (referenceApiXml); + File.Delete (referenceApiXml); + Assert.IsTrue (builder.Build (binding, doNotCleanupOnUpdate: true, saveProject: false), "Missing reference API recovery build should have succeeded."); + FileAssert.Exists (referenceApiXml); StringAssert.Contains ("com.xamarin.android.test.msbuildtest.JavaSourceJarTest", File.ReadAllText (apiXml), "Bind='false' dependency types should be available while resolving the generated API."); var generatedSourceDirectory = Path.Combine (Root, builder.ProjectDirectory, binding.IntermediateOutputPath, "generated", "src"); From ad0e9a6005a8e4f3bf9f2d4d943f30ff13299c8a Mon Sep 17 00:00:00 2001 From: Jonathan Peppers Date: Fri, 14 Aug 2026 07:49:52 -0500 Subject: [PATCH 3/3] Address additional binding review feedback Populate the reference API item in the inputs helper and make reference extraction output directories required task inputs, avoiding a new uncoded product diagnostic. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 5e7acc46-f300-4f95-802f-96c02ef45817 --- .../Xamarin.Android.Bindings.ClassParse.targets | 11 +++++------ .../Tasks/ExtractJarsFromAar.cs | 12 +++++------- .../Tasks/ExtractJarsFromAarTests.cs | 6 ++++++ 3 files changed, 16 insertions(+), 13 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 16fb0db732e..4e1b6e05063 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 @@ -16,11 +16,16 @@ This file is only used by binding projects. <_ExportJarToXmlOutputs Remove="@(_ExportJarToXmlOutputs)" /> + <_JavaReferenceApiXml Remove="@(_JavaReferenceApiXml)" /> <_ExportJarToXmlOutputs Include="$(ApiOutputFile)" /> <_ExportJarToXmlOutputs Include="$(ApiOutputFile).reference.class-parse" Condition=" '@(EmbeddedReferenceJar->Count())' != '0' or '@(ReferenceJar->Count())' != '0' " /> + <_JavaReferenceApiXml + Include="$(ApiOutputFile).reference.class-parse" + Condition=" '@(EmbeddedReferenceJar->Count())' != '0' or '@(ReferenceJar->Count())' != '0' " + /> @@ -53,12 +58,6 @@ This file is only used by binding projects. ToolPath="$(_BindingsToolsLocation)" ToolExe="$(ClassParseToolExe)" /> - - <_JavaReferenceApiXml - Include="$(ApiOutputFile).reference.class-parse" - Condition=" ('@(EmbeddedReferenceJar->Count())' != '0' or '@(ReferenceJar->Count())' != '0') and Exists('$(ApiOutputFile).reference.class-parse') " - /> - 0) { - Log.LogError ("Reference output directories are required when reference libraries are specified."); - } + ExtractLibraries (ReferenceLibraries, OutputReferenceJarsDirectory, OutputReferenceAnnotationsDirectory, memoryStream); } finally { MemoryStreamPool.Shared.Return (memoryStream); } diff --git a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Tasks/ExtractJarsFromAarTests.cs b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Tasks/ExtractJarsFromAarTests.cs index 0c88a8f1de3..39e29f596b5 100644 --- a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Tasks/ExtractJarsFromAarTests.cs +++ b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Tasks/ExtractJarsFromAarTests.cs @@ -41,6 +41,8 @@ public void PathTraversalInJarEntry () BuildEngine = engine, OutputJarsDirectory = jarOutputDir, OutputAnnotationsDirectory = annotationOutputDir, + OutputReferenceJarsDirectory = Path.Combine (path, "reference-jars"), + OutputReferenceAnnotationsDirectory = Path.Combine (path, "reference-annotations"), Libraries = [aarPath], }; @@ -65,6 +67,8 @@ public void PathTraversalInAnnotationsEntry () BuildEngine = engine, OutputJarsDirectory = jarOutputDir, OutputAnnotationsDirectory = annotationOutputDir, + OutputReferenceJarsDirectory = Path.Combine (path, "reference-jars"), + OutputReferenceAnnotationsDirectory = Path.Combine (path, "reference-annotations"), Libraries = [aarPath], }; @@ -85,6 +89,8 @@ public void ValidJarEntry () BuildEngine = engine, OutputJarsDirectory = jarOutputDir, OutputAnnotationsDirectory = annotationOutputDir, + OutputReferenceJarsDirectory = Path.Combine (path, "reference-jars"), + OutputReferenceAnnotationsDirectory = Path.Combine (path, "reference-annotations"), Libraries = [aarPath], };