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