Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(291)

Side by Side Diff: tools/isolate_driver.py

Issue 228463003: Make isolate_driver.py process build.ninja and extract dependencies. (Closed) Base URL: svn://svn.chromium.org/chrome/trunk/src
Patch Set: Remove logging message now that it works Created 6 years, 8 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch | Annotate | Revision Log
« no previous file with comments | « no previous file | no next file » | no next file with comments »
Toggle Intra-line Diffs ('i') | Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
OLDNEW
1 #!/usr/bin/env python 1 #!/usr/bin/env python
2 # Copyright 2014 The Chromium Authors. All rights reserved. 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 3 # Use of this source code is governed by a BSD-style license that can be
4 # found in the LICENSE file. 4 # found in the LICENSE file.
5 5
6 """Adaptor script called through build/isolate.gypi. 6 """Adaptor script called through build/isolate.gypi.
7 7
8 Creates a wrapping .isolate which 'includes' the original one, that can be 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 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 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 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. 12 be used so the wrapping .isolate doesn't affect this value.
13 13
14 It packages all the dynamic libraries found in this wrapping .isolate. This is 14 This script loads build.ninja and processes it to determine all the executables
15 inefficient and non-deterministic. In the very near future, it will parse 15 referenced by the isolated target. It adds them in the wrapping .isolate file.
16 build.ninja, find back the root target and find all the dynamic libraries that
17 are marked as a dependency to this target.
18 """ 16 """
19 17
18 import StringIO
20 import glob 19 import glob
20 import logging
21 import os 21 import os
22 import posixpath 22 import posixpath
23 import subprocess 23 import subprocess
24 import sys 24 import sys
25 import time 25 import time
26 26
27 TOOLS_DIR = os.path.dirname(os.path.abspath(__file__)) 27 TOOLS_DIR = os.path.dirname(os.path.abspath(__file__))
28 SWARMING_CLIENT_DIR = os.path.join(TOOLS_DIR, 'swarming_client') 28 SWARMING_CLIENT_DIR = os.path.join(TOOLS_DIR, 'swarming_client')
29 SRC_DIR = os.path.dirname(TOOLS_DIR) 29 SRC_DIR = os.path.dirname(TOOLS_DIR)
30 30
31 sys.path.insert(0, SWARMING_CLIENT_DIR) 31 sys.path.insert(0, SWARMING_CLIENT_DIR)
32 32
33 import isolate_format 33 import isolate_format
34 34
35 35
36 def load_ninja_recursively(build_dir, ninja_path, build_steps):
37 """Crudely extracts all the subninja and build referenced in ninja_path.
36 38
37 # Location to grab binaries based on the OS. 39 In particular, it ignores rule and variable declarations. The goal is to be
38 DYNAMIC_LIBRARIES = { 40 performant (well, as much as python can be performant) which is currently in
39 'darwin': '*.dylib', 41 the <200ms range for a complete chromium tree. As such the code is laid out
40 'linux2': 'lib/*.so', 42 for performance instead of readability.
41 'win32': '*.dll', 43 """
42 } 44 logging.debug('Loading %s', ninja_path)
45 try:
46 with open(os.path.join(build_dir, ninja_path), 'rb') as f:
47 line = None
48 merge_line = ''
49 subninja = []
50 for line in f:
51 line = line.rstrip()
52 if not line:
53 continue
54
55 if line[-1] == '$':
56 # The next line needs to be merged in.
57 merge_line += line[:-1]
58 continue
59
60 if merge_line:
61 line = merge_line + line
62 merge_line = ''
63
64 statement = line[:line.find(' ')]
65 if statement == 'build':
66 # Save the dependency list as a raw string. Only the lines needed will
67 # be processed with raw_build_to_deps(). This saves a good 70ms of
68 # processing time.
69 build_target, dependencies = line[6:].split(': ', 1)
70 # Interestingly, trying to be smart and only saving the build steps
71 # with the intended extensions ('', '.stamp', '.so') slows down
72 # parsing even if 90% of the build rules can be skipped.
73 # On Windows, a single step may generate two target, so split items
74 # accordingly. It has only been seen for .exe/.exe.pdb combos.
75 for i in build_target.strip().split():
76 build_steps[i] = dependencies
77 elif statement == 'subninja':
78 subninja.append(line[9:])
79 except IOError:
80 print >> sys.stderr, 'Failed to open %s' % ninja_path
81 raise
82
83 total = 1
84 for rel_path in subninja:
85 try:
86 # Load each of the files referenced.
87 # TODO(maruel): Skip the files known to not be needed. It saves an aweful
88 # lot of processing time.
89 total += load_ninja_recursively(build_dir, rel_path, build_steps)
90 except IOError:
91 print >> sys.stderr, '... as referenced by %s' % ninja_path
92 raise
93 return total
43 94
44 95
45 def get_dynamic_libs(build_dir): 96 def load_ninja(build_dir):
46 """Finds all the dynamic libs to map. 97 """Loads the tree of .ninja files in build_dir."""
98 build_steps = {}
99 total = load_ninja_recursively(build_dir, 'build.ninja', build_steps)
100 logging.info('Loaded %d ninja files, %d build steps', total, len(build_steps))
101 return build_steps
47 102
48 Returns: 103
49 list of relative path, e.g. [../out/Debug/lib/libuser_prefs.so]. 104 def using_blacklist(item):
105 """Returns True if an item should be analyzed.
106
107 Ignores many rules that are assumed to not depend on a dynamic library. If
108 the assumption doesn't hold true anymore for a file format, remove it from
109 this list. This is simply an optimization.
50 """ 110 """
51 items = set() 111 IGNORED = (
52 root = os.path.join(build_dir, DYNAMIC_LIBRARIES[sys.platform]) 112 '.a', '.cc', '.css', '.def', '.h', '.html', '.js', '.json', '.manifest',
53 for i in glob.iglob(root): 113 '.o', '.obj', '.pak', '.png', '.pdb', '.strings', '.txt',
54 try: 114 )
55 # Will throw on Windows if another process is writing to this file. 115 # ninja files use native path format.
56 open(i).close() 116 ext = os.path.splitext(item)[1]
57 items.add((i, os.stat(i).st_size)) 117 if ext in IGNORED:
58 except IOError: 118 return False
59 continue 119 # Special case Windows, keep .dll.lib but discard .lib.
120 if item.endswith('.dll.lib'):
121 return True
122 if ext == '.lib':
123 return False
124 return item not in ('', '|', '||')
60 125
61 # The following sleep value was carefully selected via random guessing. The
62 # goal is to detect files that are being linked by checking their file size
63 # after a small delay.
64 #
65 # This happens as other build targets can be built simultaneously. For
66 # example, base_unittests.isolated is being processed but dynamic libraries
67 # for chrome are currently being linked.
68 #
69 # TODO(maruel): Obviously, this must go away and be replaced with a proper
70 # ninja parser but we need something now. http://crbug.com/333473
71 time.sleep(10)
72 126
73 for item in sorted(items): 127 def raw_build_to_deps(item):
74 file_name, file_size = item 128 """Converts a raw ninja build statement into the list of interesting
75 try: 129 dependencies.
76 open(file_name).close() 130 """
77 if os.stat(file_name).st_size != file_size: 131 # TODO(maruel): Use a whitelist instead? .stamp, .so.TOC, .dylib.TOC,
78 items.remove(item) 132 # .dll.lib, .exe and empty.
79 except IOError: 133 # The first item is the build rule, e.g. 'link', 'cxx', 'phony', etc.
80 items.remove(item) 134 return filter(using_blacklist, item.split(' ')[1:])
81 continue
82 135
83 return [i[0].replace(os.path.sep, '/') for i in items] 136
137 def recurse(target, build_steps, rules_seen):
138 """Recursively returns all the interesting dependencies for root_item."""
139 out = []
140 if rules_seen is None:
141 rules_seen = set()
142 if target in rules_seen:
143 # TODO(maruel): Figure out how it happens.
144 logging.warning('Circular dependency for %s!', target)
145 return []
146 rules_seen.add(target)
147 try:
148 dependencies = raw_build_to_deps(build_steps[target])
149 except KeyError:
150 logging.info('Failed to find a build step to generate: %s', target)
151 return []
152 logging.debug('recurse(%s) -> %s', target, dependencies)
153 for dependency in dependencies:
154 out.append(dependency)
155 dependency_raw_dependencies = build_steps.get(dependency)
156 if dependency_raw_dependencies:
157 for i in raw_build_to_deps(dependency_raw_dependencies):
158 out.extend(recurse(i, build_steps, rules_seen))
159 else:
160 logging.info('Failed to find a build step to generate: %s', dependency)
161 return out
162
163
164 def post_process_deps(dependencies):
165 """Processes the dependency list with OS specific rules."""
166 def filter_item(i):
167 if i.endswith('.so.TOC'):
168 # Remove only the suffix .TOC, not the .so!
169 return i[:-4]
170 if i.endswith('.dylib.TOC'):
171 # Remove only the suffix .TOC, not the .dylib!
172 return i[:-4]
173 if i.endswith('.dll.lib'):
174 # Remove only the suffix .lib, not the .dll!
175 return i[:-4]
176 return i
177
178 return map(filter_item, dependencies)
84 179
85 180
86 def create_wrapper(args, isolate_index, isolated_index): 181 def create_wrapper(args, isolate_index, isolated_index):
87 """Creates a wrapper .isolate that add dynamic libs. 182 """Creates a wrapper .isolate that add dynamic libs.
88 183
89 The original .isolate is not modified. 184 The original .isolate is not modified.
90 """ 185 """
91 cwd = os.getcwd() 186 cwd = os.getcwd()
92 isolate = args[isolate_index] 187 isolate = args[isolate_index]
93 # The code assumes the .isolate file is always specified path-less in cwd. Fix 188 # The code assumes the .isolate file is always specified path-less in cwd. Fix
(...skipping 10 matching lines...) Expand all
104 199
105 # The wrapping .isolate. This will look like 200 # The wrapping .isolate. This will look like
106 # ../out/Debug/gen/chrome/unit_tests.isolate. 201 # ../out/Debug/gen/chrome/unit_tests.isolate.
107 temp_isolate = os.path.join(build_dir, 'gen', src_isolate) 202 temp_isolate = os.path.join(build_dir, 'gen', src_isolate)
108 temp_isolate_dir = os.path.dirname(temp_isolate) 203 temp_isolate_dir = os.path.dirname(temp_isolate)
109 204
110 # Relative path between the new and old .isolate file. 205 # Relative path between the new and old .isolate file.
111 isolate_relpath = os.path.relpath( 206 isolate_relpath = os.path.relpath(
112 '.', temp_isolate_dir).replace(os.path.sep, '/') 207 '.', temp_isolate_dir).replace(os.path.sep, '/')
113 208
114 # Will look like ['<(PRODUCT_DIR)/lib/flibuser_prefs.so']. 209 # It's a big assumption here that the name of the isolate file matches the
115 rebased_libs = [ 210 # primary target. Fix accordingly if this doesn't hold true.
116 '<(PRODUCT_DIR)/%s' % i[len(build_dir)+1:] 211 target = isolate[:-len('.isolate')]
117 for i in get_dynamic_libs(build_dir) 212 build_steps = load_ninja(build_dir)
118 ] 213 binary_deps = post_process_deps(recurse(target, build_steps, None))
214 logging.debug(
215 'Binary dependencies:%s', ''.join('\n ' + i for i in binary_deps))
119 216
120 # Now do actual wrapping .isolate. 217 # Now do actual wrapping .isolate.
121 out = { 218 isolate_dict = {
122 'includes': [ 219 'includes': [
123 posixpath.join(isolate_relpath, isolate), 220 posixpath.join(isolate_relpath, isolate),
124 ], 221 ],
125 'variables': { 222 'variables': {
126 isolate_format.KEY_TRACKED: rebased_libs, 223 # Will look like ['<(PRODUCT_DIR)/lib/flibuser_prefs.so'].
224 isolate_format.KEY_TRACKED: sorted(
225 '<(PRODUCT_DIR)/%s' % i.replace(os.path.sep, '/')
226 for i in binary_deps),
127 }, 227 },
128 } 228 }
129 if not os.path.isdir(temp_isolate_dir): 229 if not os.path.isdir(temp_isolate_dir):
130 os.makedirs(temp_isolate_dir) 230 os.makedirs(temp_isolate_dir)
131 comment = ( 231 comment = (
132 '# Warning: this file was AUTOGENERATED.\n' 232 '# Warning: this file was AUTOGENERATED.\n'
133 '# DO NO EDIT.\n') 233 '# DO NO EDIT.\n')
234 out = StringIO.StringIO()
235 isolate_format.print_all(comment, isolate_dict, out)
236 isolate_content = out.getvalue()
134 with open(temp_isolate, 'wb') as f: 237 with open(temp_isolate, 'wb') as f:
135 isolate_format.print_all(comment, out, f) 238 f.write(isolate_content)
136 if '--verbose' in args: 239 logging.info('Added %d dynamic libs', len(binary_deps))
137 print('Added %d dynamic libs' % len(dynamic_libs)) 240 logging.debug('%s', isolate_content)
138 args[isolate_index] = temp_isolate 241 args[isolate_index] = temp_isolate
139 242
140 243
141 def main(): 244 def main():
245 logging.basicConfig(level=logging.ERROR, format='%(levelname)7s %(message)s')
142 args = sys.argv[1:] 246 args = sys.argv[1:]
143 isolate = None 247 isolate = None
144 isolated = None 248 isolated = None
145 is_component = False 249 is_component = False
146 for i, arg in enumerate(args): 250 for i, arg in enumerate(args):
147 if arg == '--isolate': 251 if arg == '--isolate':
148 isolate = i + 1 252 isolate = i + 1
149 if arg == '--isolated': 253 if arg == '--isolated':
150 isolated = i + 1 254 isolated = i + 1
151 if arg == 'component=shared_library': 255 if arg == 'component=shared_library':
152 is_component = True 256 is_component = True
153 if isolate is None or isolated is None: 257 if isolate is None or isolated is None:
154 print >> sys.stderr, 'Internal failure' 258 print >> sys.stderr, 'Internal failure'
155 return 1 259 return 1
156 260
157 # Implement a ninja parser. 261 if is_component:
158 # http://crbug.com/360223
159 if is_component and False:
160 create_wrapper(args, isolate, isolated) 262 create_wrapper(args, isolate, isolated)
161 263
162 swarming_client = os.path.join(SRC_DIR, 'tools', 'swarming_client') 264 swarming_client = os.path.join(SRC_DIR, 'tools', 'swarming_client')
163 sys.stdout.flush() 265 sys.stdout.flush()
164 result = subprocess.call( 266 result = subprocess.call(
165 [sys.executable, os.path.join(swarming_client, 'isolate.py')] + args) 267 [sys.executable, os.path.join(swarming_client, 'isolate.py')] + args)
166 return result 268 return result
167 269
168 270
169 if __name__ == '__main__': 271 if __name__ == '__main__':
170 sys.exit(main()) 272 sys.exit(main())
OLDNEW
« no previous file with comments | « no previous file | no next file » | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698