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

Side by Side Diff: tools/ninja_parser.py

Issue 270333002: Enable the ninja parsing code all the time. (Closed) Base URL: svn://svn.chromium.org/chrome/trunk/src
Patch Set: more comments Created 6 years, 7 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 | « tools/isolate_driver.py ('k') | 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
(Empty)
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 """Primitive ninja parser.
7
8 It's primary use case is to be used by isolate_driver.py. It is a standalone
9 tool so it can be used to verify its behavior more efficiently. It's a quirky
10 tool since it is designed only to gather binary dependencies.
11
12 <directory> is assumed to be based on src/, so usually it will have the form
13 out/Release. Any target outside this directory is ignored.
14
15 This script is assumed to be run *after* the target was built, since it looks
16 for the +x bit on the files and only returns the ones that exist.
17 """
18
19 import logging
20 import os
21 import optparse
22 import sys
23 import time
24
25 TOOLS_DIR = os.path.dirname(os.path.abspath(__file__))
26 SRC_DIR = os.path.dirname(TOOLS_DIR)
27
28
29 ### Private stuff.
30
31
32 _HEX = frozenset('0123456789abcdef')
33
34
35 # Ignore any dependency with one of these extensions. In particular, we do not
36 # want to mark tool that would have generated these as binary dependencies,
37 # these are likely tools only used during the build process that are not
38 # necessary at runtime. The worst thing that would happen is either:
39 # - Mapping tools generating a source file but not necessary at run time.
40 # - Slower processing.
41 # On the other hand, being too aggressive will hide legitimate runtime
42 # dependencies. In particular, .a may cause an explicit dependency on a .so.
43 _IGNORED_EXTENSIONS = frozenset([
44 '.asm', '.c', '.cc', '.cpp', '.css', '.def', '.grd', '.gypcmd', '.h',
45 '.html', '.idl', '.in', '.jinja2', '.js', '.json', '.manifest', '.mm', '.o',
46 '.obj', '.pak', '.pickle', '.png', '.pdb', '.prep', '.proto', '.py', '.rc',
47 '.strings', '.svg', '.tmp', '.ttf', '.txt', '.xtb', '.wav',
48 ])
49
50
51 def _load_ninja_recursively(build_dir, ninja_path, build_steps):
52 """Crudely extracts all the subninja and build referenced in ninja_path.
53
54 In particular, it ignores rule and variable declarations. The goal is to be
55 performant (well, as much as python can be performant) which is currently in
56 the <200ms range for a complete chromium tree. As such the code is laid out
57 for performance instead of readability.
58 """
59 logging.debug('Loading %s', ninja_path)
60 try:
61 with open(os.path.join(build_dir, ninja_path), 'rb') as f:
62 line = None
63 merge_line = ''
64 subninja = []
65 for line in f:
66 line = line.rstrip()
67 if not line:
68 continue
69
70 if line[-1] == '$':
71 # The next line needs to be merged in.
72 if merge_line:
73 merge_line += ' ' + line[:-1].strip(' ')
74 else:
75 merge_line = line[:-1].strip(' ')
76 continue
77
78 if merge_line:
79 line = merge_line + ' ' + line.strip(' ')
80 merge_line = ''
81
82 statement = line[:line.find(' ')]
83 if statement == 'build':
84 # Save the dependency list as a raw string. Only the lines needed will
85 # be processed within _recurse(). This saves a good 70ms of processing
86 # time.
87 build_target, dependencies = line[6:].split(': ', 1)
88 # Interestingly, trying to be smart and only saving the build steps
89 # with the intended extensions ('', '.stamp', '.so') slows down
90 # parsing even if 90% of the build rules can be skipped.
91 # On Windows, a single step may generate two target, so split items
92 # accordingly. It has only been seen for .exe/.exe.pdb combos.
93 for i in build_target.strip().split():
94 build_steps[i] = dependencies
95 elif statement == 'subninja':
96 subninja.append(line[9:])
97 except IOError:
98 print >> sys.stderr, 'Failed to open %s' % ninja_path
99 raise
100
101 total = 1
102 for rel_path in subninja:
103 try:
104 # Load each of the files referenced.
105 # TODO(maruel): Skip the files known to not be needed. It saves an aweful
106 # lot of processing time.
107 total += _load_ninja_recursively(build_dir, rel_path, build_steps)
108 except IOError:
109 print >> sys.stderr, '... as referenced by %s' % ninja_path
110 raise
111 return total
112
113
114 def _simple_blacklist(item):
115 """Returns True if an item should be analyzed."""
116 return item not in ('', '|', '||')
117
118
119 def _using_blacklist(item):
120 """Returns True if an item should be analyzed.
121
122 Ignores many rules that are assumed to not depend on a dynamic library. If
123 the assumption doesn't hold true anymore for a file format, remove it from
124 this list. This is simply an optimization.
125 """
126 # ninja files use native path format.
127 ext = os.path.splitext(item)[1]
128 if ext in _IGNORED_EXTENSIONS:
129 return False
130 # Special case Windows, keep .dll.lib but discard .lib.
131 if item.endswith('.dll.lib'):
132 return True
133 if ext == '.lib':
134 return False
135 return _simple_blacklist(item)
136
137
138 def _should_process(build_dir, target, build_steps, rules_seen):
139 """Returns the raw dependencies if the target should be processed."""
140 if target in rules_seen:
141 # The rule was already seen. Since rules_seen is not scoped at the target
142 # visibility, it usually simply means that multiple targets depends on the
143 # same dependencies. It's fine.
144 return None
145
146 raw_dependencies = build_steps.get(target, None)
147 if raw_dependencies is None:
148 # There is not build step defined to generate 'target'.
149 parts = target.rsplit('_', 1)
150 if len(parts) == 2 and len(parts[1]) == 32 and _HEX.issuperset(parts[1]):
151 # It's because it is a phony rule.
152 return None
153
154 # Kind of a hack, assume source files are always outside build_bir.
155 if (target.startswith('..') and
156 os.path.exists(os.path.join(build_dir, target))):
157 # It's because it is a source file.
158 return None
159
160 logging.debug('Failed to find a build step to generate: %s', target)
161 return None
162 return raw_dependencies
163
164
165 def _recurse(build_dir, target, build_steps, rules_seen, blacklist):
166 raw_dependencies = _should_process(build_dir, target, build_steps, rules_seen)
167 rules_seen.add(target)
168 if raw_dependencies is None:
169 return []
170
171 out = [target]
172 # Filter out what we don't want to speed things up. This cuts off large parts
173 # of the dependency tree to analyze.
174 # The first item is the build rule, e.g. 'link', 'cxx', 'phony', 'stamp', etc.
175 dependencies = filter(blacklist, raw_dependencies.split(' ')[1:])
176 logging.debug('recurse(%s) -> %s', target, dependencies)
177 for dependency in dependencies:
178 out.extend(_recurse(
179 build_dir, dependency, build_steps, rules_seen, blacklist))
180 return out
181
182
183 def _find_link(build_dir, target, build_steps, rules_seen, search_for):
184 raw_dependencies = _should_process(build_dir, target, build_steps, rules_seen)
185 rules_seen.add(target)
186 if raw_dependencies is None:
187 return
188
189 # Filter out what we don't want to speed things up. This cuts off large parts
190 # of the dependency tree to analyze.
191 # The first item is the build rule, e.g. 'link', 'cxx', 'phony', 'stamp', etc.
192 dependencies = filter(_simple_blacklist, raw_dependencies.split(' ')[1:])
193 for dependency in dependencies:
194 if dependency == search_for:
195 yield [dependency]
196 else:
197 for out in _find_link(
198 build_dir, dependency, build_steps, rules_seen, search_for):
199 yield [dependency] + out
200
201
202 ### Public API.
203
204
205 def load_ninja(build_dir):
206 """Loads build.ninja and the tree of .ninja files in build_dir.
207
208 Returns:
209 dict of the target->build_step where build_step is an unprocessed str.
210
211 TODO(maruel): This should really just be done by ninja itself, then simply
212 process its output.
213 """
214 build_steps = {}
215 total = _load_ninja_recursively(build_dir, 'build.ninja', build_steps)
216 logging.info('Loaded %d ninja files, %d build steps', total, len(build_steps))
217 return build_steps
218
219
220 def recurse(build_dir, target, build_steps, blacklist=_using_blacklist):
221 """Recursively returns all the interesting dependencies for the target
222 specified.
223 """
224 return _recurse(build_dir, target, build_steps, set(), blacklist)
225
226
227 def find_link(build_dir, target, build_steps, search_for):
228 """Finds all the links from 'target' to 'search_for'.
229
230 Example:
231 With target='foo' and search_for='baz', if foo depends on bar which depends
232 on baz, it will yield only one list, ['foo', 'bar', 'baz']. If there were
233 multiple dependency links in the DAG, each one would be yield as a list.
234 """
235 for link in _find_link(build_dir, target, build_steps, set(), search_for):
236 yield link
237
238
239 def post_process_deps(build_dir, dependencies):
240 """Processes the dependency list with OS specific rules.
241
242 It maps the dynamic libraries import library into their actual dynamic
243 library.
244
245 Must be run after the build is completed.
246 """
247 def filter_item(i):
248 if i.endswith('.so.TOC'):
249 # Remove only the suffix .TOC, not the .so!
250 return i[:-4]
251 if i.endswith('.dylib.TOC'):
252 # Remove only the suffix .TOC, not the .dylib!
253 return i[:-4]
254 if i.endswith('.dll.lib'):
255 # Remove only the suffix .lib, not the .dll!
256 return i[:-4]
257 return i
258
259 # Check for execute access. This gets rid of all the phony rules.
260 return [
261 i for i in map(filter_item, dependencies)
262 if os.access(os.path.join(build_dir, i), os.X_OK)
263 ]
264
265
266 def main():
267 parser = optparse.OptionParser(
268 usage='%prog [options] <directory> <target> <search_for>',
269 description=sys.modules[__name__].__doc__)
270 parser.add_option(
271 '-v', '--verbose', action='count', default=0,
272 help='Use twice for more info')
273 options, args = parser.parse_args()
274
275 levels = (logging.ERROR, logging.INFO, logging.DEBUG)
276 logging.basicConfig(
277 level=levels[min(len(levels)-1, options.verbose)],
278 format='%(levelname)7s %(message)s')
279 if len(args) == 2:
280 build_dir, target = args
281 search_for = None
282 elif len(args) == 3:
283 build_dir, target, search_for = args
284 else:
285 parser.error('Please provide a directory, a target, optionally a link')
286
287 start = time.time()
288 build_dir = os.path.abspath(build_dir)
289 if not os.path.isdir(build_dir):
290 parser.error('build dir must exist')
291 build_steps = load_ninja(build_dir)
292
293 if search_for:
294 found_one = False
295 # Find how to get to link from target.
296 for rules in find_link(build_dir, target, build_steps, search_for):
297 found_one = True
298 print('%s -> %s' % (target, ' -> '.join(rules)))
299 if not found_one:
300 print('Find to find a link between %s and %s' % (target, search_for))
301 end = time.time()
302 logging.info('Processing took %.3fs', end-start)
303 else:
304 binary_deps = post_process_deps(
305 build_dir, recurse(build_dir, target, build_steps))
306 end = time.time()
307 logging.info('Processing took %.3fs', end-start)
308 print('Binary dependencies:%s' % ''.join('\n ' + i for i in binary_deps))
309 return 0
310
311
312 if __name__ == '__main__':
313 sys.exit(main())
OLDNEW
« no previous file with comments | « tools/isolate_driver.py ('k') | no next file » | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698