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

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

Powered by Google App Engine
This is Rietveld 408576698