1 #!/usr/bin/env python 2 # Copyright 2014 The Chromium Authors. All rights reserved. 3 # Use of this source code is governed by a BSD-style license that can be 4 # found in the LICENSE file. 5 6 """Adaptor script called through build/isolate.gypi. 7 8 Creates a wrapping .isolate which 'includes' the original one, that can be 9 consumed by tools/swarming_client/isolate.py. Path variables are determined 10 based on the current working directory. The relative_cwd in the .isolated file 11 is determined based on the .isolate file that declare the 'command' variable to 12 be used so the wrapping .isolate doesn't affect this value. 13 14 This script loads build.ninja and processes it to determine all the executables 15 referenced by the isolated target. It adds them in the wrapping .isolate file. 16 17 WARNING: The target to use for build.ninja analysis is the base name of the 18 .isolate file plus '_run'. For example, 'foo_test.isolate' would have the target 19 'foo_test_run' analysed. 20 """ 21 22 import glob 23 import json 24 import logging 25 import os 26 import posixpath 27 import StringIO 28 import subprocess 29 import sys 30 import time 31 32 TOOLS_DIR = os.path.dirname(os.path.abspath(__file__)) 33 SWARMING_CLIENT_DIR = os.path.join(TOOLS_DIR, 'swarming_client') 34 SRC_DIR = os.path.dirname(TOOLS_DIR) 35 36 sys.path.insert(0, SWARMING_CLIENT_DIR) 37 38 import isolate_format 39 40 41 def load_ninja_recursively(build_dir, ninja_path, build_steps): 42 """Crudely extracts all the subninja and build referenced in ninja_path. 43 44 In particular, it ignores rule and variable declarations. The goal is to be 45 performant (well, as much as python can be performant) which is currently in 46 the <200ms range for a complete chromium tree. As such the code is laid out 47 for performance instead of readability. 48 """ 49 logging.debug('Loading %s', ninja_path) 50 try: 51 with open(os.path.join(build_dir, ninja_path), 'rb') as f: 52 line = None 53 merge_line = '' 54 subninja = [] 55 for line in f: 56 line = line.rstrip() 57 if not line: 58 continue 59 60 if line[-1] == '$': 61 # The next line needs to be merged in. 62 merge_line += line[:-1] 63 continue 64 65 if merge_line: 66 line = merge_line + line 67 merge_line = '' 68 69 statement = line[:line.find(' ')] 70 if statement == 'build': 71 # Save the dependency list as a raw string. Only the lines needed will 72 # be processed with raw_build_to_deps(). This saves a good 70ms of 73 # processing time. 74 build_target, dependencies = line[6:].split(': ', 1) 75 # Interestingly, trying to be smart and only saving the build steps 76 # with the intended extensions ('', '.stamp', '.so') slows down 77 # parsing even if 90% of the build rules can be skipped. 78 # On Windows, a single step may generate two target, so split items 79 # accordingly. It has only been seen for .exe/.exe.pdb combos. 80 for i in build_target.strip().split(): 81 build_steps[i] = dependencies 82 elif statement == 'subninja': 83 subninja.append(line[9:]) 84 except IOError: 85 print >> sys.stderr, 'Failed to open %s' % ninja_path 86 raise 87 88 total = 1 89 for rel_path in subninja: 90 try: 91 # Load each of the files referenced. 92 # TODO(maruel): Skip the files known to not be needed. It saves an aweful 93 # lot of processing time. 94 total += load_ninja_recursively(build_dir, rel_path, build_steps) 95 except IOError: 96 print >> sys.stderr, '... as referenced by %s' % ninja_path 97 raise 98 return total 99 100 101 def load_ninja(build_dir): 102 """Loads the tree of .ninja files in build_dir.""" 103 build_steps = {} 104 total = load_ninja_recursively(build_dir, 'build.ninja', build_steps) 105 logging.info('Loaded %d ninja files, %d build steps', total, len(build_steps)) 106 return build_steps 107 108 109 def using_blacklist(item): 110 """Returns True if an item should be analyzed. 111 112 Ignores many rules that are assumed to not depend on a dynamic library. If 113 the assumption doesn't hold true anymore for a file format, remove it from 114 this list. This is simply an optimization. 115 """ 116 IGNORED = ( 117 '.a', '.cc', '.css', '.def', '.frag', '.h', '.html', '.js', '.json', 118 '.manifest', '.o', '.obj', '.pak', '.png', '.pdb', '.strings', '.test', 119 '.txt', '.vert', 120 ) 121 # ninja files use native path format. 122 ext = os.path.splitext(item)[1] 123 if ext in IGNORED: 124 return False 125 # Special case Windows, keep .dll.lib but discard .lib. 126 if item.endswith('.dll.lib'): 127 return True 128 if ext == '.lib': 129 return False 130 return item not in ('', '|', '||') 131 132 133 def raw_build_to_deps(item): 134 """Converts a raw ninja build statement into the list of interesting 135 dependencies. 136 """ 137 # TODO(maruel): Use a whitelist instead? .stamp, .so.TOC, .dylib.TOC, 138 # .dll.lib, .exe and empty. 139 # The first item is the build rule, e.g. 'link', 'cxx', 'phony', etc. 140 return filter(using_blacklist, item.split(' ')[1:]) 141 142 143 def collect_deps(target, build_steps, dependencies_added, rules_seen): 144 """Recursively adds all the interesting dependencies for |target| 145 into |dependencies_added|. 146 """ 147 if rules_seen is None: 148 rules_seen = set() 149 if target in rules_seen: 150 # TODO(maruel): Figure out how it happens. 151 logging.warning('Circular dependency for %s!', target) 152 return 153 rules_seen.add(target) 154 try: 155 dependencies = raw_build_to_deps(build_steps[target]) 156 except KeyError: 157 logging.info('Failed to find a build step to generate: %s', target) 158 return 159 logging.debug('collect_deps(%s) -> %s', target, dependencies) 160 for dependency in dependencies: 161 dependencies_added.add(dependency) 162 collect_deps(dependency, build_steps, dependencies_added, rules_seen) 163 164 165 def post_process_deps(build_dir, dependencies): 166 """Processes the dependency list with OS specific rules.""" 167 def filter_item(i): 168 if i.endswith('.so.TOC'): 169 # Remove only the suffix .TOC, not the .so! 170 return i[:-4] 171 if i.endswith('.dylib.TOC'): 172 # Remove only the suffix .TOC, not the .dylib! 173 return i[:-4] 174 if i.endswith('.dll.lib'): 175 # Remove only the suffix .lib, not the .dll! 176 return i[:-4] 177 return i 178 179 def f(i): 180 # This script is only for adding new binaries that are created as part of 181 # the component build. 182 ext = os.path.splitext(i)[1] 183 if ext not in ['.dll', '.nexe', '.so', '.dylib']: 184 return False 185 186 # Check for execute access and strip directories. This gets rid of all the 187 # phony rules. 188 p = os.path.join(build_dir, i) 189 return os.access(p, os.X_OK) and not os.path.isdir(p) 190 191 return filter(f, map(filter_item, dependencies)) 192 193 194 def create_wrapper(args, isolate_index, isolated_index): 195 """Creates a wrapper .isolate that add dynamic libs. 196 197 The original .isolate is not modified. 198 """ 199 cwd = os.getcwd() 200 isolate = args[isolate_index] 201 # The code assumes the .isolate file is always specified path-less in cwd. Fix 202 # if this assumption doesn't hold true. 203 assert os.path.basename(isolate) == isolate, isolate 204 205 # This will look like ../out/Debug. This is based against cwd. Note that this 206 # must equal the value provided as PRODUCT_DIR. 207 build_dir = os.path.dirname(args[isolated_index]) 208 209 # This will look like chrome/unit_tests.isolate. It is based against SRC_DIR. 210 # It's used to calculate temp_isolate. 211 src_isolate = os.path.relpath(os.path.join(cwd, isolate), SRC_DIR) 212 213 # The wrapping .isolate. This will look like 214 # ../out/Debug/gen/chrome/unit_tests.isolate. 215 temp_isolate = os.path.join(build_dir, 'gen', src_isolate) 216 temp_isolate_dir = os.path.dirname(temp_isolate) 217 218 # Relative path between the new and old .isolate file. 219 isolate_relpath = os.path.relpath( 220 '.', temp_isolate_dir).replace(os.path.sep, '/') 221 222 # It's a big assumption here that the name of the isolate file matches the 223 # primary target '_run'. Fix accordingly if this doesn't hold true, e.g. 224 # complain to maruel@. 225 target = isolate[:-len('.isolate')] + '_run' 226 build_steps = load_ninja(build_dir) 227 binary_deps = set() 228 collect_deps(target, build_steps, binary_deps, None) 229 binary_deps = post_process_deps(build_dir, binary_deps) 230 logging.debug( 231 'Binary dependencies:%s', ''.join('\n ' + i for i in binary_deps)) 232 233 # Now do actual wrapping .isolate. 234 isolate_dict = { 235 'includes': [ 236 posixpath.join(isolate_relpath, isolate), 237 ], 238 'variables': { 239 # Will look like ['<(PRODUCT_DIR)/lib/flibuser_prefs.so']. 240 isolate_format.KEY_TRACKED: sorted( 241 '<(PRODUCT_DIR)/%s' % i.replace(os.path.sep, '/') 242 for i in binary_deps), 243 }, 244 } 245 if not os.path.isdir(temp_isolate_dir): 246 os.makedirs(temp_isolate_dir) 247 comment = ( 248 '# Warning: this file was AUTOGENERATED.\n' 249 '# DO NO EDIT.\n') 250 out = StringIO.StringIO() 251 isolate_format.print_all(comment, isolate_dict, out) 252 isolate_content = out.getvalue() 253 with open(temp_isolate, 'wb') as f: 254 f.write(isolate_content) 255 logging.info('Added %d dynamic libs', len(binary_deps)) 256 logging.debug('%s', isolate_content) 257 args[isolate_index] = temp_isolate 258 259 260 def prepare_isolate_call(args, output): 261 """Gathers all information required to run isolate.py later. 262 263 Dumps it as JSON to |output| file. 264 """ 265 with open(output, 'wb') as f: 266 json.dump({ 267 'args': args, 268 'dir': os.getcwd(), 269 'version': 1, 270 }, f, indent=2, sort_keys=True) 271 272 273 def main(): 274 logging.basicConfig(level=logging.ERROR, format='%(levelname)7s %(message)s') 275 args = sys.argv[1:] 276 mode = args[0] if args else None 277 isolate = None 278 isolated = None 279 is_component = False 280 for i, arg in enumerate(args): 281 if arg == '--isolate': 282 isolate = i + 1 283 if arg == '--isolated': 284 isolated = i + 1 285 if arg == 'component=shared_library': 286 is_component = True 287 if isolate is None or isolated is None or not mode: 288 print >> sys.stderr, 'Internal failure' 289 return 1 290 291 if is_component: 292 create_wrapper(args, isolate, isolated) 293 294 # In 'prepare' mode just collect all required information for postponed 295 # isolated.py invocation later, store it in *.isolated.gen.json file. 296 if mode == 'prepare': 297 prepare_isolate_call(args[1:], args[isolated] + '.gen.json') 298 return 0 299 300 swarming_client = os.path.join(SRC_DIR, 'tools', 'swarming_client') 301 sys.stdout.flush() 302 result = subprocess.call( 303 [sys.executable, os.path.join(swarming_client, 'isolate.py')] + args) 304 return result 305 306 307 if __name__ == '__main__': 308 sys.exit(main()) 309