tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

compile_xcassets.py (13205B)


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