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

Side by Side Diff: pylib/gyp/generator/ninja.py

Issue 7696003: ninja: land generator for ninja files (Closed) Base URL: https://gyp.googlecode.com/svn/trunk
Patch Set: reup Created 9 years, 4 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 | pylib/gyp/ninja_syntax.py » ('j') | pylib/gyp/ninja_syntax.py » ('J')
Toggle Intra-line Diffs ('i') | Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
OLDNEW
(Empty)
1 #!/usr/bin/python
2
3 # Copyright (c) 2010 Google Inc. All rights reserved.
Nico 2011/08/19 19:07:48 2011
Evan Martin 2011/08/19 20:26:43 Heh, shows how long I've been sitting on this patc
4 # Use of this source code is governed by a BSD-style license that can be
5 # found in the LICENSE file.
6
7 import gyp
8 import gyp.common
9 import gyp.system_test
10 import os.path
11 import pprint
12 import subprocess
13 import sys
14
15 import gyp.ninja_syntax as ninja_syntax
16
17 generator_default_variables = {
18 'OS': 'linux',
19
20 'EXECUTABLE_PREFIX': '',
21 'EXECUTABLE_SUFFIX': '',
22 'STATIC_LIB_PREFIX': '',
23 'STATIC_LIB_SUFFIX': '.a',
24 'SHARED_LIB_PREFIX': 'lib',
25 'SHARED_LIB_SUFFIX': '.so',
26 # TODO: intermediate dir should *not* be shared between different targets.
27 # Unfortunately, whatever we provide here gets written into many different
28 # places within the gyp spec so it's difficult to make it target-specific.
29 # Apparently we've made it this far with one global path for the make build
30 # we're safe for now.
31 'INTERMEDIATE_DIR': '$b/geni',
32 'SHARED_INTERMEDIATE_DIR': '$b/gen',
33 'PRODUCT_DIR': '$b',
34 'SHARED_LIB_DIR': '$b/lib',
35 'LIB_DIR': '$b',
36
37 # Special variables that may be used by gyp 'rule' targets.
38 # We generate definitions for these variables on the fly when processing a
39 # rule.
40 'RULE_INPUT_ROOT': '$root',
41 'RULE_INPUT_PATH': '$source',
42 'RULE_INPUT_EXT': '$ext',
43 'RULE_INPUT_NAME': '$name',
44 }
45
46 NINJA_BASE = """\
47 builddir = %(builddir)s
48 # Short alias for builddir.
49 b = %(builddir)s
50
51 cc = %(cc)s
52 cxx = %(cxx)s
53
54 rule cc
55 depfile = $out.d
56 description = CC $out
57 command = $cc -MMD -MF $out.d $defines $includes $cflags $cflags_cc $
Nico 2011/08/19 19:07:48 I think the inconsistency of calling the flags cfl
58 -c $in -o $out
59
60 rule cxx
61 depfile = $out.d
62 description = CXX $out
63 command = $cxx -MMD -MF $out.d $defines $includes $cflags $cflags_cxx $
64 -c $in -o $out
65
66 rule alink
67 description = AR $out
68 command = rm -f $out && ar rcsT $out $in
69
70 rule solink
71 description = SOLINK $out
72 command = g++ -Wl,--threads -Wl,--thread-count=4 $
73 -shared $ldflags -o $out -Wl,-soname=$soname $
74 -Wl,--whole-archive $in -Wl,--no-whole-archive $libs
75
76 rule link
77 description = LINK $out
78 command = g++ -Wl,--threads -Wl,--thread-count=4 $
79 $ldflags -o $out -Wl,-rpath=\$$ORIGIN/lib $
80 -Wl,--start-group $in -Wl,--end-group $libs
81
82 rule stamp
83 description = STAMP $out
84 command = touch $out
85
86 rule copy
87 description = COPY $in $out
88 command = ln -f $in $out 2>/dev/null || cp -af $in $out
89
90 """
91
92
93 def StripPrefix(arg, prefix):
94 if arg.startswith(prefix):
95 return arg[len(prefix):]
96 return arg
97
98
99 def QuoteShellArgument(arg):
100 return "'" + arg.replace("'", "'" + '"\'"' + "'") + "'"
101
102
103 def MaybeQuoteShellArgument(arg):
104 if '"' in arg or ' ' in arg:
105 return QuoteShellArgument(arg)
106 return arg
107
108
109 # A small discourse on paths as used within the Ninja build:
110 #
111 # Paths within a given .gyp file are always relative to the directory
112 # containing the .gyp file. Call these "gyp paths". This includes
113 # sources as well as the starting directory a given gyp rule/action
114 # expects to be run from. We call this directory "base_dir" within
115 # the per-.gyp-file NinjaWriter code.
116 #
117 # All paths as written into the .ninja files are relative to root of
118 # the tree. Call these paths "ninja paths". We set up the ninja
119 # variable "$b" to be the path to the root of the build output,
120 # e.g. out/Debug/. All files we produce (both at gyp and at build
121 # time) appear in that output directory.
122 #
123 # We translate between these two notions of paths with two helper
124 # functions:
125 #
126 # - ExpandGypPath translates a gyp path (i.e. relative to the .gyp file)
127 # into the equivalent ninja path.
Nico 2011/08/19 19:07:48 Why not NinjaFromGypPath() and NinjaOutputFromGypP
Evan Martin 2011/08/19 20:26:43 I renamed them GypPathToNinja and GypPathToUniqueO
128 #
129 # - UniqueOutputPath translates a gyp path into a ninja path to write
130 # an output file; the result can be namespaced such that is unique
131 # to the input file name as well as the output target name.
132
133 class NinjaWriter:
134 def __init__(self, target_outputs, base_dir, output_file):
135 self.target_outputs = target_outputs
136 # The root-relative path to the source .gyp file; by gyp
137 # semantics, all input paths are relative to this.
138 self.base_dir = base_dir
139 self.ninja = ninja_syntax.Writer(output_file)
140
141 def ExpandGypPath(self, path):
142 """Translate a gyp path to a ninja path.
143
144 See the above discourse on path conversions."""
Nico 2011/08/19 19:07:48 nit: It's a bit weird to have a docstring refer to
145 if path.startswith('$'):
146 # If the path contains a reference to a ninja variable, we know
147 # it's already relative to the source root.
148 return path
149 return os.path.normpath(os.path.join(self.base_dir, path))
150
151 def UniqueOutputPath(self, path, qualified=False):
152 """Translate a gyp path to a ninja path for writing output.
153
154 If qualified is True, qualify the resulting filename with the name
155 of the target. This is necessary when e.g. compiling the same
156 path twice for two separate output targets.
157
158 See the above discourse on path conversions."""
159
160 # It may seem strange to discard components of the path, but we are just
161 # attempting to produce a known-unique output filename; we don't want to
162 # reuse any global directory.
Nico 2011/08/19 19:07:48 assert not generator_default_variables['INTERMEDIA
163 path = StripPrefix(path,
164 generator_default_variables['SHARED_INTERMEDIATE_DIR'])
165 path = StripPrefix(path,
166 generator_default_variables['INTERMEDIATE_DIR'])
167 path = StripPrefix(path, '/')
168 assert not path.startswith('$')
169 path_dir, path_filename = os.path.split(path)
Nico 2011/08/19 19:07:48 Usually, filename = dirname + basename, so I'd cal
170 if qualified:
171 path_filename = self.name + '.' + path_filename
172 return os.path.normpath(os.path.join('$b/obj', self.base_dir, path_dir,
173 path_filename))
174
175 def StampPath(self, name):
Nico 2011/08/19 19:07:48 (If you rename the two functions above, this would
Evan Martin 2011/08/19 20:26:43 I renamed the other functions, but since stamps ne
176 """Return a path for a stamp file with a particular name.
177
178 Stamp files are used to collapse a dependency on a bunch of files
179 into a single file."""
180 return self.UniqueOutputPath(name + '.stamp', qualified=True)
181
182 def WriteSpec(self, spec, config):
183 """The main entry point for NinjaWriter: write the build rules for a spec.
184
185 Returns the path to the build output, or None."""
186
187 if spec['type'] == 'settings':
188 # TODO: 'settings' is not actually part of gyp; it was
189 # accidentally introduced somehow into just the Linux build files.
190 return None
191
192 self.name = spec['target_name']
193
194 # Compute predepends for all rules.
195 # prebuild is the dependencies this target depends on before
196 # running any of its internal steps.
197 prebuild = []
198 if 'dependencies' in spec:
199 deps = [self.target_outputs.get(dep, (None, False))
200 for dep in spec['dependencies']]
201 prebuild_deps = [x for x, _ in deps if x]
Nico 2011/08/19 19:07:48 Is this the same as `filter(None, deps.keys())`? I
Evan Martin 2011/08/19 20:26:43 Unfortunately, deps is a list of tuples. I rewrot
202 if prebuild_deps:
203 prebuild = [self.StampPath('predepends')]
204 self.ninja.build(prebuild, 'stamp', prebuild_deps)
205 self.ninja.newline()
206
207 # Write out actions, rules, and copies. These must happen before we
208 # compile any sources, so compute a list of predependencies for sources
209 # while we do it.
210 extra_sources = []
211 sources_predepends = self.WriteActionsRulesCopies(spec, extra_sources,
212 prebuild)
213
214 # Write out the compilation steps, if any.
215 link_deps = []
216 sources = spec.get('sources', []) + extra_sources
217 if sources:
218 link_deps = self.WriteSources(config, sources,
219 sources_predepends or prebuild)
220 # Some actions/rules output 'sources' that are already object files.
221 link_deps += [f for f in sources if f.endswith('.o')]
222
223 # The final output of our target depends on the last output of the
224 # above steps.
225 final_deps = link_deps or sources_predepends or prebuild
Nico 2011/08/19 19:07:48 `sources_predepends or prebuild` is repeated here
Evan Martin 2011/08/19 20:26:43 I think I had originally used a variable here and
226 if final_deps:
227 return self.WriteTarget(spec, config, final_deps)
228
229 def WriteActionsRulesCopies(self, spec, extra_sources, prebuild):
230 """Write out the Actions, Rules, and Copies steps. Return any outputs
231 of these steps (or a stamp file if there are lots of outputs)."""
232 outputs = []
233
234 if 'actions' in spec:
235 outputs += self.WriteActions(spec['actions'], extra_sources, prebuild)
236 if 'rules' in spec:
237 outputs += self.WriteRules(spec['rules'], extra_sources, prebuild)
238 if 'copies' in spec:
239 outputs += self.WriteCopies(spec['copies'], prebuild)
240
241 # To simplify downstream build edges, ensure we generate a single
242 # stamp file that represents the results of all of the above.
243 if len(outputs) > 1:
244 stamp = self.StampPath('actions_rules_copies')
245 outputs = self.ninja.build(stamp, 'stamp', outputs)
246
247 return outputs
248
249 def WriteActions(self, actions, extra_sources, prebuild):
250 all_outputs = []
251 for action in actions:
252 # First write out a rule for the action.
253 name = action['action_name']
254 if 'message' in action:
255 description = 'ACTION ' + action['message']
256 else:
257 description = 'ACTION %s: %s' % (self.name, action['action_name'])
258 rule_name = self.WriteNewNinjaRule(name, action['action'], description)
259
260 inputs = [self.ExpandGypPath(i) for i in action['inputs']]
261 if int(action.get('process_outputs_as_sources', False)):
262 extra_sources += action['outputs']
263 outputs = [self.ExpandGypPath(o) for o in action['outputs']]
264
265 # Then write out an edge using the rule.
266 self.ninja.build(outputs, rule_name, inputs,
267 order_only=prebuild)
268 all_outputs += outputs
269
270 self.ninja.newline()
271
272 return all_outputs
273
274 def WriteRules(self, rules, extra_sources, prebuild):
275 all_outputs = []
276 for rule in rules:
277 # First write out a rule for the rule action.
278 name = rule['rule_name']
279 args = rule['action']
280 if 'message' in rule:
281 description = 'RULE ' + rule['message']
282 else:
283 description = 'RULE %s: %s $source' % (self.name, name)
284 rule_name = self.WriteNewNinjaRule(name, args, description)
285
286 # TODO: if the command references the outputs directly, we should
287 # simplify it to just use $out.
288
289 # Rules can potentially make use of some special variables which
290 # must vary per source file.
291 # Compute the list of variables we'll need to provide.
292 special_locals = ('source', 'root', 'ext', 'name')
293 needed_variables = set(['source'])
294 for argument in args:
295 for var in special_locals:
296 if '$' + var in argument:
297 needed_variables.add(var)
298
299 # For each source file, write an edge that generates all the outputs.
300 for source in rule.get('rule_sources', []):
301 basename = os.path.basename(source)
302 root, ext = os.path.splitext(basename)
303 source = self.ExpandGypPath(source)
304
305 outputs = []
306 for output in rule['outputs']:
307 outputs.append(output.replace('$root', root))
308
309 extra_bindings = []
310 for var in needed_variables:
311 if var == 'root':
312 extra_bindings.append(('root', root))
313 elif var == 'source':
314 extra_bindings.append(('source', source))
315 elif var == 'ext':
316 extra_bindings.append(('ext', ext))
317 elif var == 'name':
318 extra_bindings.append(('name', basename))
319 else:
320 assert var == None, repr(var)
321
322 inputs = map(self.ExpandGypPath, rule.get('inputs', []))
323 self.ninja.build(outputs, rule_name, source,
324 implicit=inputs,
325 order_only=prebuild,
326 variables=extra_bindings)
327
328 if int(rule.get('process_outputs_as_sources', False)):
329 extra_sources += outputs
330
331 all_outputs.extend(outputs)
332
333 return all_outputs
334
335 def WriteCopies(self, copies, prebuild):
336 outputs = []
337 for copy in copies:
338 for path in copy['files']:
339 # Normalize the path so trailing slashes don't confuse us.
340 path = os.path.normpath(path)
341 filename = os.path.split(path)[1]
Nico 2011/08/19 19:07:48 (if you say 'basename' above, do so here too)
342 src = self.ExpandGypPath(path)
343 dst = self.ExpandGypPath(os.path.join(copy['destination'], filename))
344 self.ninja.build(dst, 'copy', src,
345 order_only=prebuild)
346 outputs.append(dst)
347
348 return outputs
349
350 def WriteSources(self, config, sources, predepends):
351 """Write build rules to compile all of |sources|."""
352 self.WriteVariableList('defines',
353 ['-D' + MaybeQuoteShellArgument(ninja_syntax.escape(d))
354 for d in config.get('defines', [])])
355 self.WriteVariableList('includes',
356 ['-I' + self.ExpandGypPath(i)
357 for i in config.get('include_dirs', [])])
358 self.WriteVariableList('cflags', config.get('cflags'))
359 self.WriteVariableList('cflags_cc', config.get('cflags_c'))
360 self.WriteVariableList('cflags_cxx', config.get('cflags_cc'))
361 self.ninja.newline()
362 outputs = []
363 for source in sources:
364 filename, ext = os.path.splitext(source)
365 ext = ext[1:]
366 if ext in ('cc', 'cpp', 'cxx'):
367 command = 'cxx'
368 elif ext in ('c', 's', 'S'):
369 command = 'cc'
370 else:
371 # if ext in ('h', 'hxx'):
Nico 2011/08/19 19:07:48 is this a todo? something else?
372 # elif ext in ('re', 'gperf', 'grd', ):
373 continue
374 input = self.ExpandGypPath(source)
375 output = self.UniqueOutputPath(filename + '.o', qualified=True)
376 self.ninja.build(output, command, input,
377 order_only=predepends)
378 outputs.append(output)
379 self.ninja.newline()
380 return outputs
381
382 def WriteTarget(self, spec, config, final_deps):
383 output = self.ComputeOutput(spec)
384
385 output_uses_linker = spec['type'] in ('executable', 'loadable_module',
386 'shared_library')
387
388 implicit_deps = set()
389 if 'dependencies' in spec:
390 # Two kinds of dependencies:
391 # - Linkable dependencies (like a .a or a .so): add them to the link line.
392 # - Non-linkable dependencies (like a rule that generates a
393 # file and writes a stamp file): add them to implicit_deps
Nico 2011/08/19 19:07:48 nit break: before the "and" instead of before the
394 if output_uses_linker:
395 extra_deps = set()
396 for dep in spec['dependencies']:
397 input, linkable = self.target_outputs.get(dep, (None, False))
398 if not input:
399 continue
400 if linkable:
401 extra_deps.add(input)
402 else:
403 # XXX Chrome-specific HACK. Chrome runs this lastchange rule on
404 # every build, but we don't want to rebuild when it runs.
405 if 'lastchange.stamp' not in input:
406 implicit_deps.add(input)
407 final_deps.extend(list(extra_deps))
408 command_map = {
409 'executable': 'link',
410 'static_library': 'alink',
411 'loadable_module': 'solink',
412 'shared_library': 'solink',
413 'none': 'stamp',
414 }
415 command = command_map[spec['type']]
416
417 if output_uses_linker:
418 self.WriteVariableList('ldflags',
419 gyp.common.uniquer(config.get('ldflags', [])))
420 self.WriteVariableList('libs',
421 gyp.common.uniquer(spec.get('libraries', [])))
422
423 extra_bindings = []
424 if command == 'solink':
425 extra_bindings.append(('soname', os.path.split(output)[1]))
426
427 self.ninja.build(output, command, final_deps,
428 implicit=list(implicit_deps),
429 variables=extra_bindings)
430
431 # Write a short name to build this target. This benefits both the
432 # "build chrome" case as well as the gyp tests, which expect to be
433 # able to run actions and build libraries by their short name.
434 self.ninja.build(self.name, 'phony', output)
435
436 return output
437
438 def ComputeOutputFileName(self, spec):
439 # Compute filename prefix: the product prefix, or a default for
440 # the product type.
441 DEFAULT_PREFIX = {
442 'loadable_module': 'lib',
443 'shared_library': 'lib',
444 }
445 prefix = spec.get('product_prefix', DEFAULT_PREFIX.get(spec['type'], ''))
446
447 # Compute filename extension: the product extension, or a default
448 # for the product type.
449 DEFAULT_EXTENSION = {
450 'static_library': 'a',
451 'loadable_module': 'so',
452 'shared_library': 'so',
453 }
454 extension = spec.get('product_extension',
455 DEFAULT_EXTENSION.get(spec['type'], ''))
456 if extension:
457 extension = '.' + extension
458
459 if 'product_name' in spec:
460 # If we were given an explicit name, use that.
461 target = spec['product_name']
462 else:
463 # Otherwise, derive a name from the target name.
464 target = spec['target_name']
465 if prefix == 'lib':
466 # Snip out an extra 'lib' from libs if appropriate.
467 target = StripPrefix(target, 'lib')
468
469 if spec['type'] in ('static_library', 'loadable_module', 'shared_library',
470 'executable'):
471 return '%s%s%s' % (prefix, target, extension)
472 elif spec['type'] == 'none':
473 return '%s.stamp' % target
474 elif spec['type'] == 'settings':
475 return None
476 else:
477 raise 'Unhandled output type', spec['type']
478
479 def ComputeOutput(self, spec):
480 filename = self.ComputeOutputFileName(spec)
481
482 if 'product_dir' in spec:
483 path = os.path.join(spec['product_dir'], filename)
484 print 'pdir', path
485 return path
486
487 # Executables and loadable modules go into the output root,
488 # libraries go into shared library dir, and everything else
489 # goes into the normal place.
490 if spec['type'] in ('executable', 'loadable_module'):
491 return os.path.join('$b/', filename)
492 elif spec['type'] == 'shared_library':
493 return os.path.join('$b/lib', filename)
494 else:
495 return self.UniqueOutputPath(filename)
496
497 def WriteVariableList(self, var, values):
498 if values is None:
499 values = []
500 self.ninja.variable(var, ' '.join(values))
501
502 def WriteNewNinjaRule(self, name, args, description):
503 """Write out a new ninja "rule" statement for a given command.
504
505 Returns the name of the new rule."""
506
507 # TODO: we shouldn't need to qualify names; we do it because
508 # currently the ninja rule namespace is global, but it really
509 # should be scoped to the subninja.
510 rule_name = ('%s.%s' % (self.name, name)).replace(' ', '_')
511
512 cd = ''
513 args = args[:]
514 if self.base_dir:
515 # gyp dictates that commands are run from the base directory.
516 # cd into the directory before running, and adjust all paths in
517 # the arguments point to the proper locations.
518 cd = 'cd %s; ' % self.base_dir
519 cdup = '../' * len(self.base_dir.split('/'))
520 for i, arg in enumerate(args):
521 arg = arg.replace('$b', cdup + '$b')
522 arg = arg.replace('$source', cdup + '$source')
523 args[i] = arg
524
525 command = cd + gyp.common.EncodePOSIXShellList(args)
526 self.ninja.rule(rule_name, command, description)
527 self.ninja.newline()
528
529 return rule_name
530
531
532 def CalculateVariables(default_variables, params):
533 """Calculate additional variables for use in the build (called by gyp)."""
534 cc_target = os.environ.get('CC.target', os.environ.get('CC', 'cc'))
535 default_variables['LINKER_SUPPORTS_ICF'] = \
536 gyp.system_test.TestLinkerSupportsICF(cc_command=cc_target)
537
538
539 def OpenOutput(path):
540 """Open |path| for writing, creating directories if necessary."""
541 try:
542 os.makedirs(os.path.dirname(path))
543 except OSError:
544 pass
545 return open(path, 'w')
546
547
548 def GenerateOutput(target_list, target_dicts, data, params):
549 options = params['options']
550 generator_flags = params.get('generator_flags', {})
551
552 if options.generator_output:
553 raise NotImplementedError, "--generator_output not implemented for ninja"
554
555 config_name = generator_flags.get('config', None)
556 if config_name is None:
557 # Guess which config we want to use: pick the first one from the
558 # first target.
559 config_name = target_dicts[target_list[0]]['default_configuration']
560
561 # builddir: relative path from source root to our output files.
562 # e.g. "out/Debug"
563 builddir = os.path.join(generator_flags.get('output_dir', 'out'), config_name)
564
565 master_ninja = OpenOutput(os.path.join(options.toplevel_dir, builddir,
566 'build.ninja'))
567 master_ninja.write(NINJA_BASE % {
568 'builddir': builddir,
569 'cc': os.environ.get('CC', 'gcc'),
570 'cxx': os.environ.get('CXX', 'g++'),
571 })
572
573 all_targets = set()
574 for build_file in params['build_files']:
575 for target in gyp.common.AllTargets(target_list, target_dicts, build_file):
576 all_targets.add(target)
577 all_outputs = set()
578
579 subninjas = set()
580 target_outputs = {}
581 for qualified_target in target_list:
582 # qualified_target is like: third_party/icu/icu.gyp:icui18n#target
583 build_file, target, _ = gyp.common.ParseQualifiedTarget(qualified_target)
584
585 # TODO: what is options.depth and how is it different than
586 # options.toplevel_dir?
587 build_file = gyp.common.RelativePath(build_file, options.depth)
588
589 base_path = os.path.dirname(build_file)
590 output_file = os.path.join(builddir, 'obj', base_path, target + '.ninja')
591 spec = target_dicts[qualified_target]
592 config = spec['configurations'][config_name]
593
594 writer = NinjaWriter(target_outputs, base_path,
595 OpenOutput(os.path.join(options.toplevel_dir,
596 output_file)))
597 subninjas.add(output_file)
598
599 output = writer.WriteSpec(spec, config)
600 if output:
601 linkable = spec['type'] in ('static_library', 'shared_library')
602 target_outputs[qualified_target] = (output, linkable)
603
604 if qualified_target in all_targets:
605 all_outputs.add(output)
606
607 for ninja in subninjas:
608 print >>master_ninja, 'subninja', ninja
609
610 if all_outputs:
611 print >>master_ninja, 'build all: phony ||' + ' '.join(all_outputs)
612
613 master_ninja.close()
OLDNEW
« no previous file with comments | « no previous file | pylib/gyp/ninja_syntax.py » ('j') | pylib/gyp/ninja_syntax.py » ('J')

Powered by Google App Engine
This is Rietveld 408576698