| # Copyright 2016 The Chromium Authors. All rights reserved. |
| # Use of this source code is governed by a BSD-style license that can be |
| # found in the LICENSE file. |
| |
| import argparse |
| import os |
| import re |
| import subprocess |
| import sys |
| import tempfile |
| """Wrapper around actool to compile assets catalog. |
| |
| The script compile_xcassets.py is a wrapper around actool to compile |
| assets catalog to Assets.car that turns warning into errors. It also |
| fixes some quirks of actool to make it work from ninja (mostly that |
| actool seems to require absolute path but gn generates command-line |
| with relative paths). |
| |
| The wrapper filter out any message that is not a section header and |
| not a warning or error message, and fails if filtered output is not |
| empty. This should to treat all warnings as error until actool has |
| an option to fail with non-zero error code when there are warnings. |
| """ |
| |
| # Pattern matching a section header in the output of actool. |
| SECTION_HEADER = re.compile('^/\\* ([^ ]*) \\*/$') |
| |
| # Name of the section containing informational messages that can be ignored. |
| NOTICE_SECTION = 'com.apple.actool.compilation-results' |
| |
| # Regular expressions matching spurious messages from actool that should be |
| # ignored (as they are bogus). Generally a bug should be filed with Apple |
| # when adding a pattern here. |
| SPURIOUS_PATTERNS = [ |
| re.compile(v) for v in [ |
| # crbug.com/770634, likely a bug in Xcode 9.1 beta, remove once build |
| # requires a version of Xcode with a fix. |
| r'\[\]\[ipad\]\[76x76\]\[\]\[\]\[1x\]\[\]\[\]: notice: \(null\)', |
| |
| # crbug.com/770634, likely a bug in Xcode 9.2 beta, remove once build |
| # requires a version of Xcode with a fix. |
| r'\[\]\[ipad\]\[76x76\]\[\]\[\]\[1x\]\[\]\[\]: notice: 76x76@1x app' |
| ' icons only apply to iPad apps targeting releases of iOS prior to' |
| ' 10.0.', |
| ] |
| ] |
| |
| # Map special type of asset catalog to the corresponding command-line |
| # parameter that need to be passed to actool. |
| ACTOOL_FLAG_FOR_ASSET_TYPE = { |
| '.appiconset': '--app-icon', |
| '.launchimage': '--launch-image', |
| } |
| |
| |
| def IsSpuriousMessage(line): |
| """Returns whether line contains a spurious message that should be ignored.""" |
| for pattern in SPURIOUS_PATTERNS: |
| match = pattern.search(line) |
| if match is not None: |
| return True |
| return False |
| |
| |
| def FilterCompilerOutput(compiler_output, relative_paths): |
| """Filers actool compilation output. |
| |
| The compiler output is composed of multiple sections for each different |
| level of output (error, warning, notices, ...). Each section starts with |
| the section name on a single line, followed by all the messages from the |
| section. |
| |
| The function filter any lines that are not in com.apple.actool.errors or |
| com.apple.actool.document.warnings sections (as spurious messages comes |
| before any section of the output). |
| |
| See crbug.com/730054, crbug.com/739163 and crbug.com/770634 for some example |
| messages that pollute the output of actool and cause flaky builds. |
| |
| Args: |
| compiler_output: string containing the output generated by the |
| compiler (contains both stdout and stderr) |
| relative_paths: mapping from absolute to relative paths used to |
| convert paths in the warning and error messages (unknown paths |
| will be left unaltered) |
| |
| Returns: |
| The filtered output of the compiler. If the compilation was a |
| success, then the output will be empty, otherwise it will use |
| relative path and omit any irrelevant output. |
| """ |
| |
| filtered_output = [] |
| current_section = None |
| data_in_section = False |
| for line in compiler_output.splitlines(): |
| match = SECTION_HEADER.search(line) |
| if match is not None: |
| data_in_section = False |
| current_section = match.group(1) |
| continue |
| if current_section and current_section != NOTICE_SECTION: |
| if IsSpuriousMessage(line): |
| continue |
| absolute_path = line.split(':')[0] |
| relative_path = relative_paths.get(absolute_path, absolute_path) |
| if absolute_path != relative_path: |
| line = relative_path + line[len(absolute_path):] |
| if not data_in_section: |
| data_in_section = True |
| filtered_output.append('/* %s */\n' % current_section) |
| filtered_output.append(line + '\n') |
| |
| return ''.join(filtered_output) |
| |
| |
| def CompileAssetCatalog(output, platform, product_type, min_deployment_target, |
| inputs, compress_pngs, partial_info_plist): |
| """Compile the .xcassets bundles to an asset catalog using actool. |
| |
| Args: |
| output: absolute path to the containing bundle |
| platform: the targeted platform |
| product_type: the bundle type |
| min_deployment_target: minimum deployment target |
| inputs: list of absolute paths to .xcassets bundles |
| compress_pngs: whether to enable compression of pngs |
| partial_info_plist: path to partial Info.plist to generate |
| """ |
| command = [ |
| 'xcrun', |
| 'actool', |
| '--output-format=human-readable-text', |
| '--notices', |
| '--warnings', |
| '--errors', |
| '--platform', |
| platform, |
| '--minimum-deployment-target', |
| min_deployment_target, |
| ] |
| |
| if compress_pngs: |
| command.extend(['--compress-pngs']) |
| |
| if product_type != '': |
| command.extend(['--product-type', product_type]) |
| |
| if platform == 'macosx': |
| command.extend(['--target-device', 'mac']) |
| else: |
| command.extend(['--target-device', 'iphone', '--target-device', 'ipad']) |
| |
| # 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: |
| |
| if not os.path.isdir(relative_path): |
| continue |
| |
| for file_or_dir_name in os.listdir(relative_path): |
| if not os.path.isdir(os.path.join(relative_path, file_or_dir_name)): |
| continue |
| |
| asset_name, asset_type = os.path.splitext(file_or_dir_name) |
| if asset_type not in ACTOOL_FLAG_FOR_ASSET_TYPE: |
| continue |
| |
| command.extend([ACTOOL_FLAG_FOR_ASSET_TYPE[asset_type], asset_name]) |
| |
| # Always ask actool to generate a partial Info.plist file. If not path |
| # has been given by the caller, use a temporary file name. |
| temporary_file = None |
| if not partial_info_plist: |
| temporary_file = tempfile.NamedTemporaryFile(suffix='.plist') |
| partial_info_plist = temporary_file.name |
| |
| command.extend(['--output-partial-info-plist', partial_info_plist]) |
| |
| # Dictionary used to convert absolute paths back to their relative form |
| # in the output of actool. |
| relative_paths = {} |
| |
| # actool crashes if paths are relative, so convert input and output paths |
| # to absolute paths, and record the relative paths to fix them back when |
| # filtering the output. |
| absolute_output = os.path.abspath(output) |
| relative_paths[output] = absolute_output |
| relative_paths[os.path.dirname(output)] = os.path.dirname(absolute_output) |
| command.extend(['--compile', os.path.dirname(os.path.abspath(output))]) |
| |
| for relative_path in inputs: |
| absolute_path = os.path.abspath(relative_path) |
| relative_paths[absolute_path] = relative_path |
| command.append(absolute_path) |
| |
| try: |
| # Run actool and redirect stdout and stderr to the same pipe (as actool |
| # is confused about what should go to stderr/stdout). |
| process = subprocess.Popen(command, |
| stdout=subprocess.PIPE, |
| stderr=subprocess.STDOUT) |
| stdout, _ = process.communicate() |
| |
| # Filter the output to remove all garbarge and to fix the paths. |
| stdout = FilterCompilerOutput(stdout.decode('UTF-8'), relative_paths) |
| |
| if process.returncode or stdout: |
| sys.stderr.write(stdout) |
| sys.exit(1) |
| |
| finally: |
| if temporary_file: |
| temporary_file.close() |
| |
| |
| def Main(): |
| parser = argparse.ArgumentParser( |
| description='compile assets catalog for a bundle') |
| parser.add_argument('--platform', |
| '-p', |
| required=True, |
| choices=('macosx', 'iphoneos', 'iphonesimulator'), |
| help='target platform for the compiled assets catalog') |
| parser.add_argument( |
| '--minimum-deployment-target', |
| '-t', |
| required=True, |
| help='minimum deployment target for the compiled assets catalog') |
| parser.add_argument('--output', |
| '-o', |
| required=True, |
| help='path to the compiled assets catalog') |
| parser.add_argument('--compress-pngs', |
| '-c', |
| action='store_true', |
| default=False, |
| help='recompress PNGs while compiling assets catalog') |
| parser.add_argument('--product-type', |
| '-T', |
| help='type of the containing bundle') |
| parser.add_argument('--partial-info-plist', |
| '-P', |
| help='path to partial info plist to create') |
| parser.add_argument('inputs', |
| nargs='+', |
| help='path to input assets catalog sources') |
| args = parser.parse_args() |
| |
| if os.path.basename(args.output) != 'Assets.car': |
| sys.stderr.write('output should be path to compiled asset catalog, not ' |
| 'to the containing bundle: %s\n' % (args.output, )) |
| sys.exit(1) |
| |
| CompileAssetCatalog(args.output, args.platform, args.product_type, |
| args.minimum_deployment_target, args.inputs, |
| args.compress_pngs, args.partial_info_plist) |
| |
| |
| if __name__ == '__main__': |
| sys.exit(Main()) |