Skip to content

Commit

Permalink
[XABT] Use ZipArchive to build APKs.
Browse files Browse the repository at this point in the history
  • Loading branch information
jpobst committed Dec 16, 2024
1 parent 6b91b04 commit 6194d12
Show file tree
Hide file tree
Showing 5 changed files with 410 additions and 109 deletions.
76 changes: 36 additions & 40 deletions src/Xamarin.Android.Build.Tasks/Tasks/BuildArchive.cs
Original file line number Diff line number Diff line change
Expand Up @@ -43,8 +43,10 @@ public class BuildArchive : AndroidTask

public override bool RunTask ()
{
var is_aab = string.Compare (AndroidPackageFormat, "aab", true) == 0;

// Nothing needs to be compressed with app bundles. BundleConfig.json specifies the final compression mode.
if (string.Compare (AndroidPackageFormat, "aab", true) == 0)
if (is_aab)
uncompressedMethod = CompressionMethod.Default;

var refresh = true;
Expand All @@ -57,7 +59,7 @@ public override bool RunTask ()
refresh = false;
}

using var apk = new ZipArchiveEx (ApkOutputPath, FileMode.Open);
using var apk = ZipArchiveDotNet.Create (Log, ApkOutputPath, System.IO.Compression.ZipArchiveMode.Update);

// Set up AutoFlush
if (int.TryParse (ZipFlushFilesLimit, out int flushFilesLimit)) {
Expand All @@ -73,10 +75,9 @@ public override bool RunTask ()
var existingEntries = new List<string> ();

if (refresh) {
for (var i = 0; i < apk.Archive.EntryCount; i++) {
var entry = apk.Archive.ReadEntry ((ulong) i);
Log.LogDebugMessage ($"Registering item {entry.FullName}");
existingEntries.Add (entry.FullName);
foreach (var entry in apk.GetAllEntryNames ()) {
Log.LogDebugMessage ($"Registering item {entry}");
existingEntries.Add (entry);
}
}

Expand All @@ -98,6 +99,11 @@ public override bool RunTask ()
Log.LogDebugMessage ($"Fixing up malformed entry `{entry.FullName}` -> `{entryName}`");
}

if (entryName == "AndroidManifest.xml" && is_aab) {
Log.LogDebugMessage ("Renaming AndroidManifest.xml to manifest/AndroidManifest.xml");
entryName = "manifest/AndroidManifest.xml";
}

Log.LogDebugMessage ($"Deregistering item {entryName}");
existingEntries.Remove (entryName);

Expand All @@ -106,24 +112,28 @@ public override bool RunTask ()
continue;
}

if (apk.Archive.ContainsEntry (entryName)) {
ZipEntry e = apk.Archive.ReadEntry (entryName);
if (apk.ContainsEntry (entryName)) {
var e = apk.GetEntry (entryName);
// check the CRC values as the ModifiedDate is always 01/01/1980 in the aapt generated file.
if (entry.CRC == e.CRC && entry.CompressedSize == e.CompressedSize) {
Log.LogDebugMessage ($"Skipping {entryName} from {ApkInputPath} as its up to date.");
continue;
}

// Delete the existing entry so we can replace it with the new one.
apk.DeleteEntry (entryName);
}

var ms = new MemoryStream ();
entry.Extract (ms);
ms.Position = 0;
Log.LogDebugMessage ($"Refreshing {entryName} from {ApkInputPath}");
apk.Archive.AddStream (ms, entryName, compressionMethod: entry.CompressionMethod);
apk.AddEntry (ms, entryName, entry.CompressionMethod.ToCompressionLevel ());
}
}
}

apk.FixupWindowsPathSeparators ((a, b) => Log.LogDebugMessage ($"Fixing up malformed entry `{a}` -> `{b}`"));
apk.FixupWindowsPathSeparators (Log);

// Add the files to the apk
foreach (var file in FilesToAddToArchive) {
Expand All @@ -135,6 +145,8 @@ public override bool RunTask ()
return !Log.HasLoggedErrors;
}

apk_path = apk_path.Replace ('\\', '/');

// This is a temporary hack for adding files directly from inside a .jar/.aar
// into the APK. Eventually another task should be writing them to disk and just
// passing us a filename like everything else.
Expand All @@ -145,7 +157,7 @@ public override bool RunTask ()
// eg: "obj/myjar.jar#myfile.txt"
var jar_file_path = disk_path.Substring (0, disk_path.Length - (jar_entry_name.Length + 1));

if (apk.Archive.Any (ze => ze.FullName == apk_path)) {
if (apk.ContainsEntry (apk_path)) {
Log.LogDebugMessage ("Failed to add jar entry {0} from {1}: the same file already exists in the apk", jar_entry_name, Path.GetFileName (jar_file_path));
continue;
}
Expand All @@ -165,7 +177,7 @@ public override bool RunTask ()
}

Log.LogDebugMessage ($"Adding {jar_entry_name} from {jar_file_path} as the archive file is out of date.");
apk.AddEntryAndFlush (data, apk_path);
apk.AddEntry (data, apk_path);
}

continue;
Expand All @@ -181,62 +193,46 @@ public override bool RunTask ()
continue;

Log.LogDebugMessage ($"Removing {entry} as it is not longer required.");
apk.Archive.DeleteEntry (entry);
apk.DeleteEntry (entry);
}

if (string.Compare (AndroidPackageFormat, "aab", true) == 0)
if (is_aab)
FixupArchive (apk);

return !Log.HasLoggedErrors;
}

bool AddFileToArchiveIfNewer (ZipArchiveEx apk, string file, string inArchivePath, ITaskItem item, List<string> existingEntries)
bool AddFileToArchiveIfNewer (IZipArchive apk, string file, string inArchivePath, ITaskItem item, List<string> existingEntries)
{
var compressionMethod = GetCompressionMethod (item);
var compressionMethod = GetCompressionLevel (item);
existingEntries.Remove (inArchivePath.Replace (Path.DirectorySeparatorChar, '/'));

if (apk.SkipExistingFile (file, inArchivePath, compressionMethod)) {
Log.LogDebugMessage ($"Skipping {file} as the archive file is up to date.");
return false;
}

Log.LogDebugMessage ($"Adding {file} as the archive file is out of date.");
apk.AddFileAndFlush (file, inArchivePath, compressionMethod);

return true;
return apk.AddFileIfChanged (Log, file, inArchivePath, compressionMethod);
}

/// <summary>
/// aapt2 is putting AndroidManifest.xml in the root of the archive instead of at manifest/AndroidManifest.xml that bundletool expects.
/// I see no way to change this behavior, so we can move the file for now:
/// https://github.com/aosp-mirror/platform_frameworks_base/blob/e80b45506501815061b079dcb10bf87443bd385d/tools/aapt2/LoadedApk.h#L34
/// </summary>
void FixupArchive (ZipArchiveEx zip)
void FixupArchive (IZipArchive zip)
{
if (!zip.Archive.ContainsEntry ("AndroidManifest.xml")) {
if (!zip.ContainsEntry ("AndroidManifest.xml")) {
Log.LogDebugMessage ($"No AndroidManifest.xml. Skipping Fixup");
return;
}

var entry = zip.Archive.ReadEntry ("AndroidManifest.xml");
Log.LogDebugMessage ($"Fixing up AndroidManifest.xml to be manifest/AndroidManifest.xml.");

if (zip.Archive.ContainsEntry ("manifest/AndroidManifest.xml"))
zip.Archive.DeleteEntry (zip.Archive.ReadEntry ("manifest/AndroidManifest.xml"));
if (zip.ContainsEntry ("manifest/AndroidManifest.xml"))
zip.DeleteEntry ("manifest/AndroidManifest.xml");

entry.Rename ("manifest/AndroidManifest.xml");
zip.MoveEntry ("AndroidManifest.xml", "manifest/AndroidManifest.xml");
}

CompressionMethod GetCompressionMethod (ITaskItem item)
System.IO.Compression.CompressionLevel GetCompressionLevel (ITaskItem item)
{
var compression = item.GetMetadataOrDefault ("Compression", "");

if (compression.HasValue ()) {
if (Enum.TryParse (compression, out CompressionMethod result))
return result;
}

return UncompressedFileExtensionsSet.Contains (Path.GetExtension (item.ItemSpec)) ? uncompressedMethod : CompressionMethod.Default;
return (UncompressedFileExtensionsSet.Contains (Path.GetExtension (item.ItemSpec)) ? uncompressedMethod : CompressionMethod.Default).ToCompressionLevel ();
}

HashSet<string> ParseUncompressedFileExtensions ()
Expand Down
46 changes: 46 additions & 0 deletions src/Xamarin.Android.Build.Tasks/Utilities/UtilityExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
using System;
using System.IO;
using System.IO.Compression;
using Xamarin.Tools.Zip;

namespace Xamarin.Android.Tasks;

static class UtilityExtensions
{
public static System.IO.Compression.CompressionLevel ToCompressionLevel (this CompressionMethod method)
{
switch (method) {
case CompressionMethod.Store:
return System.IO.Compression.CompressionLevel.NoCompression;
case CompressionMethod.Default:
case CompressionMethod.Deflate:
return System.IO.Compression.CompressionLevel.Optimal;
default:
throw new ArgumentOutOfRangeException (nameof (method), method, null);
}
}

public static CompressionMethod ToCompressionMethod (this System.IO.Compression.CompressionLevel level)
{
switch (level) {
case System.IO.Compression.CompressionLevel.NoCompression:
return CompressionMethod.Store;
case System.IO.Compression.CompressionLevel.Optimal:
return CompressionMethod.Deflate;
default:
throw new ArgumentOutOfRangeException (nameof (level), level, null);
}
}

public static FileMode ToFileMode (this ZipArchiveMode mode)
{
switch (mode) {
case ZipArchiveMode.Create:
return FileMode.Create;
case ZipArchiveMode.Update:
return FileMode.Open;
default:
throw new ArgumentOutOfRangeException (nameof (mode), mode, null);
}
}
}
Loading

0 comments on commit 6194d12

Please sign in to comment.