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