xref: /aosp_15_r20/external/cronet/build/toolchain/ios/compile_xcassets.py (revision 6777b5387eb2ff775bb5750e3f5d96f37fb7352b)
1# Copyright 2016 The Chromium Authors
2# Use of this source code is governed by a BSD-style license that can be
3# found in the LICENSE file.
4
5"""Wrapper around actool to compile assets catalog.
6
7The script compile_xcassets.py is a wrapper around actool to compile
8assets catalog to Assets.car that turns warning into errors. It also
9fixes some quirks of actool to make it work from ninja (mostly that
10actool seems to require absolute path but gn generates command-line
11with relative paths).
12
13The wrapper filter out any message that is not a section header and
14not a warning or error message, and fails if filtered output is not
15empty. This should to treat all warnings as error until actool has
16an option to fail with non-zero error code when there are warnings.
17"""
18
19import argparse
20import os
21import re
22import shutil
23import subprocess
24import sys
25import tempfile
26import zipfile
27
28# Pattern matching a section header in the output of actool.
29SECTION_HEADER = re.compile('^/\\* ([^ ]*) \\*/$')
30
31# Name of the section containing informational messages that can be ignored.
32NOTICE_SECTION = 'com.apple.actool.compilation-results'
33
34# Map special type of asset catalog to the corresponding command-line
35# parameter that need to be passed to actool.
36ACTOOL_FLAG_FOR_ASSET_TYPE = {
37    '.appiconset': '--app-icon',
38    '.launchimage': '--launch-image',
39}
40
41def FixAbsolutePathInLine(line, relative_paths):
42  """Fix absolute paths present in |line| to relative paths."""
43  absolute_path = line.split(':')[0]
44  relative_path = relative_paths.get(absolute_path, absolute_path)
45  if absolute_path == relative_path:
46    return line
47  return relative_path + line[len(absolute_path):]
48
49
50def FilterCompilerOutput(compiler_output, relative_paths):
51  """Filers actool compilation output.
52
53  The compiler output is composed of multiple sections for each different
54  level of output (error, warning, notices, ...). Each section starts with
55  the section name on a single line, followed by all the messages from the
56  section.
57
58  The function filter any lines that are not in com.apple.actool.errors or
59  com.apple.actool.document.warnings sections (as spurious messages comes
60  before any section of the output).
61
62  See crbug.com/730054, crbug.com/739163 and crbug.com/770634 for some example
63  messages that pollute the output of actool and cause flaky builds.
64
65  Args:
66    compiler_output: string containing the output generated by the
67      compiler (contains both stdout and stderr)
68    relative_paths: mapping from absolute to relative paths used to
69      convert paths in the warning and error messages (unknown paths
70      will be left unaltered)
71
72  Returns:
73    The filtered output of the compiler. If the compilation was a
74    success, then the output will be empty, otherwise it will use
75    relative path and omit any irrelevant output.
76  """
77
78  filtered_output = []
79  current_section = None
80  data_in_section = False
81  for line in compiler_output.splitlines():
82    match = SECTION_HEADER.search(line)
83    if match is not None:
84      data_in_section = False
85      current_section = match.group(1)
86      continue
87    if current_section and current_section != NOTICE_SECTION:
88      if not data_in_section:
89        data_in_section = True
90        filtered_output.append('/* %s */\n' % current_section)
91
92      fixed_line = FixAbsolutePathInLine(line, relative_paths)
93      filtered_output.append(fixed_line + '\n')
94
95  return ''.join(filtered_output)
96
97
98def CompileAssetCatalog(output, platform, target_environment, product_type,
99                        min_deployment_target, possibly_zipped_inputs,
100                        compress_pngs, partial_info_plist, temporary_dir):
101  """Compile the .xcassets bundles to an asset catalog using actool.
102
103  Args:
104    output: absolute path to the containing bundle
105    platform: the targeted platform
106    product_type: the bundle type
107    min_deployment_target: minimum deployment target
108    possibly_zipped_inputs: list of absolute paths to .xcassets bundles or zips
109    compress_pngs: whether to enable compression of pngs
110    partial_info_plist: path to partial Info.plist to generate
111    temporary_dir: path to directory for storing temp data
112  """
113  command = [
114      'xcrun',
115      'actool',
116      '--output-format=human-readable-text',
117      '--notices',
118      '--warnings',
119      '--errors',
120      '--minimum-deployment-target',
121      min_deployment_target,
122  ]
123
124  if compress_pngs:
125    command.extend(['--compress-pngs'])
126
127  if product_type != '':
128    command.extend(['--product-type', product_type])
129
130  if platform == 'mac':
131    command.extend([
132        '--platform',
133        'macosx',
134        '--target-device',
135        'mac',
136    ])
137  elif platform == 'ios':
138    if target_environment == 'simulator':
139      command.extend([
140          '--platform',
141          'iphonesimulator',
142          '--target-device',
143          'iphone',
144          '--target-device',
145          'ipad',
146      ])
147    elif target_environment == 'device':
148      command.extend([
149          '--platform',
150          'iphoneos',
151          '--target-device',
152          'iphone',
153          '--target-device',
154          'ipad',
155      ])
156    elif target_environment == 'catalyst':
157      command.extend([
158          '--platform',
159          'macosx',
160          '--target-device',
161          'ipad',
162          '--ui-framework-family',
163          'uikit',
164      ])
165
166  # Unzip any input zipfiles to a temporary directory.
167  inputs = []
168  for relative_path in possibly_zipped_inputs:
169    if os.path.isfile(relative_path) and zipfile.is_zipfile(relative_path):
170      catalog_name = os.path.basename(relative_path)
171      unzip_path = os.path.join(temporary_dir, os.path.dirname(relative_path))
172      with zipfile.ZipFile(relative_path) as z:
173        invalid_files = [
174            x for x in z.namelist()
175            if '..' in x or not x.startswith(catalog_name)
176        ]
177        if invalid_files:
178          sys.stderr.write('Invalid files in zip: %s' % invalid_files)
179          sys.exit(1)
180        z.extractall(unzip_path)
181      inputs.append(os.path.join(unzip_path, catalog_name))
182    else:
183      inputs.append(relative_path)
184
185  # Scan the input directories for the presence of asset catalog types that
186  # require special treatment, and if so, add them to the actool command-line.
187  for relative_path in inputs:
188
189    if not os.path.isdir(relative_path):
190      continue
191
192    for file_or_dir_name in os.listdir(relative_path):
193      if not os.path.isdir(os.path.join(relative_path, file_or_dir_name)):
194        continue
195
196      asset_name, asset_type = os.path.splitext(file_or_dir_name)
197      if asset_type not in ACTOOL_FLAG_FOR_ASSET_TYPE:
198        continue
199
200      command.extend([ACTOOL_FLAG_FOR_ASSET_TYPE[asset_type], asset_name])
201
202  # Always ask actool to generate a partial Info.plist file. If no path
203  # has been given by the caller, use a temporary file name.
204  temporary_file = None
205  if not partial_info_plist:
206    temporary_file = tempfile.NamedTemporaryFile(suffix='.plist')
207    partial_info_plist = temporary_file.name
208
209  command.extend(['--output-partial-info-plist', partial_info_plist])
210
211  # Dictionary used to convert absolute paths back to their relative form
212  # in the output of actool.
213  relative_paths = {}
214
215  # actool crashes if paths are relative, so convert input and output paths
216  # to absolute paths, and record the relative paths to fix them back when
217  # filtering the output.
218  absolute_output = os.path.abspath(output)
219  relative_paths[output] = absolute_output
220  relative_paths[os.path.dirname(output)] = os.path.dirname(absolute_output)
221  command.extend(['--compile', os.path.dirname(os.path.abspath(output))])
222
223  for relative_path in inputs:
224    absolute_path = os.path.abspath(relative_path)
225    relative_paths[absolute_path] = relative_path
226    command.append(absolute_path)
227
228  try:
229    # Run actool and redirect stdout and stderr to the same pipe (as actool
230    # is confused about what should go to stderr/stdout).
231    process = subprocess.Popen(command,
232                               stdout=subprocess.PIPE,
233                               stderr=subprocess.STDOUT)
234    stdout = process.communicate()[0].decode('utf-8')
235
236    # If the invocation of `actool` failed, copy all the compiler output to
237    # the standard error stream and exit. See https://crbug.com/1205775 for
238    # example of compilation that failed with no error message due to filter.
239    if process.returncode:
240      for line in stdout.splitlines():
241        fixed_line = FixAbsolutePathInLine(line, relative_paths)
242        sys.stderr.write(fixed_line + '\n')
243      sys.exit(1)
244
245    # Filter the output to remove all garbage and to fix the paths. If the
246    # output is not empty after filtering, then report the compilation as a
247    # failure (as some version of `actool` report error to stdout, yet exit
248    # with an return code of zero).
249    stdout = FilterCompilerOutput(stdout, relative_paths)
250    if stdout:
251      sys.stderr.write(stdout)
252      sys.exit(1)
253
254  finally:
255    if temporary_file:
256      temporary_file.close()
257
258
259def Main():
260  parser = argparse.ArgumentParser(
261      description='compile assets catalog for a bundle')
262  parser.add_argument('--platform',
263                      '-p',
264                      required=True,
265                      choices=('mac', 'ios'),
266                      help='target platform for the compiled assets catalog')
267  parser.add_argument('--target-environment',
268                      '-e',
269                      default='',
270                      choices=('simulator', 'device', 'catalyst'),
271                      help='target environment for the compiled assets catalog')
272  parser.add_argument(
273      '--minimum-deployment-target',
274      '-t',
275      required=True,
276      help='minimum deployment target for the compiled assets catalog')
277  parser.add_argument('--output',
278                      '-o',
279                      required=True,
280                      help='path to the compiled assets catalog')
281  parser.add_argument('--compress-pngs',
282                      '-c',
283                      action='store_true',
284                      default=False,
285                      help='recompress PNGs while compiling assets catalog')
286  parser.add_argument('--product-type',
287                      '-T',
288                      help='type of the containing bundle')
289  parser.add_argument('--partial-info-plist',
290                      '-P',
291                      help='path to partial info plist to create')
292  parser.add_argument('inputs',
293                      nargs='+',
294                      help='path to input assets catalog sources')
295  args = parser.parse_args()
296
297  if os.path.basename(args.output) != 'Assets.car':
298    sys.stderr.write('output should be path to compiled asset catalog, not '
299                     'to the containing bundle: %s\n' % (args.output, ))
300    sys.exit(1)
301
302  if os.path.exists(args.output):
303    if os.path.isfile(args.output):
304      os.unlink(args.output)
305    else:
306      shutil.rmtree(args.output)
307
308  with tempfile.TemporaryDirectory() as temporary_dir:
309    CompileAssetCatalog(args.output, args.platform, args.target_environment,
310                        args.product_type, args.minimum_deployment_target,
311                        args.inputs, args.compress_pngs,
312                        args.partial_info_plist, temporary_dir)
313
314
315if __name__ == '__main__':
316  sys.exit(Main())
317