OILS / build / ninja_lib.py View on Github | oils.pub

749 lines, 441 significant
1#!/usr/bin/env python2
2"""
3ninja_lib.py
4
5Runtime options:
6
7 CXXFLAGS Additional flags to pass to the C++ compiler
8
9Notes on ninja_syntax.py:
10
11- escape_path() seems wrong?
12 - It should really take $ to $$.
13 - It doesn't escape newlines
14
15 return word.replace('$ ', '$$ ').replace(' ', '$ ').replace(':', '$:')
16
17 Ninja shouldn't have used $ and ALSO used shell commands (sh -c)! Better
18 solutions:
19
20 - Spawn a process with environment variables.
21 - use % for substitution instead
22
23- Another problem: Ninja doesn't escape the # comment character like $#, so
24 how can you write a string with a # as the first char on a line?
25"""
26from __future__ import print_function
27
28import collections
29import os
30import sys
31
32
33def log(msg, *args):
34 if args:
35 msg = msg % args
36 print(msg, file=sys.stderr)
37
38
39# Matrix of configurations
40
41COMPILERS_VARIANTS = [
42 ('cxx', 'dbg'),
43 ('cxx', 'opt'),
44 ('cxx', 'asan'),
45 ('cxx', 'asan+gcalways'),
46 ('cxx', 'asan32+gcalways'),
47 ('cxx', 'ubsan'),
48
49 #('clang', 'asan'),
50 ('clang', 'dbg'), # compile-quickly
51 ('clang', 'opt'), # for comparisons
52 ('clang', 'ubsan'), # finds different bugs
53 ('clang', 'coverage'),
54]
55
56GC_PERF_VARIANTS = [
57 ('cxx', 'opt+bumpleak'),
58 ('cxx', 'opt+bumproot'),
59 ('cxx', 'opt+bumpsmall'),
60 #('cxx', 'asan+bumpsmall'),
61 ('cxx', 'opt+nopool'),
62
63 # TODO: should be binary with different files
64 #('cxx', 'opt+tcmalloc'),
65
66 # For tracing allocations, or debugging
67 ('cxx', 'uftrace'),
68
69 # Test performance of 32-bit build. (It uses less memory usage, but can be
70 # slower.)
71 ('cxx', 'opt32'),
72]
73
74OTHER_VARIANTS = [
75 # Affects mycpp/gc_mops.cc - we can do overflow checking
76 ('cxx', 'opt+bigint'),
77 ('cxx', 'asan+bigint'),
78]
79
80SMALL_TEST_MATRIX = [
81 ('cxx', 'asan'),
82 ('cxx', 'ubsan'),
83 ('clang', 'coverage'),
84]
85
86
87def ConfigDir(config):
88 compiler, variant, more_cxx_flags = config
89 if more_cxx_flags is None:
90 return '%s-%s' % (compiler, variant)
91 else:
92 # -D CPP_UNIT_TEST -> D_CPP_UNIT_TEST
93 flags_str = more_cxx_flags.replace('-', '').replace(' ', '_')
94 return '%s-%s-%s' % (compiler, variant, flags_str)
95
96
97def ObjPath(src_path, config):
98 rel_path, _ = os.path.splitext(src_path)
99 return '_build/obj/%s/%s.o' % (ConfigDir(config), rel_path)
100
101
102# Used namedtuple since it doesn't have any state
103CcBinary = collections.namedtuple(
104 'CcBinary',
105 'main_cc symlinks implicit deps matrix phony_prefix preprocessed')
106
107
108class Rules(object):
109 """High-level wrapper for NinjaWriter
110
111 What should it handle?
112
113 - The (compiler, variant) matrix loop
114 - Implicit deps for generated code
115 - Phony convenience targets
116
117 Maybe: exporting data to test runner
118
119 Terminology:
120
121 Ninja has
122 - rules, which are like Bazel "actions"
123 - build targets
124
125 Our library has:
126 - Build config: (compiler, variant), and more later
127
128 - Labels: identifiers starting with //, which are higher level than Ninja
129 "targets"
130 cc_library:
131 //mycpp/runtime
132
133 //mycpp/examples/expr.asdl
134 //frontend/syntax.asdl
135
136 - Deps are lists of labels, and have a transitive closure
137
138 - H Rules / High level rules? B rules / Boil?
139 cc_binary, cc_library, asdl, etc.
140 """
141
142 def __init__(self, n):
143 self.n = n # direct ninja writer
144
145 self.cc_bins = [] # list of CcBinary() objects to write
146 self.cc_libs = {} # label -> CcLibrary object
147
148 self.phony = {} # list of phony targets
149
150 def AddPhony(self, phony_to_add):
151 self.phony.update(phony_to_add)
152
153 def WritePhony(self):
154 for name in sorted(self.phony):
155 targets = self.phony[name]
156 if targets:
157 self.n.build([name], 'phony', targets)
158 self.n.newline()
159
160 def compile(self,
161 out_obj,
162 in_cc,
163 deps,
164 config,
165 implicit=None,
166 maybe_preprocess=False):
167 """ .cc -> compiler -> .o """
168
169 implicit = implicit or []
170
171 compiler, variant, more_cxx_flags = config
172 if more_cxx_flags is None:
173 flags_str = "''"
174 else:
175 assert "'" not in more_cxx_flags, more_cxx_flags # can't handle single quotes
176 flags_str = "'%s'" % more_cxx_flags
177
178 v = [('compiler', compiler), ('variant', variant),
179 ('more_cxx_flags', flags_str)]
180 if maybe_preprocess:
181 # Limit it to certain configs
182 if more_cxx_flags is None and variant in ('dbg', 'opt'):
183 pre = '_build/preprocessed/%s-%s/%s' % (compiler, variant,
184 in_cc)
185 self.n.build(pre,
186 'preprocess', [in_cc],
187 implicit=implicit,
188 variables=v)
189 else:
190 self.n.build([out_obj],
191 'compile_one', [in_cc],
192 implicit=implicit,
193 variables=v)
194
195 self.n.newline()
196
197 def link(self, out_bin, main_obj, deps, config):
198 """ list of .o -> linker -> executable, along with stripped version """
199 compiler, variant, _ = config
200
201 assert isinstance(out_bin, str), out_bin
202 assert isinstance(main_obj, str), main_obj
203
204 objects = [main_obj]
205 for label in deps:
206 try:
207 cc_lib = self.cc_libs[label]
208 except KeyError:
209 raise RuntimeError("Couldn't resolve label %r" % label)
210
211 o = cc_lib.obj_lookup[config]
212 objects.extend(o)
213
214 v = [('compiler', compiler), ('variant', variant),
215 ('more_link_flags', "''")]
216 self.n.build([out_bin], 'link', objects, variables=v)
217 self.n.newline()
218
219 # Strip any .opt binaries
220 if variant.startswith('opt') or variant.startswith('opt32'):
221 stripped = out_bin + '.stripped'
222 symbols = out_bin + '.symbols'
223 self.n.build([stripped, symbols], 'strip', [out_bin])
224 self.n.newline()
225
226 def comment(self, s):
227 self.n.comment(s)
228 self.n.newline()
229
230 def cc_library(
231 self,
232 label,
233 srcs=None,
234 implicit=None,
235 deps=None,
236 # note: headers is only used for tarball manifest, not compiler command line
237 headers=None,
238 generated_headers=None):
239
240 # srcs = [] is allowed for _gen/asdl/hnode.asdl.h
241 if srcs is None:
242 raise RuntimeError('cc_library %r requires srcs' % label)
243
244 implicit = implicit or []
245 deps = deps or []
246 headers = headers or []
247 generated_headers = generated_headers or []
248
249 if label in self.cc_libs:
250 raise RuntimeError('%s was already defined' % label)
251
252 self.cc_libs[label] = CcLibrary(label, srcs, implicit, deps, headers,
253 generated_headers)
254
255 def cc_binary(
256 self,
257 main_cc, # e.g. cpp/core_test.cc
258 symlinks=None, # make these symlinks - separate rules?
259 implicit=None, # for COMPILE action, not link action
260 deps=None, # libraries to depend on, transitive closure
261 matrix=None, # $compiler $variant +bumpleak
262 phony_prefix=None, # group
263 preprocessed=False, # generate report
264 ):
265 """
266 A cc_binary() depends on a list of cc_library() rules specified by
267 //package/label
268
269 It accepts a config matrix of (compiler, variant, +other)
270
271 The transitive closure is computed.
272
273 Then we write Ninja rules corresponding to each dependent library, with
274 respect to the config.
275 """
276 symlinks = symlinks or []
277 implicit = implicit or []
278 deps = deps or []
279 if not matrix:
280 raise RuntimeError("Config matrix required")
281
282 cc_bin = CcBinary(main_cc, symlinks, implicit, deps, matrix,
283 phony_prefix, preprocessed)
284
285 self.cc_bins.append(cc_bin)
286
287 def asdl_library(self,
288 asdl_path,
289 deps=None,
290 pretty_print_methods=True,
291 abbrev_module=None):
292
293 deps = deps or []
294
295 # SYSTEM header, _gen/asdl/hnode.asdl.h
296 deps.append('//asdl/hnode.asdl')
297 deps.append('//display/pretty.asdl')
298
299 # to create _gen/mycpp/examples/expr.asdl.h
300 prefix = '_gen/%s' % asdl_path
301
302 out_cc = prefix + '.cc'
303 out_header = prefix + '.h'
304
305 asdl_flags = []
306
307 if pretty_print_methods:
308 outputs = [out_cc, out_header]
309 else:
310 outputs = [out_header]
311 asdl_flags.append('--no-pretty-print-methods')
312
313 if abbrev_module:
314 asdl_flags.append('--abbrev-module=%s' % abbrev_module)
315
316 debug_mod = prefix + '_debug.py'
317 outputs.append(debug_mod)
318
319 # Generating syntax_asdl.h does NOT depend on hnode_asdl.h existing ...
320 self.n.build(outputs,
321 'asdl-cpp', [asdl_path],
322 implicit=['_bin/shwrap/asdl_main'],
323 variables=[
324 ('action', 'cpp'),
325 ('out_prefix', prefix),
326 ('asdl_flags', ' '.join(asdl_flags)),
327 ('debug_mod', debug_mod),
328 ])
329 self.n.newline()
330
331 # ... But COMPILING anything that #includes it does.
332 # Note: assumes there's a build rule for this "system" ASDL schema
333
334 srcs = [out_cc] if pretty_print_methods else []
335 # Define lazy CC library
336 self.cc_library(
337 '//' + asdl_path,
338 srcs=srcs,
339 deps=deps,
340 # For compile_one steps of files that #include this ASDL file
341 generated_headers=[out_header],
342 )
343
344 def py_binary(self, main_py, deps_base_dir='_build/NINJA', template='py'):
345 """Wrapper for Python script with dynamically discovered deps
346
347 Args:
348 template: py, mycpp, or pea
349
350 Example:
351 _bin/shwrap/mycpp_main wraps mycpp/mycpp_main.py
352 - using dependencies from prebuilt/ninja/mycpp.mycpp_main/deps.txt
353 - with the 'shwrap-mycpp' template defined in build/ninja-lib.sh
354 """
355 rel_path, _ = os.path.splitext(main_py)
356 # asdl/asdl_main.py -> asdl.asdl_main
357 py_module = rel_path.replace('/', '.')
358
359 deps_path = os.path.join(deps_base_dir, py_module, 'deps.txt')
360 with open(deps_path) as f:
361 deps = [line.strip() for line in f]
362
363 deps.remove(main_py) # raises ValueError if it's not there
364
365 shwrap_name = os.path.basename(rel_path)
366 self.n.build('_bin/shwrap/%s' % shwrap_name,
367 'write-shwrap', [main_py] + deps,
368 variables=[('template', template)])
369 self.n.newline()
370
371 def souffle_binary(self, souffle_cpp):
372 """
373 Compile souffle C++ into a native executable.
374 """
375 rel_path, _ = os.path.splitext(souffle_cpp)
376 basename = os.path.basename(rel_path)
377
378 souffle_obj = '_build/obj/datalog/%s.o' % basename
379 self.n.build([souffle_obj],
380 'compile_one',
381 souffle_cpp,
382 variables=[('compiler', 'cxx'), ('variant', 'opt'),
383 ('more_cxx_flags', "'-Ivendor -std=c++17'")])
384
385 souffle_bin = '_bin/datalog/%s' % basename
386 self.n.build([souffle_bin],
387 'link',
388 souffle_obj,
389 variables=[('compiler', 'cxx'), ('variant', 'opt'),
390 ('more_link_flags', "'-lstdc++fs'")])
391
392 self.n.newline()
393
394
395def _TransitiveClosure(cc_libs, name, deps, unique_out):
396 """
397 Args:
398 name: for error messages
399 """
400 for label in deps:
401 if label in unique_out:
402 continue
403 unique_out.add(label)
404
405 try:
406 cc_lib = cc_libs[label]
407 except KeyError:
408 raise RuntimeError('Undefined label %s in %r' % (label, name))
409
410 _TransitiveClosure(cc_libs, cc_lib.label, cc_lib.deps, unique_out)
411
412
413def _CalculateDeps(cc_libs, cc_rule, debug_name=''):
414 """ Compile actions for cc_library() also need implicit deps on generated headers"""
415 out_deps = set()
416 _TransitiveClosure(cc_libs, debug_name, cc_rule.deps, out_deps)
417 unique_deps = sorted(out_deps)
418
419 implicit = list(cc_rule.implicit) # copy
420 for label in unique_deps:
421 cc_lib = cc_libs[label]
422 implicit.extend(cc_lib.generated_headers)
423 return unique_deps, implicit
424
425
426class CcLibrary(object):
427 """
428 Life cycle:
429
430 1. A cc_library is first created
431 2. A cc_binary can depend on it
432 - maybe writing rules, and ensuring uniques per configuration
433 3. The link step needs the list of objects
434 4. The tarball needs the list of sources for binary
435 """
436
437 def __init__(self, label, srcs, implicit, deps, headers,
438 generated_headers):
439 self.label = label
440 self.srcs = srcs # queried by SourcesForBinary
441 self.implicit = implicit
442 self.deps = deps
443 self.headers = headers
444 # TODO: asdl() rule should add to this.
445 # Generated headers are different than regular headers. The former need an
446 # implicit dep in Ninja, while the latter can rely on the .d mechanism.
447 self.generated_headers = generated_headers
448
449 self.obj_lookup = {} # config -> list of objects
450 self.preprocessed_lookup = {} # config -> boolean
451
452 def MaybeWrite(self, ru, config, preprocessed):
453 """
454 Args:
455 preprocessed: Did the cc_binary() request preprocessing?
456 """
457 if config not in self.obj_lookup: # already written by some other cc_binary()
458 _, implicit = _CalculateDeps(ru.cc_libs,
459 self,
460 debug_name=self.label)
461
462 objects = []
463 for src in self.srcs:
464 obj = ObjPath(src, config)
465 ru.compile(obj, src, self.deps, config, implicit=implicit)
466 objects.append(obj)
467
468 self.obj_lookup[config] = objects
469
470 if preprocessed and (config not in self.preprocessed_lookup):
471 _, implicit = _CalculateDeps(ru.cc_libs,
472 self,
473 debug_name=self.label)
474
475 for src in self.srcs:
476 # no output needed
477 ru.compile('',
478 src,
479 self.deps,
480 config,
481 implicit=implicit,
482 maybe_preprocess=True)
483 self.preprocessed_lookup[config] = True
484
485
486class Deps(object):
487
488 def __init__(self, ru):
489 self.ru = ru
490 # main_cc -> list of LABELS, for tarball manifest
491 self.cc_binary_deps = {}
492
493 def SourcesForBinary(self, main_cc):
494 """
495 Used for preprocessed metrics, release tarball, _build/oils.sh, etc.
496 """
497 deps = self.cc_binary_deps[main_cc]
498 sources = [main_cc]
499 for label in deps:
500 sources.extend(self.ru.cc_libs[label].srcs)
501 return sources
502
503 def HeadersForBinary(self, main_cc):
504 deps = self.cc_binary_deps[main_cc]
505 headers = []
506 for label in deps:
507 headers.extend(self.ru.cc_libs[label].headers)
508 headers.extend(self.ru.cc_libs[label].generated_headers)
509 return headers
510
511 def WriteRules(self):
512 for cc_bin in self.ru.cc_bins:
513 self.WriteCcBinary(cc_bin)
514
515 def WriteCcBinary(self, cc_bin):
516 ru = self.ru
517 c = cc_bin
518
519 unique_deps, compile_imp = _CalculateDeps(ru.cc_libs,
520 cc_bin,
521 debug_name=cc_bin.main_cc)
522 # compile actions of binaries that have ASDL label deps need the
523 # generated header as implicit dep
524
525 # to compute tarball manifest, with SourcesForBinary()
526 self.cc_binary_deps[c.main_cc] = unique_deps
527
528 for config in c.matrix:
529 if len(config) == 2:
530 config = (config[0], config[1], None)
531
532 # Write cc_library() rules LAZILY
533 for label in unique_deps:
534 cc_lib = ru.cc_libs[label] # should exit
535 cc_lib.MaybeWrite(ru, config, c.preprocessed)
536
537 # Compile main object, maybe with IMPLICIT headers deps
538 main_obj = ObjPath(c.main_cc, config)
539 ru.compile(main_obj,
540 c.main_cc,
541 c.deps,
542 config,
543 implicit=compile_imp)
544 if c.preprocessed:
545 ru.compile('',
546 c.main_cc,
547 c.deps,
548 config,
549 implicit=compile_imp,
550 maybe_preprocess=True)
551
552 config_dir = ConfigDir(config)
553 bin_dir = '_bin/%s' % config_dir # e.g. _bin/cxx-asan
554
555 # e.g. _gen/mycpp/examples/classes.mycpp
556 rel_path, _ = os.path.splitext(c.main_cc)
557
558 # Special rule for
559 # sources = hello.mycpp.cc and hello.mycpp-main.cc
560 # binary = _bin/hello.mycpp
561 if rel_path.endswith('-main'):
562 rel_path = rel_path[:-len('-main')]
563
564 # Put binary in _bin/cxx-dbg/mycpp/examples, not _bin/cxx-dbg/_gen/mycpp/examples
565 if rel_path.startswith('_gen/'):
566 rel_path = rel_path[len('_gen/'):]
567
568 bin_to_link = '%s/%s' % (bin_dir, rel_path)
569
570 # Link with OBJECT deps
571 ru.link(bin_to_link, main_obj, unique_deps, config)
572
573 # Make symlinks
574 symlink_dir = os.path.dirname(bin_to_link)
575 bin_name = os.path.basename(bin_to_link)
576 for symlink in c.symlinks:
577 # e.g. _bin/cxx-dbg/mycpp-souffle/osh
578 symlink_path = '%s/%s' % (bin_dir, symlink)
579 symlink_dir = os.path.dirname(symlink_path)
580
581 # Compute relative path.
582 symlink_val = os.path.relpath(bin_to_link, symlink_dir)
583
584 if 0:
585 log('---')
586 log('bin %s', bin_to_link)
587 log('symlink_path %s', symlink_path)
588 log('symlink_val %s', symlink_val)
589
590 ru.n.build(
591 [symlink_path],
592 'symlink',
593 # if we build _bin/cxx-opt/osh, then the binary
594 # should be built too
595 [bin_to_link],
596 variables=[('symlink_val', symlink_val)])
597 ru.n.newline()
598
599 if 0: # disabled oils-for-unix.stripped symlink
600 variant = config[1]
601 if os.path.basename(symlink) == 'oils-for-unix' and (
602 variant.startswith('opt') or
603 variant.startswith('opt32')):
604 stripped_bin = bin_to_link + '.stripped'
605 symlink_val = os.path.relpath(stripped_bin,
606 symlink_dir)
607 ru.n.build([symlink_path + '.stripped'],
608 'symlink', [stripped_bin],
609 variables=[('symlink_val', symlink_val)])
610 ru.n.newline()
611
612 # Maybe add this cc_binary to a group
613 if c.phony_prefix:
614 key = '%s-%s' % (c.phony_prefix, config_dir)
615 if key not in ru.phony:
616 ru.phony[key] = []
617 ru.phony[key].append(bin_to_link)
618
619
620SHWRAP = {
621 'mycpp': '_bin/shwrap/mycpp_main',
622 'mycpp-souffle': '_bin/shwrap/mycpp_main_souffle',
623 'pea': '_bin/shwrap/pea_main',
624}
625
626# TODO: should have dependencies with sh_binary
627RULES_PY = 'build/ninja-rules-py.sh'
628
629# Copied from build/ninja-rules-py.sh mycpp-gen
630DEFAULT_MYPY_PATH = '$NINJA_REPO_ROOT:$NINJA_REPO_ROOT/pyext'
631
632
633def TryDynamicDeps(py_main):
634 """
635 Read dynamic deps files built in ./NINJA-config.sh
636 """
637 # bin/oils_for_unix
638 py_rel_path, _ = os.path.splitext(py_main)
639 # bin.oils_for_unix
640 py_module = py_rel_path.replace('/', '.')
641
642 deps_file = '_build/NINJA/%s/translate.txt' % py_module
643 if os.path.exists(deps_file):
644 with open(deps_file) as f:
645 return [line.strip() for line in f]
646
647 return None
648
649
650def mycpp_library(ru,
651 py_main,
652 mypy_path=DEFAULT_MYPY_PATH,
653 preamble=None,
654 translator='mycpp',
655 py_inputs=None,
656 deps=None):
657 """
658 Generate a .cc file with mycpp, and a cc_library() for it
659 """
660 # e.g. bin/oils_for_unix
661 py_rel_path, _ = os.path.splitext(py_main)
662
663 py_inputs = py_inputs or [py_main] # if not specified, it's a single file
664 deps = deps or []
665
666 headers = []
667 if preamble is None:
668 p = py_rel_path + '_preamble.h'
669 if os.path.exists(p):
670 preamble = p
671 if preamble:
672 headers.append(preamble)
673
674 n = ru.n
675
676 # Two steps
677 bundle_cc = '_gen/%s.%s.cc' % (py_rel_path, translator)
678
679 translator_shwrap = SHWRAP[translator]
680
681 n.build(
682 bundle_cc,
683 'translate-%s' % translator,
684 py_inputs, # files to translate
685 # Implicit dependency: if the translator changes, regenerate source
686 # code. But don't pass it on the command line.
687 implicit=[translator_shwrap],
688 # examples/parse uses pyext/fastfunc.pyi
689 variables=[('mypypath', mypy_path), ('preamble_path', preamble or
690 "''")])
691
692 ru.cc_library(
693 # e.g. //bin/oils_for_unix.mycpp-souffle
694 '//%s.%s' % (py_rel_path, translator),
695 srcs=[bundle_cc],
696 headers=headers,
697 deps=deps,
698 )
699
700
701def main_cc(ru, main_cc, template='unix'):
702 """
703 Generate a $name.mycpp-main.cc file
704 """
705 n = ru.n
706
707 # '_gen/bin/hello.mycpp-souffle.cc' -> hello
708 basename = os.path.basename(main_cc)
709 main_namespace = basename.split('.')[0]
710
711 n.build(
712 [main_cc],
713 'write-main',
714 [],
715 # in case any templates change
716 implicit='build/ninja-rules-py.sh',
717 variables=[
718 ('template', template),
719 # e.g. 'hello'
720 ('main_namespace', main_namespace)
721 ])
722
723
724def mycpp_binary(ru,
725 cc_lib,
726 template='unix',
727 matrix=None,
728 symlinks=None,
729 preprocessed=False,
730 phony_prefix=None):
731 """
732 Generate a $name.mycpp-main.cc file, and a cc_binary() for it
733 """
734 matrix = matrix or []
735
736 assert cc_lib.startswith('//')
737 rel_path = cc_lib[2:]
738 main_cc_path = '_gen/%s-main.cc' % rel_path
739
740 # Generate a main.cc file
741 main_cc(ru, main_cc_path, template=template)
742
743 # Then compile and link it
744 ru.cc_binary(main_cc_path,
745 deps=[cc_lib],
746 matrix=matrix,
747 symlinks=symlinks,
748 preprocessed=preprocessed,
749 phony_prefix=phony_prefix)