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

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: fixes 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 | « gyptest.py ('k') | pylib/gyp/ninja_syntax.py » ('j') | 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/python
2
3 # Copyright (c) 2011 Google Inc. All rights reserved.
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_c $
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_cc $
64 -c $in -o $out
65
66 rule alink
67 description = AR $out
68 command = rm -f $out && ar rcsT $out $in
Nico 2011/08/19 22:45:10 Should you `assert system_test.ArSupportsThinArchi
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 the root
118 # of 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 # - GypPathToNinja translates a gyp path (i.e. relative to the .gyp file)
127 # into the equivalent ninja path.
128 #
129 # - GypPathToUniqueOutput 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 GypPathToNinja(self, path):
142 """Translate a gyp path to a ninja path.
143
144 See the above discourse on path conversions."""
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 GypPathToUniqueOutput(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.
163 assert not generator_default_variables['SHARED_INTERMEDIATE_DIR'].startswith (
Nico 2011/08/19 22:45:10 80 cols
164 generator_default_variables['INTERMEDIATE_DIR'])
165 path = StripPrefix(path,
166 generator_default_variables['INTERMEDIATE_DIR'])
167 path = StripPrefix(path,
168 generator_default_variables['SHARED_INTERMEDIATE_DIR'])
169 path = StripPrefix(path, '/')
170 assert not path.startswith('$')
171
172 # Translate the path following this scheme:
173 # Input: foo/bar.gyp, target targ, references baz/out.o
174 # Output: $b/obj/foo/baz/targ.out.o (if qualified)
175 # $b/obj/foo/baz/out.o (otherwise)
176 #
177 # Why this scheme and not some other one?
178 # 1) for a given input, you can compute all derived outputs by matching
179 # its path, even if the input is brought via a gyp file with '..'.
180 # 2) simple files like libraries and stamps have a simple filename.
181 path_dir, path_basename = os.path.split(path)
182 if qualified:
183 path_basename = self.name + '.' + path_basename
184 return os.path.normpath(os.path.join('$b/obj', self.base_dir, path_dir,
185 path_basename))
186
187 def StampPath(self, name):
188 """Return a path for a stamp file with a particular name.
189
190 Stamp files are used to collapse a dependency on a bunch of files
191 into a single file."""
192 return self.GypPathToUniqueOutput(name + '.stamp', qualified=True)
193
194 def WriteSpec(self, spec, config):
195 """The main entry point for NinjaWriter: write the build rules for a spec.
196
197 Returns the path to the build output, or None."""
198
199 if spec['type'] == 'settings':
200 # TODO: 'settings' is not actually part of gyp; it was
201 # accidentally introduced somehow into just the Linux build files.
202 return None
203
204 self.name = spec['target_name']
205
206 # Compute predepends for all rules.
207 # prebuild is the dependencies this target depends on before
208 # running any of its internal steps.
209 prebuild = []
210 if 'dependencies' in spec:
211 prebuild_deps = []
212 for dep in spec['dependencies']:
213 if dep in self.target_outputs:
214 prebuild_deps.append(self.target_outputs[dep][0])
215 if prebuild_deps:
216 stamp = self.StampPath('predepends')
217 prebuild = self.ninja.build(stamp, 'stamp', prebuild_deps)
218 self.ninja.newline()
219
220 # Write out actions, rules, and copies. These must happen before we
221 # compile any sources, so compute a list of predependencies for sources
222 # while we do it.
223 extra_sources = []
224 sources_predepends = self.WriteActionsRulesCopies(spec, extra_sources,
225 prebuild)
226
227 # Write out the compilation steps, if any.
228 link_deps = []
229 sources = spec.get('sources', []) + extra_sources
230 if sources:
231 link_deps = self.WriteSources(config, sources,
232 sources_predepends or prebuild)
233 # Some actions/rules output 'sources' that are already object files.
234 link_deps += [f for f in sources if f.endswith('.o')]
235
236 # The final output of our target depends on the last output of the
237 # above steps.
238 final_deps = link_deps or sources_predepends or prebuild
239 if final_deps:
240 return self.WriteTarget(spec, config, final_deps)
241
242 def WriteActionsRulesCopies(self, spec, extra_sources, prebuild):
243 """Write out the Actions, Rules, and Copies steps. Return any outputs
244 of these steps (or a stamp file if there are lots of outputs)."""
245 outputs = []
246
247 if 'actions' in spec:
248 outputs += self.WriteActions(spec['actions'], extra_sources, prebuild)
249 if 'rules' in spec:
250 outputs += self.WriteRules(spec['rules'], extra_sources, prebuild)
251 if 'copies' in spec:
252 outputs += self.WriteCopies(spec['copies'], prebuild)
253
254 # To simplify downstream build edges, ensure we generate a single
255 # stamp file that represents the results of all of the above.
256 if len(outputs) > 1:
257 stamp = self.StampPath('actions_rules_copies')
258 outputs = self.ninja.build(stamp, 'stamp', outputs)
259
260 return outputs
261
262 def WriteActions(self, actions, extra_sources, prebuild):
263 all_outputs = []
264 for action in actions:
265 # First write out a rule for the action.
266 name = action['action_name']
267 if 'message' in action:
268 description = 'ACTION ' + action['message']
269 else:
270 description = 'ACTION %s: %s' % (self.name, action['action_name'])
271 rule_name = self.WriteNewNinjaRule(name, action['action'], description)
272
273 inputs = [self.GypPathToNinja(i) for i in action['inputs']]
274 if int(action.get('process_outputs_as_sources', False)):
275 extra_sources += action['outputs']
276 outputs = [self.GypPathToNinja(o) for o in action['outputs']]
277
278 # Then write out an edge using the rule.
279 self.ninja.build(outputs, rule_name, inputs,
280 order_only=prebuild)
281 all_outputs += outputs
282
283 self.ninja.newline()
284
285 return all_outputs
286
287 def WriteRules(self, rules, extra_sources, prebuild):
288 all_outputs = []
289 for rule in rules:
290 # First write out a rule for the rule action.
291 name = rule['rule_name']
292 args = rule['action']
293 if 'message' in rule:
294 description = 'RULE ' + rule['message']
295 else:
296 description = 'RULE %s: %s $source' % (self.name, name)
297 rule_name = self.WriteNewNinjaRule(name, args, description)
298
299 # TODO: if the command references the outputs directly, we should
300 # simplify it to just use $out.
301
302 # Rules can potentially make use of some special variables which
303 # must vary per source file.
304 # Compute the list of variables we'll need to provide.
305 special_locals = ('source', 'root', 'ext', 'name')
306 needed_variables = set(['source'])
307 for argument in args:
308 for var in special_locals:
309 if '$' + var in argument:
310 needed_variables.add(var)
311
312 # For each source file, write an edge that generates all the outputs.
313 for source in rule.get('rule_sources', []):
314 basename = os.path.basename(source)
315 root, ext = os.path.splitext(basename)
316 source = self.GypPathToNinja(source)
317
318 outputs = []
319 for output in rule['outputs']:
320 outputs.append(output.replace('$root', root))
321
322 extra_bindings = []
323 for var in needed_variables:
324 if var == 'root':
325 extra_bindings.append(('root', root))
326 elif var == 'source':
327 extra_bindings.append(('source', source))
328 elif var == 'ext':
329 extra_bindings.append(('ext', ext))
330 elif var == 'name':
331 extra_bindings.append(('name', basename))
332 else:
333 assert var == None, repr(var)
334
335 inputs = map(self.GypPathToNinja, rule.get('inputs', []))
336 self.ninja.build(outputs, rule_name, source,
337 implicit=inputs,
338 order_only=prebuild,
339 variables=extra_bindings)
340
341 if int(rule.get('process_outputs_as_sources', False)):
342 extra_sources += outputs
343
344 all_outputs.extend(outputs)
345
346 return all_outputs
347
348 def WriteCopies(self, copies, prebuild):
349 outputs = []
350 for copy in copies:
351 for path in copy['files']:
352 # Normalize the path so trailing slashes don't confuse us.
353 path = os.path.normpath(path)
354 basename = os.path.split(path)[1]
355 src = self.GypPathToNinja(path)
356 dst = self.GypPathToNinja(os.path.join(copy['destination'], basename))
357 outputs += self.ninja.build(dst, 'copy', src,
358 order_only=prebuild)
359
360 return outputs
361
362 def WriteSources(self, config, sources, predepends):
363 """Write build rules to compile all of |sources|."""
364 self.WriteVariableList('defines',
365 ['-D' + MaybeQuoteShellArgument(ninja_syntax.escape(d))
366 for d in config.get('defines', [])])
367 self.WriteVariableList('includes',
368 ['-I' + self.GypPathToNinja(i)
369 for i in config.get('include_dirs', [])])
370 self.WriteVariableList('cflags', config.get('cflags'))
371 self.WriteVariableList('cflags_c', config.get('cflags_c'))
372 self.WriteVariableList('cflags_cc', config.get('cflags_cc'))
373 self.ninja.newline()
374 outputs = []
375 for source in sources:
376 filename, ext = os.path.splitext(source)
377 ext = ext[1:]
378 if ext in ('cc', 'cpp', 'cxx'):
379 command = 'cxx'
380 elif ext in ('c', 's', 'S'):
381 command = 'cc'
382 else:
383 # TODO: should we assert here on unexpected extensions?
384 continue
385 input = self.GypPathToNinja(source)
386 output = self.GypPathToUniqueOutput(filename + '.o', qualified=True)
387 self.ninja.build(output, command, input,
388 order_only=predepends)
389 outputs.append(output)
390 self.ninja.newline()
391 return outputs
392
393 def WriteTarget(self, spec, config, final_deps):
394 output = self.ComputeOutput(spec)
395
396 output_uses_linker = spec['type'] in ('executable', 'loadable_module',
397 'shared_library')
398
399 implicit_deps = set()
400 if 'dependencies' in spec:
401 # Two kinds of dependencies:
402 # - Linkable dependencies (like a .a or a .so): add them to the link line.
403 # - Non-linkable dependencies (like a rule that generates a file
404 # and writes a stamp file): add them to implicit_deps
405 if output_uses_linker:
406 extra_deps = set()
407 for dep in spec['dependencies']:
408 input, linkable = self.target_outputs.get(dep, (None, False))
409 if not input:
410 continue
411 if linkable:
412 extra_deps.add(input)
Nico 2011/08/19 22:45:10 dpranke will not appreciate this
413 else:
414 # XXX Chrome-specific HACK. Chrome runs this lastchange rule on
Nico 2011/08/19 22:45:10 s/XXX/TODO/
415 # every build, but we don't want to rebuild when it runs.
416 if 'lastchange.stamp' not in input:
417 implicit_deps.add(input)
418 final_deps.extend(list(extra_deps))
419 command_map = {
420 'executable': 'link',
421 'static_library': 'alink',
422 'loadable_module': 'solink',
423 'shared_library': 'solink',
424 'none': 'stamp',
425 }
426 command = command_map[spec['type']]
427
428 if output_uses_linker:
429 self.WriteVariableList('ldflags',
430 gyp.common.uniquer(config.get('ldflags', [])))
431 self.WriteVariableList('libs',
432 gyp.common.uniquer(spec.get('libraries', [])))
433
434 extra_bindings = []
435 if command == 'solink':
436 extra_bindings.append(('soname', os.path.split(output)[1]))
437
438 self.ninja.build(output, command, final_deps,
439 implicit=list(implicit_deps),
440 variables=extra_bindings)
441
442 # Write a short name to build this target. This benefits both the
443 # "build chrome" case as well as the gyp tests, which expect to be
444 # able to run actions and build libraries by their short name.
445 self.ninja.build(self.name, 'phony', output)
446
447 return output
448
449 def ComputeOutputFileName(self, spec):
450 """Compute the filename of the final output for the current target."""
451
452 # Compute filename prefix: the product prefix, or a default for
453 # the product type.
454 DEFAULT_PREFIX = {
455 'loadable_module': 'lib',
456 'shared_library': 'lib',
457 }
458 prefix = spec.get('product_prefix', DEFAULT_PREFIX.get(spec['type'], ''))
459
460 # Compute filename extension: the product extension, or a default
461 # for the product type.
462 DEFAULT_EXTENSION = {
463 'static_library': 'a',
464 'loadable_module': 'so',
465 'shared_library': 'so',
466 }
467 extension = spec.get('product_extension',
468 DEFAULT_EXTENSION.get(spec['type'], ''))
469 if extension:
470 extension = '.' + extension
471
472 if 'product_name' in spec:
473 # If we were given an explicit name, use that.
474 target = spec['product_name']
475 else:
476 # Otherwise, derive a name from the target name.
477 target = spec['target_name']
478 if prefix == 'lib':
479 # Snip out an extra 'lib' from libs if appropriate.
480 target = StripPrefix(target, 'lib')
Nico 2011/08/19 22:45:10 Remark: In this file there are several "if spec['t
481
482 if spec['type'] in ('static_library', 'loadable_module', 'shared_library',
483 'executable'):
484 return '%s%s%s' % (prefix, target, extension)
485 elif spec['type'] == 'none':
486 return '%s.stamp' % target
487 elif spec['type'] == 'settings':
488 return None
489 else:
490 raise 'Unhandled output type', spec['type']
491
492 def ComputeOutput(self, spec):
493 """Compute the path for the final output of the spec."""
494
495 filename = self.ComputeOutputFileName(spec)
496
497 if 'product_dir' in spec:
498 path = os.path.join(spec['product_dir'], filename)
499 print 'pdir', path
Nico 2011/08/19 22:45:10 Are you intentionally writing to stdout here? (pro
500 return path
501
502 # Executables and loadable modules go into the output root,
503 # libraries go into shared library dir, and everything else
504 # goes into the normal place.
505 if spec['type'] in ('executable', 'loadable_module'):
506 return os.path.join('$b', filename)
507 elif spec['type'] == 'shared_library':
508 return os.path.join('$b/lib', filename)
509 else:
510 return self.GypPathToUniqueOutput(filename)
511
512 def WriteVariableList(self, var, values):
513 if values is None:
514 values = []
515 self.ninja.variable(var, ' '.join(values))
516
517 def WriteNewNinjaRule(self, name, args, description):
518 """Write out a new ninja "rule" statement for a given command.
519
520 Returns the name of the new rule."""
521
522 # TODO: we shouldn't need to qualify names; we do it because
523 # currently the ninja rule namespace is global, but it really
524 # should be scoped to the subninja.
Nico 2011/08/19 22:45:10 I like the word "subninja"
525 rule_name = ('%s.%s' % (self.name, name)).replace(' ', '_')
526
527 cd = ''
528 args = args[:]
529 if self.base_dir:
530 # gyp dictates that commands are run from the base directory.
531 # cd into the directory before running, and adjust all paths in
532 # the arguments point to the proper locations.
533 cd = 'cd %s; ' % self.base_dir
534 cdup = '../' * len(self.base_dir.split('/'))
535 for i, arg in enumerate(args):
536 arg = arg.replace('$b', cdup + '$b')
537 arg = arg.replace('$source', cdup + '$source')
538 args[i] = arg
Nico 2011/08/19 22:45:10 This could be in a separate function maybe
539
540 command = cd + gyp.common.EncodePOSIXShellList(args)
541 self.ninja.rule(rule_name, command, description)
542 self.ninja.newline()
543
544 return rule_name
545
546
547 def CalculateVariables(default_variables, params):
548 """Calculate additional variables for use in the build (called by gyp)."""
549 cc_target = os.environ.get('CC.target', os.environ.get('CC', 'cc'))
550 default_variables['LINKER_SUPPORTS_ICF'] = \
551 gyp.system_test.TestLinkerSupportsICF(cc_command=cc_target)
552
553
554 def OpenOutput(path):
555 """Open |path| for writing, creating directories if necessary."""
556 try:
557 os.makedirs(os.path.dirname(path))
558 except OSError:
559 pass
560 return open(path, 'w')
561
562
563 def GenerateOutput(target_list, target_dicts, data, params):
564 options = params['options']
565 generator_flags = params.get('generator_flags', {})
566
567 if options.generator_output:
568 raise NotImplementedError, "--generator_output not implemented for ninja"
569
570 config_name = generator_flags.get('config', None)
571 if config_name is None:
Nico 2011/08/19 22:45:10 and len(target_list) > 0? :-P (maybe that's handle
572 # Guess which config we want to use: pick the first one from the
573 # first target.
574 config_name = target_dicts[target_list[0]]['default_configuration']
575
576 # builddir: relative path from source root to our output files.
577 # e.g. "out/Debug"
578 builddir = os.path.join(generator_flags.get('output_dir', 'out'), config_name)
579
580 master_ninja = OpenOutput(os.path.join(options.toplevel_dir, builddir,
581 'build.ninja'))
582 master_ninja.write(NINJA_BASE % {
583 'builddir': builddir,
584 'cc': os.environ.get('CC', 'gcc'),
585 'cxx': os.environ.get('CXX', 'g++'),
586 })
587
588 all_targets = set()
589 for build_file in params['build_files']:
590 for target in gyp.common.AllTargets(target_list, target_dicts, build_file):
591 all_targets.add(target)
592 all_outputs = set()
593
594 subninjas = set()
595 target_outputs = {}
596 for qualified_target in target_list:
597 # qualified_target is like: third_party/icu/icu.gyp:icui18n#target
598 build_file, target, _ = gyp.common.ParseQualifiedTarget(qualified_target)
599
600 # TODO: what is options.depth and how is it different than
601 # options.toplevel_dir?
602 build_file = gyp.common.RelativePath(build_file, options.depth)
603
604 base_path = os.path.dirname(build_file)
605 output_file = os.path.join(builddir, 'obj', base_path, target + '.ninja')
606 spec = target_dicts[qualified_target]
607 config = spec['configurations'][config_name]
608
609 writer = NinjaWriter(target_outputs, base_path,
610 OpenOutput(os.path.join(options.toplevel_dir,
611 output_file)))
612 subninjas.add(output_file)
613
614 output = writer.WriteSpec(spec, config)
615 if output:
616 linkable = spec['type'] in ('static_library', 'shared_library')
617 target_outputs[qualified_target] = (output, linkable)
618
619 if qualified_target in all_targets:
620 all_outputs.add(output)
621
622 for ninja in subninjas:
623 print >>master_ninja, 'subninja', ninja
624
625 if all_outputs:
626 print >>master_ninja, 'build all: phony ||' + ' '.join(all_outputs)
627
628 master_ninja.close()
OLDNEW
« no previous file with comments | « gyptest.py ('k') | pylib/gyp/ninja_syntax.py » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698