[ios] Pass zipped xcassets to compile_xcassets.py.

The existing mechanism of hardlinking individual assets fails when an
asset is renamed, because stale files remain in the output directory,
and actool operates on the entire directory at once.

Sidestep the problem of stale files by creating a zip file with the
assets for each target, then extracting those zips to a temporary
directory and passing the result to actool. Since we extract to a new
temp directory each time, stale files cannot exist.

Bug: 332929378
Change-Id: I26e829bd625fcdbba1a87e75c76a21024f58415d
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/5454598
Reviewed-by: Sylvain Defresne <sdefresne@chromium.org>
Commit-Queue: Rohit Rao <rohitrao@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1288230}
NOKEYCHECK=True
GitOrigin-RevId: 311b90905db03b8c72301fc1a969b2ea67d51cd5
diff --git a/ios/compile_xcassets.py b/ios/compile_xcassets.py
index a62b96a..09ee2c1 100644
--- a/ios/compile_xcassets.py
+++ b/ios/compile_xcassets.py
@@ -23,6 +23,7 @@
 import subprocess
 import sys
 import tempfile
+import zipfile
 
 # Pattern matching a section header in the output of actool.
 SECTION_HEADER = re.compile('^/\\* ([^ ]*) \\*/$')
@@ -95,8 +96,8 @@
 
 
 def CompileAssetCatalog(output, platform, target_environment, product_type,
-                        min_deployment_target, inputs, compress_pngs,
-                        partial_info_plist):
+                        min_deployment_target, possibly_zipped_inputs,
+                        compress_pngs, partial_info_plist, temporary_dir):
   """Compile the .xcassets bundles to an asset catalog using actool.
 
   Args:
@@ -104,9 +105,10 @@
     platform: the targeted platform
     product_type: the bundle type
     min_deployment_target: minimum deployment target
-    inputs: list of absolute paths to .xcassets bundles
+    possibly_zipped_inputs: list of absolute paths to .xcassets bundles or zips
     compress_pngs: whether to enable compression of pngs
     partial_info_plist: path to partial Info.plist to generate
+    temporary_dir: path to directory for storing temp data
   """
   command = [
       'xcrun',
@@ -161,6 +163,25 @@
           'uikit',
       ])
 
+  # Unzip any input zipfiles to a temporary directory.
+  inputs = []
+  for relative_path in possibly_zipped_inputs:
+    if os.path.isfile(relative_path) and zipfile.is_zipfile(relative_path):
+      catalog_name = os.path.basename(relative_path)
+      unzip_path = os.path.join(temporary_dir, os.path.dirname(relative_path))
+      with zipfile.ZipFile(relative_path) as z:
+        invalid_files = [
+            x for x in z.namelist()
+            if '..' in x or not x.startswith(catalog_name)
+        ]
+        if invalid_files:
+          sys.stderr.write('Invalid files in zip: %s' % invalid_files)
+          sys.exit(1)
+        z.extractall(unzip_path)
+      inputs.append(os.path.join(unzip_path, catalog_name))
+    else:
+      inputs.append(relative_path)
+
   # Scan the input directories for the presence of asset catalog types that
   # require special treatment, and if so, add them to the actool command-line.
   for relative_path in inputs:
@@ -284,9 +305,11 @@
     else:
       shutil.rmtree(args.output)
 
-  CompileAssetCatalog(args.output, args.platform, args.target_environment,
-                      args.product_type, args.minimum_deployment_target,
-                      args.inputs, args.compress_pngs, args.partial_info_plist)
+  with tempfile.TemporaryDirectory() as temporary_dir:
+    CompileAssetCatalog(args.output, args.platform, args.target_environment,
+                        args.product_type, args.minimum_deployment_target,
+                        args.inputs, args.compress_pngs,
+                        args.partial_info_plist, temporary_dir)
 
 
 if __name__ == '__main__':