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