|  | # 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 = map(re.compile, [ | 
|  | # 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, 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()) |