From 6194d124d70f68371d0971b34caee70b32657e6e Mon Sep 17 00:00:00 2001 From: Jonathan Pobst Date: Mon, 16 Dec 2024 12:36:42 -1000 Subject: [PATCH 1/2] [XABT] Use ZipArchive to build APKs. --- .../Tasks/BuildArchive.cs | 76 +++--- .../Utilities/UtilityExtensions.cs | 46 ++++ .../Utilities/ZipArchiveDotNet.cs | 257 ++++++++++++++++++ .../Utilities/ZipArchiveEx.cs | 73 ++++- .../Utilities/ZipArchiveFileListBuilder.cs | 67 ----- 5 files changed, 410 insertions(+), 109 deletions(-) create mode 100644 src/Xamarin.Android.Build.Tasks/Utilities/UtilityExtensions.cs create mode 100644 src/Xamarin.Android.Build.Tasks/Utilities/ZipArchiveDotNet.cs delete mode 100644 src/Xamarin.Android.Build.Tasks/Utilities/ZipArchiveFileListBuilder.cs diff --git a/src/Xamarin.Android.Build.Tasks/Tasks/BuildArchive.cs b/src/Xamarin.Android.Build.Tasks/Tasks/BuildArchive.cs index db57c4eb989..165180666c3 100644 --- a/src/Xamarin.Android.Build.Tasks/Tasks/BuildArchive.cs +++ b/src/Xamarin.Android.Build.Tasks/Tasks/BuildArchive.cs @@ -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; @@ -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)) { @@ -73,10 +75,9 @@ public override bool RunTask () var existingEntries = new List (); 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); } } @@ -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); @@ -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) { @@ -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. @@ -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; } @@ -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; @@ -181,29 +193,21 @@ 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 existingEntries) + bool AddFileToArchiveIfNewer (IZipArchive apk, string file, string inArchivePath, ITaskItem item, List 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); } /// @@ -211,32 +215,24 @@ bool AddFileToArchiveIfNewer (ZipArchiveEx apk, string file, string inArchivePat /// 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 /// - 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 ParseUncompressedFileExtensions () diff --git a/src/Xamarin.Android.Build.Tasks/Utilities/UtilityExtensions.cs b/src/Xamarin.Android.Build.Tasks/Utilities/UtilityExtensions.cs new file mode 100644 index 00000000000..a566a6b754f --- /dev/null +++ b/src/Xamarin.Android.Build.Tasks/Utilities/UtilityExtensions.cs @@ -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); + } + } +} diff --git a/src/Xamarin.Android.Build.Tasks/Utilities/ZipArchiveDotNet.cs b/src/Xamarin.Android.Build.Tasks/Utilities/ZipArchiveDotNet.cs new file mode 100644 index 00000000000..08a4570fc0c --- /dev/null +++ b/src/Xamarin.Android.Build.Tasks/Utilities/ZipArchiveDotNet.cs @@ -0,0 +1,257 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.IO.Compression; +using System.Linq; +using System.Reflection; +using Microsoft.Android.Build.Tasks; +using Microsoft.Build.Utilities; + +namespace Xamarin.Android.Tasks; + +interface IZipArchive : IDisposable +{ + int ZipFlushFilesLimit { get; set; } + int ZipFlushSizeLimit { get; set; } + + void AddEntry (byte [] data, string apkPath); + void AddEntry (Stream stream, string apkPath, CompressionLevel compression); + bool AddFileIfChanged (TaskLoggingHelper log, string filename, string archiveFileName, CompressionLevel compression); + bool ContainsEntry (string entryPath); + void DeleteEntry (string entry); + void FixupWindowsPathSeparators (TaskLoggingHelper log); + IEnumerable GetAllEntryNames (); + IZipArchiveEntry GetEntry (string entryName); + void MoveEntry (string oldEntry, string newEntry); +} + +interface IZipArchiveEntry +{ + uint CRC { get; } + ulong CompressedSize { get; } +} + +class ZipArchiveDotNet : IZipArchive +{ + const int DEFAULT_FLUSH_SIZE_LIMIT = 100 * 1024 * 1024; + const int DEFAULT_FLUSH_FILES_LIMIT = 512; + + // ZipFile seems to be performant enough that we have not currently implemented a flush mechanism. + public int ZipFlushSizeLimit { get; set; } = DEFAULT_FLUSH_SIZE_LIMIT; + public int ZipFlushFilesLimit { get; set; } = DEFAULT_FLUSH_FILES_LIMIT; + + public ZipArchive Archive { get; } + + static readonly FieldInfo? crc_field; + static readonly FieldInfo? comp_field; + + // This is private to ensure the only way to create an instance is through the Create method. + ZipArchiveDotNet (string archive, ZipArchiveMode mode) + { + Archive = ZipFile.Open (archive, mode); + } + + static ZipArchiveDotNet () + { + // netstandard2.0 does not provide a way to access a ZipArchiveEntry's CRC or compresion level. + // We need to use reflection to access the private fields. + + // These private fields exist on both .NET Framework 4.7.2 and .NET 9.0. If they are not found, + // we will return a ZipArchiveEx which uses libZipSharp instead. + crc_field = typeof (ZipArchiveEntry).GetField ("_crc32", BindingFlags.NonPublic | BindingFlags.Instance); + comp_field = typeof (ZipArchiveEntry).GetField ("_storedCompressionMethod", BindingFlags.NonPublic | BindingFlags.Instance); + } + + public static IZipArchive Create (TaskLoggingHelper log, string archive, ZipArchiveMode mode) + { + if (crc_field is null) { + log.LogDebugMessage ($"ZipArchiveDotNet: Could not find private CRC field, falling back to libZipSharp."); + return new ZipArchiveEx (archive, mode.ToFileMode ()); + } + + if (comp_field is null) { + log.LogDebugMessage ($"ZipArchiveDotNet: Could not find private CompressionMethod field, falling back to libZipSharp."); + return new ZipArchiveEx (archive, mode.ToFileMode ()); + } + + return new ZipArchiveDotNet (archive, mode); + } + + public void AddEntry (byte [] data, string apkPath) + { + var entry = Archive.CreateEntry (apkPath); + + using (var stream = entry.Open ()) + stream.Write (data, 0, data.Length); + } + + public void AddEntry (Stream stream, string apkPath, CompressionLevel compression) + { + var entry = Archive.CreateEntry (apkPath, compression); + + using (var entry_stream = entry.Open ()) + stream.CopyTo (entry_stream); + } + + public bool AddFileIfChanged (TaskLoggingHelper log, string filename, string archiveFileName, CompressionLevel compression) + { + if (!FileNeedsUpdating (log, filename, archiveFileName, compression)) + return false; + + DeleteEntry (archiveFileName); + Archive.CreateEntryFromFile (filename, archiveFileName, compression); + + return true; + } + + public bool ContainsEntry (string entryName) + { + return Archive.GetEntry (entryName) is not null; + } + + public void DeleteEntry (string entryName) + { + var entry = Archive.GetEntry (entryName); + + entry?.Delete (); + } + + /// + /// HACK: aapt2 is creating zip entries on Windows such as `assets\subfolder/asset2.txt` + /// + public void FixupWindowsPathSeparators (TaskLoggingHelper log) + { + var malformed_entries = Archive.Entries.Where (entry => entry.FullName.Contains ('\\')).ToList (); + + foreach (var entry in malformed_entries) { + var name = entry.FullName.Replace ('\\', '/'); + if (name != entry.FullName) { + log.LogDebugMessage ($"Fixing up malformed entry `{entry.FullName}` -> `{name}`"); + MoveEntry (entry.FullName, name); + } + } + } + + public IEnumerable GetAllEntryNames () + { + return Archive.Entries.Select (entry => entry.FullName); + } + + public IZipArchiveEntry GetEntry (string entryName) + { + var entry = Archive.GetEntry (entryName); + + if (entry is null) + throw new ArgumentOutOfRangeException (nameof (entryName)); + + return new ZipArchiveEntryDotNet (entry, GetCrc32 (entry)); + } + + public void Dispose () + { + Archive.Dispose (); + } + + public void MoveEntry (string oldPath, string newPath) + { + var old_entry = Archive.GetEntry (oldPath); + + if (old_entry is null) + return; + + var new_entry = Archive.CreateEntry (newPath); + + using (var oldStream = old_entry.Open ()) + using (var newStream = new_entry.Open ()) + oldStream.CopyTo (newStream); + + old_entry.Delete (); + } + + bool FileNeedsUpdating (TaskLoggingHelper log, string filename, string archiveFileName, CompressionLevel compression) + { + var entry = Archive.GetEntry (archiveFileName); + + if (entry is null) { + log.LogDebugMessage ($"Adding {filename} as it doesn't already exist."); + return true; + } + + var stored_compression = GetCompressionLevel (entry); + + if (stored_compression != compression) { + log.LogDebugMessage ($"Updating {filename} as the compression level changed: existing - '{stored_compression}', requested - '{compression}'."); + return true; + } + + var last_write = File.GetLastWriteTimeUtc (filename); + var file_write_dos_time = DateTimeToDosTime (last_write); + var zip_write_dos_time = DateTimeToDosTime (entry.LastWriteTime.UtcDateTime); + + if (DateTimeToDosTime (entry.LastWriteTime.UtcDateTime) < DateTimeToDosTime (last_write)) { + log.LogDebugMessage ($"Updating {filename} as the file write time is newer: file in zip - '{zip_write_dos_time}', file on disk - '{file_write_dos_time}'."); + return true; + } + + log.LogDebugMessage ($"Skipping {filename} as the archive file is up to date."); + return false; + } + + static uint GetCrc32 (ZipArchiveEntry entry) + { + if (crc_field is null) + throw new NotSupportedException ("This method is not supported on this platform."); + + return (uint) crc_field.GetValue (entry); + } + + static CompressionLevel GetCompressionLevel (ZipArchiveEntry entry) + { + if (comp_field is null) + throw new NotSupportedException ("This method is not supported on this platform."); + + var level = comp_field.GetValue (entry).ToString (); + + switch (level) { + case "Stored": + return CompressionLevel.NoCompression; + case "Deflate": + return CompressionLevel.Optimal; + default: + throw new NotSupportedException ($"Unsupported compression level: {level}"); + } + } + + // System.IO.Compression.ZipArchive apparently only provides a 2 second granularity for the LastWriteTime. + // This should be fine, it would be nearly impossible for someone to complete a build, make a change, + // and rebuild in under 2 seconds. + // This is a port of the DateTimeToDosTime method from System.IO.Compression.ZipHelper + // https://github.com/dotnet/runtime/blob/373f048bae3c46810bc030ed7c1ee0568ee5ecc0/src/libraries/System.IO.Compression/src/System/IO/Compression/ZipHelper.cs#L88 + const int ValidZipDate_YearMin = 1980; + + static uint DateTimeToDosTime (DateTime dateTime) + { + int ret = ((dateTime.Year - ValidZipDate_YearMin) & 0x7F); + ret = (ret << 4) + dateTime.Month; + ret = (ret << 5) + dateTime.Day; + ret = (ret << 5) + dateTime.Hour; + ret = (ret << 6) + dateTime.Minute; + ret = (ret << 5) + (dateTime.Second / 2); // only 5 bits for second, so we only have a granularity of 2 sec. + return (uint) ret; + } + + class ZipArchiveEntryDotNet : IZipArchiveEntry + { + readonly ZipArchiveEntry entry; + readonly uint crc; + + public uint CRC => crc; + public ulong CompressedSize => (ulong) entry.CompressedLength; + + public ZipArchiveEntryDotNet (ZipArchiveEntry entry, uint crc) + { + this.entry = entry; + this.crc = crc; + } + } +} diff --git a/src/Xamarin.Android.Build.Tasks/Utilities/ZipArchiveEx.cs b/src/Xamarin.Android.Build.Tasks/Utilities/ZipArchiveEx.cs index 54e510218ad..21ef964fe12 100644 --- a/src/Xamarin.Android.Build.Tasks/Utilities/ZipArchiveEx.cs +++ b/src/Xamarin.Android.Build.Tasks/Utilities/ZipArchiveEx.cs @@ -1,11 +1,14 @@ -using System; +using System; +using System.Collections.Generic; using System.IO; using System.Linq; +using Microsoft.Android.Build.Tasks; +using Microsoft.Build.Utilities; using Xamarin.Tools.Zip; namespace Xamarin.Android.Tasks { - public class ZipArchiveEx : IDisposable + public class ZipArchiveEx : IZipArchive { const int DEFAULT_FLUSH_SIZE_LIMIT = 100 * 1024 * 1024; @@ -235,5 +238,71 @@ protected virtual void Dispose(bool disposing) { } } } + + public void AddEntry (byte [] data, string apkPath) + => AddEntryAndFlush (data, apkPath); + + public void AddEntry (Stream stream, string apkPath, System.IO.Compression.CompressionLevel compression) + => AddEntryAndFlush (apkPath, stream, compression.ToCompressionMethod ()); + + public bool AddFileIfChanged (TaskLoggingHelper log, string filename, string archiveFileName, System.IO.Compression.CompressionLevel compression) + { + var compressionMethod = compression.ToCompressionMethod (); + + if (!SkipExistingFile (filename, archiveFileName, compressionMethod)) { + AddFileAndFlush (filename, archiveFileName, compressionMethod); + log.LogDebugMessage ($"Adding {filename} as the archive file is out of date."); + return true; + } + + log.LogDebugMessage ($"Skipping {filename} as the archive file is up to date."); + + return false; + } + + public bool ContainsEntry (string entryPath) + => zip.ContainsEntry (entryPath); + + public void DeleteEntry (string entry) + => zip.DeleteEntry (entry); + + public void FixupWindowsPathSeparators (TaskLoggingHelper log) + => FixupWindowsPathSeparators ((a, b) => log.LogDebugMessage ($"Fixing up malformed entry `{a}` -> `{b}`")); + + public IEnumerable GetAllEntryNames () + { + for (var i = 0; i < Archive.EntryCount; i++) { + var entry = Archive.ReadEntry ((ulong) i); + yield return entry.FullName; + } + } + + IZipArchiveEntry IZipArchive.GetEntry (string entryName) + { + return new ZipArchiveEntryEx (zip.ReadEntry (entryName)); + } + + void IZipArchive.MoveEntry (string oldEntry, string newEntry) + { + if (Archive.ContainsEntry (newEntry)) + Archive.DeleteEntry (Archive.ReadEntry (newEntry)); + + var entry = zip.ReadEntry (oldEntry); + entry.Rename (newEntry); + } + } + + class ZipArchiveEntryEx : IZipArchiveEntry + { + readonly ZipEntry entry; + + public ZipArchiveEntryEx (ZipEntry entry) + { + this.entry = entry; + } + + public uint CRC => entry.CRC; + + public ulong CompressedSize => entry.CompressedSize; } } diff --git a/src/Xamarin.Android.Build.Tasks/Utilities/ZipArchiveFileListBuilder.cs b/src/Xamarin.Android.Build.Tasks/Utilities/ZipArchiveFileListBuilder.cs deleted file mode 100644 index cdaf609ffc3..00000000000 --- a/src/Xamarin.Android.Build.Tasks/Utilities/ZipArchiveFileListBuilder.cs +++ /dev/null @@ -1,67 +0,0 @@ -#nullable enable - -using System; -using System.Collections.Generic; -using System.IO; -using Microsoft.Build.Framework; -using Microsoft.Build.Utilities; -using Xamarin.Tools.Zip; - -namespace Xamarin.Android.Tasks; - -// This temporary class has a nonsensical API to allow it to be a drop-in replacement -// for ZipArchiveEx. This allows us to refactor with smaller diffs that can be -// reviewed easier. This class should not exist in this form in the final state. -public class ZipArchiveFileListBuilder : IDisposable -{ - public List ApkFiles { get; } = []; - - public ZipArchiveFileListBuilder (string archive, FileMode filemode) - { - } - - public void Dispose () - { - // No-op - } - - public void Flush () - { - // No-op - } - - public void AddFileAndFlush (string filename, string archiveFileName, CompressionMethod compressionMethod) - { - var item = new TaskItem (filename); - - item.SetMetadata ("ArchivePath", archiveFileName); - item.SetMetadata ("Compression", compressionMethod.ToString ()); - - ApkFiles.Add (item); - } - - public void AddJavaEntryAndFlush (string javaFilename, string javaEntryName, string archiveFileName) - { - // An item's ItemSpec must be unique so use both the jar file name and the entry name - var item = new TaskItem ($"{javaFilename}#{javaEntryName}"); - item.SetMetadata ("ArchivePath", archiveFileName); - item.SetMetadata ("JavaArchiveEntry", javaEntryName); - - ApkFiles.Add (item); - } - - public void FixupWindowsPathSeparators (Action onRename) - { - // No-op - } - - public bool SkipExistingFile (string file, string fileInArchive, CompressionMethod compressionMethod) - { - return false; - } - - public bool SkipExistingEntry (ZipEntry sourceEntry, string fileInArchive) - { - return false; - } -} From 8a42abe412dfc3eb322c15fd96fe4ac7eed58a4d Mon Sep 17 00:00:00 2001 From: Jonathan Pobst Date: Tue, 17 Dec 2024 07:55:56 -1000 Subject: [PATCH 2/2] Add `$(_AndroidUseLibZipSharp)` fallback flag. --- .../Tasks/BuildArchive.cs | 4 +++- .../Utilities/ZipArchiveDotNet.cs | 14 ++++++++++---- .../Xamarin.Android.Common.targets | 2 ++ 3 files changed, 15 insertions(+), 5 deletions(-) diff --git a/src/Xamarin.Android.Build.Tasks/Tasks/BuildArchive.cs b/src/Xamarin.Android.Build.Tasks/Tasks/BuildArchive.cs index 165180666c3..59014547125 100644 --- a/src/Xamarin.Android.Build.Tasks/Tasks/BuildArchive.cs +++ b/src/Xamarin.Android.Build.Tasks/Tasks/BuildArchive.cs @@ -32,6 +32,8 @@ public class BuildArchive : AndroidTask public string? UncompressedFileExtensions { get; set; } + public bool UseLibZipSharp { get; set; } + public string? ZipFlushFilesLimit { get; set; } public string? ZipFlushSizeLimit { get; set; } @@ -59,7 +61,7 @@ public override bool RunTask () refresh = false; } - using var apk = ZipArchiveDotNet.Create (Log, ApkOutputPath, System.IO.Compression.ZipArchiveMode.Update); + using var apk = ZipArchiveDotNet.Create (Log, ApkOutputPath, System.IO.Compression.ZipArchiveMode.Update, UseLibZipSharp); // Set up AutoFlush if (int.TryParse (ZipFlushFilesLimit, out int flushFilesLimit)) { diff --git a/src/Xamarin.Android.Build.Tasks/Utilities/ZipArchiveDotNet.cs b/src/Xamarin.Android.Build.Tasks/Utilities/ZipArchiveDotNet.cs index 08a4570fc0c..12a84f83ab4 100644 --- a/src/Xamarin.Android.Build.Tasks/Utilities/ZipArchiveDotNet.cs +++ b/src/Xamarin.Android.Build.Tasks/Utilities/ZipArchiveDotNet.cs @@ -57,23 +57,29 @@ static ZipArchiveDotNet () // We need to use reflection to access the private fields. // These private fields exist on both .NET Framework 4.7.2 and .NET 9.0. If they are not found, - // we will return a ZipArchiveEx which uses libZipSharp instead. + // we will return a ZipArchiveEx which uses LibZipSharp instead. crc_field = typeof (ZipArchiveEntry).GetField ("_crc32", BindingFlags.NonPublic | BindingFlags.Instance); comp_field = typeof (ZipArchiveEntry).GetField ("_storedCompressionMethod", BindingFlags.NonPublic | BindingFlags.Instance); } - public static IZipArchive Create (TaskLoggingHelper log, string archive, ZipArchiveMode mode) + public static IZipArchive Create (TaskLoggingHelper log, string archive, ZipArchiveMode mode, bool fallback) { + if (fallback) { + log.LogDebugMessage ($"ZipArchiveDotNet: Falling back to LibZipSharp as requested."); + return new ZipArchiveEx (archive, mode.ToFileMode ()); + } + if (crc_field is null) { - log.LogDebugMessage ($"ZipArchiveDotNet: Could not find private CRC field, falling back to libZipSharp."); + log.LogDebugMessage ($"ZipArchiveDotNet: Could not find private CRC field, falling back to LibZipSharp."); return new ZipArchiveEx (archive, mode.ToFileMode ()); } if (comp_field is null) { - log.LogDebugMessage ($"ZipArchiveDotNet: Could not find private CompressionMethod field, falling back to libZipSharp."); + log.LogDebugMessage ($"ZipArchiveDotNet: Could not find private CompressionMethod field, falling back to LibZipSharp."); return new ZipArchiveEx (archive, mode.ToFileMode ()); } + log.LogDebugMessage ($"ZipArchiveDotNet: Using ZipArchiveDotNet."); return new ZipArchiveDotNet (archive, mode); } diff --git a/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Common.targets b/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Common.targets index af28875fe64..6a8671e6e8d 100644 --- a/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Common.targets +++ b/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Common.targets @@ -2160,6 +2160,7 @@ because xbuild doesn't support framework reference assemblies. ApkOutputPath="$(_ApkOutputPath)" FilesToAddToArchive="@(FilesToAddToArchive)" UncompressedFileExtensions="$(AndroidStoreUncompressedFileExtensions)" + UseLibZipSharp="$(_AndroidUseLibZipSharp)" ZipFlushFilesLimit="$(_ZipFlushFilesLimit)" ZipFlushSizeLimit="$(_ZipFlushSizeLimit)" /> @@ -2293,6 +2294,7 @@ because xbuild doesn't support framework reference assemblies. ApkOutputPath="$(_ApkOutputPath)" FilesToAddToArchive="@(FilesToAddToArchive)" UncompressedFileExtensions="$(AndroidStoreUncompressedFileExtensions)" + UseLibZipSharp="$(_AndroidUseLibZipSharp)" ZipFlushFilesLimit="$(_ZipFlushFilesLimit)" ZipFlushSizeLimit="$(_ZipFlushSizeLimit)" />