Skip to content

[Xamarin.Android.Build.Tasks] Replace ExtractJarsFromAar with built-in Unzip - #13506

Open
simonrozsival wants to merge 1 commit into
mainfrom
simonrozsival-xml-impl-extract-jars-from-aar
Open

simonrozsival wants to merge 1 commit into
mainfrom
simonrozsival-xml-impl-extract-jars-from-aar

Conversation

@simonrozsival

Copy link
Copy Markdown
Member

Context: #11212
Context: #13505
Context: https://github.com/dotnet/msbuild/blob/main/src/Tasks/Unzip.cs

The _ExtractAar target used the custom ExtractJarsFromAar task to extract *.jar and annotations.zip entries from @(LibraryProjectZip) and @(_AndroidReferenceLibraryProjectZip) into $(IntermediateOutputPath)library_project_jars, library_project_annotations and their reference_* counterparts. That is one more custom task to host, test and eventually make multithreaded, while MSBuild's <Unzip/> already supports Include patterns.

~~ Why not just <Unzip SkipUnchangedFiles="true"/>? ~~

The incrementality of the extracted files matters. _ExportJarToXml uses @(InputJar) and @(ReferenceJar) (the extracted jars) as Inputs. ExtractJarsFromAar rewrote a file only when its content changed, and it deleted files that no longer came from any archive.

<Unzip/> sets every extracted file's mtime to the zip entry's timestamp. Its SkipUnchangedFiles check compares only entry timestamp and size. Gradle/AGP-produced AARs typically use fixed timestamps (1980-02-01), so:

  • an updated classes.jar would be written with a 1980 mtime, and _ExportJarToXml would treat itself as up to date, leaving bindings stale;
  • an updated classes.jar of the same size would not be extracted at all.

~~ Approach ~~

_ExtractAar now:

  1. Extracts into $(IntermediateOutputPath)extract_aar_staging\, using three batched <Unzip/> calls per library:
    • non-.aar inputs: Include="*.jar"
    • .aar inputs: Include="classes.jar;libs/*.jar", which mirrors Files.ShouldSkipEntryInAar()
    • all inputs: Include="annotations.zip;*/annotations.zip"
  2. <Touch/>es the staged files. CopyIfChanged would otherwise skip a file whose destination is newer and has the same length.
  3. Copies them into the final directories with the existing CopyIfChanged task. It compares content, so unchanged files keep their timestamps.
  4. <Delete/>s stale files: everything under the four output directories minus the expected set, computed with an Exclude transform over %(RecursiveDir).
  5. Removes the staging directory.

This keeps the previous incremental semantics, and the target's Inputs/Outputs are unchanged. javac and D8 are unaffected: neither reads these directories (their jars come from ResolveLibraryProjectImports). The only consumers are class-parse and the generator's AnnotationsZipFiles. The added cost is one temporary disk write of the matched entries, and only when _ExtractAar runs.

<Unzip/> pattern notes: * also matches /, and patterns are separator-normalized only when Include contains ;. That is why every pattern containing a path separator is part of a ;-list.

~~ Behavior differences ~~

  • Path-traversal entries are rejected by <Unzip/>'s own zip-slip check instead of Files.GetArchiveExtractionPath(). Because <Unzip/> filters before it validates, a traversal .jar entry in an .aar that doesn't match classes.jar/libs/* is now ignored instead of failing the build. Nothing is written outside the destination in either case.
  • classes.jar matching is now case-insensitive.

ExtractJarsFromAar, its <UsingTask/> and ExtractJarsFromAarTests are removed. Files.ShouldSkipEntryInAar() and Files.GetArchiveExtractionPath() remain, because ResolveLibraryProjectImports uses them.

~~ Testing ~~

Not built or tested locally, to save disk space in this batch; validation is left to CI. Relevant coverage is in BindingBuildTest: the annotations.zip support test and the lint.jar exclusion check, plus the binding incremental build tests.

…n Unzip

_ExtractAar now stages the .jar and annotations.zip entries with
<Unzip/>, then uses the retained CopyIfChanged task to rewrite only
files whose content changed, and deletes files that are no longer in
any archive. Incremental behavior matches the removed task.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
@simonrozsival simonrozsival added ready-to-review This PR is ready to review/merge, I think any CI failures are just flaky (ignorable). and removed ready-to-review This PR is ready to review/merge, I think any CI failures are just flaky (ignorable). labels Nov 11, 2026
@simonrozsival
simonrozsival marked this pull request as ready for review November 11, 2026 14:33
Copilot AI balanced review requested due to automatic review settings November 11, 2026 14:33

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

The final copy step omits descendant-link validation, allowing archive files to be written through symlinks or junctions outside the intended extraction directories.

5 open issues
What changed in this PR

Replaces the custom AAR extraction task with MSBuild’s built-in Unzip, reducing custom task maintenance in the Android binding build pipeline.

Changes:

  • Adds staged extraction for bound and reference JARs and annotation archives.
  • Uses timestamp refresh, content-aware copying, and stale-file deletion to preserve incremental behavior.
  • Removes ExtractJarsFromAar, its task registration, and its dedicated tests.
File Description
src/​Xamarin.Android.Build.Tasks/​Tests/​Xamarin.Android.Build.Tests/​Tasks/​ExtractJarsFromAarTests.cs Removes the custom extractor’s tests.
src/​Xamarin.Android.Build.Tasks/​Tasks/​ExtractJarsFromAar.cs Removes the custom extraction implementation.
src/​Xamarin.Android.Build.Tasks/​Microsoft.Android.Sdk/​targets/​Microsoft.Android.Sdk.AndroidLibraries.targets Replaces the task invocation with staged extraction, copying, and cleanup.

Review effort: Balanced

</ItemGroup>
<!-- Unzip keeps the zip entry timestamps; touch the staged files so CopyIfChanged compares content and only rewrites changed files -->
<Touch Files="@(_ExtractAarFile)" />
<CopyIfChanged SourceFiles="@(_ExtractAarFile)" DestinationFiles="@(_ExtractAarFile->'$(IntermediateOutputPath)%(RecursiveDir)%(Filename)%(Extension)')" />
<Unzip
SourceFiles="@(_ExtractAarLibrary)"
DestinationFolder="$(_ExtractAarStagingDirectory)%(OutputPrefix)_jars\%(Filename)%(Extension)"
Include="classes.jar;libs/*.jar"
Comment on lines +122 to 138
<Unzip
SourceFiles="@(_ExtractAarLibrary)"
DestinationFolder="$(_ExtractAarStagingDirectory)%(OutputPrefix)_jars\%(Filename)%(Extension)"
Include="*.jar"
Condition=" '%(Extension)' != '.aar' "
/>
<Unzip
SourceFiles="@(_ExtractAarLibrary)"
DestinationFolder="$(_ExtractAarStagingDirectory)%(OutputPrefix)_jars\%(Filename)%(Extension)"
Include="classes.jar;libs/*.jar"
Condition=" '%(Extension)' == '.aar' "
/>
<Unzip
SourceFiles="@(_ExtractAarLibrary)"
DestinationFolder="$(_ExtractAarStagingDirectory)%(OutputPrefix)_annotations\%(Filename)%(Extension)"
Include="annotations.zip;*/annotations.zip"
/>
<_ExtractAarFile Include="$(_ExtractAarStagingDirectory)**" />
<_ExtractAarStaleFile
Include="$(IntermediateOutputPath)library_project_jars\**;$(IntermediateOutputPath)library_project_annotations\**;$(IntermediateOutputPath)reference_library_project_jars\**;$(IntermediateOutputPath)reference_library_project_annotations\**"
Exclude="@(_ExtractAarFile->'$(IntermediateOutputPath)%(RecursiveDir)%(Filename)%(Extension)')"
Comment on lines +146 to +149
<!-- Unzip keeps the zip entry timestamps; touch the staged files so CopyIfChanged compares content and only rewrites changed files -->
<Touch Files="@(_ExtractAarFile)" />
<CopyIfChanged SourceFiles="@(_ExtractAarFile)" DestinationFiles="@(_ExtractAarFile->'$(IntermediateOutputPath)%(RecursiveDir)%(Filename)%(Extension)')" />
<Delete Files="@(_ExtractAarStaleFile)" />

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants