OILS / build / ninja_lib.py View on Github | oilshell.org

545 lines, 334 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+cheney'),
65 ('cxx', 'opt+tcmalloc'),
66
67 # For tracing allocations, or debugging
68 ('cxx', 'uftrace'),
69
70 # Test performance of 32-bit build. (It uses less memory usage, but can be
71 # slower.)
72 ('cxx', 'opt32'),
73]
74
75SMALL_TEST_MATRIX = [
76 ('cxx', 'asan'),
77 ('cxx', 'ubsan'),
78 ('clang', 'coverage'),
79]
80
81
82def ConfigDir(config):
83 compiler, variant, more_cxx_flags = config
84 if more_cxx_flags is None:
85 return '%s-%s' % (compiler, variant)
86 else:
87 # -D CPP_UNIT_TEST -> D_CPP_UNIT_TEST
88 flags_str = more_cxx_flags.replace('-', '').replace(' ', '_')
89 return '%s-%s-%s' % (compiler, variant, flags_str)
90
91
92def ObjPath(src_path, config):
93 rel_path, _ = os.path.splitext(src_path)
94 return '_build/obj/%s/%s.o' % (ConfigDir(config), rel_path)
95
96
97# Used namedtuple since it doesn't have any state
98CcBinary = collections.namedtuple(
99 'CcBinary',
100 'main_cc symlinks implicit deps matrix phony_prefix preprocessed bin_path')
101
102
103class CcLibrary(object):
104 """
105 Life cycle:
106
107 1. A cc_library is first created
108 2. A cc_binary can depend on it
109 - maybe writing rules, and ensuring uniques per configuration
110 3. The link step needs the list of objects
111 4. The tarball needs the list of sources for binary
112 """
113
114 def __init__(self, label, srcs, implicit, deps, headers,
115 generated_headers):
116 self.label = label
117 self.srcs = srcs # queried by SourcesForBinary
118 self.implicit = implicit
119 self.deps = deps
120 self.headers = headers
121 # TODO: asdl() rule should add to this.
122 # Generated headers are different than regular headers. The former need an
123 # implicit dep in Ninja, while the latter can rely on the .d mechanism.
124 self.generated_headers = generated_headers
125
126 self.obj_lookup = {} # config -> list of objects
127 self.preprocessed_lookup = {} # config -> boolean
128
129 def _CalculateImplicit(self, ru):
130 """ Compile actions for cc_library() also need implicit deps on generated headers"""
131
132 out_deps = set()
133 ru._TransitiveClosure(self.label, self.deps, out_deps)
134 unique_deps = sorted(out_deps)
135
136 implicit = list(self.implicit) # copy
137 for label in unique_deps:
138 cc_lib = ru.cc_libs[label]
139 implicit.extend(cc_lib.generated_headers)
140 return implicit
141
142 def MaybeWrite(self, ru, config, preprocessed):
143 if config not in self.obj_lookup: # already written by some other cc_binary()
144 implicit = self._CalculateImplicit(ru)
145
146 objects = []
147 for src in self.srcs:
148 obj = ObjPath(src, config)
149 ru.compile(obj, src, self.deps, config, implicit=implicit)
150 objects.append(obj)
151
152 self.obj_lookup[config] = objects
153
154 if preprocessed and config not in self.preprocessed_lookup:
155 implicit = self._CalculateImplicit(ru)
156
157 for src in self.srcs:
158 # no output needed
159 ru.compile('',
160 src,
161 self.deps,
162 config,
163 implicit=implicit,
164 maybe_preprocess=True)
165 self.preprocessed_lookup[config] = True
166
167
168class Rules(object):
169 """High-level wrapper for NinjaWriter
170
171 What should it handle?
172
173 - The (compiler, variant) matrix loop
174 - Implicit deps for generated code
175 - Phony convenience targets
176
177 Maybe: exporting data to test runner
178
179 Terminology:
180
181 Ninja has
182 - rules, which are like Bazel "actions"
183 - build targets
184
185 Our library has:
186 - Build config: (compiler, variant), and more later
187
188 - Labels: identifiers starting with //, which are higher level than Ninja
189 "targets"
190 cc_library:
191 //mycpp/runtime
192
193 //mycpp/examples/expr.asdl
194 //frontend/syntax.asdl
195
196 - Deps are lists of labels, and have a transitive closure
197
198 - H Rules / High level rules? B rules / Boil?
199 cc_binary, cc_library, asdl, etc.
200 """
201
202 def __init__(self, n):
203 self.n = n # direct ninja writer
204
205 self.cc_bins = [] # list of CcBinary() objects to write
206 self.cc_libs = {} # label -> CcLibrary object
207 self.cc_binary_deps = {} # main_cc -> list of LABELS
208 self.phony = {} # list of phony targets
209
210 def AddPhony(self, phony_to_add):
211 self.phony.update(phony_to_add)
212
213 def WritePhony(self):
214 for name in sorted(self.phony):
215 targets = self.phony[name]
216 if targets:
217 self.n.build([name], 'phony', targets)
218 self.n.newline()
219
220 def WriteRules(self):
221 for cc_bin in self.cc_bins:
222 self.WriteCcBinary(cc_bin)
223
224 def compile(self,
225 out_obj,
226 in_cc,
227 deps,
228 config,
229 implicit=None,
230 maybe_preprocess=False):
231 """ .cc -> compiler -> .o """
232
233 implicit = implicit or []
234
235 compiler, variant, more_cxx_flags = config
236 if more_cxx_flags is None:
237 flags_str = "''"
238 else:
239 assert "'" not in more_cxx_flags, more_cxx_flags # can't handle single quotes
240 flags_str = "'%s'" % more_cxx_flags
241
242 v = [('compiler', compiler), ('variant', variant),
243 ('more_cxx_flags', flags_str)]
244 if maybe_preprocess:
245 # Limit it to certain configs
246 if more_cxx_flags is None and variant in ('dbg', 'opt'):
247 pre = '_build/preprocessed/%s-%s/%s' % (compiler, variant,
248 in_cc)
249 self.n.build(pre,
250 'preprocess', [in_cc],
251 implicit=implicit,
252 variables=v)
253 else:
254 self.n.build([out_obj],
255 'compile_one', [in_cc],
256 implicit=implicit,
257 variables=v)
258
259 self.n.newline()
260
261 def link(self, out_bin, main_obj, deps, config):
262 """ list of .o -> linker -> executable, along with stripped version """
263 compiler, variant, _ = config
264
265 assert isinstance(out_bin, str), out_bin
266 assert isinstance(main_obj, str), main_obj
267
268 objects = [main_obj]
269 for label in deps:
270 key = (label, compiler, variant)
271 try:
272 cc_lib = self.cc_libs[label]
273 except KeyError:
274 raise RuntimeError("Couldn't resolve label %r" % label)
275
276 o = cc_lib.obj_lookup[config]
277 objects.extend(o)
278
279 v = [('compiler', compiler), ('variant', variant),
280 ('more_link_flags', "''")]
281 self.n.build([out_bin], 'link', objects, variables=v)
282 self.n.newline()
283
284 # Strip any .opt binaries
285 if variant.startswith('opt') or variant.startswith('opt32'):
286 stripped = out_bin + '.stripped'
287 symbols = out_bin + '.symbols'
288 self.n.build([stripped, symbols], 'strip', [out_bin])
289 self.n.newline()
290
291 def comment(self, s):
292 self.n.comment(s)
293 self.n.newline()
294
295 def cc_library(
296 self,
297 label,
298 srcs=None,
299 implicit=None,
300 deps=None,
301 # note: headers is only used for tarball manifest, not compiler command line
302 headers=None,
303 generated_headers=None):
304
305 # srcs = [] is allowed for _gen/asdl/hnode.asdl.h
306 if srcs is None:
307 raise RuntimeError('cc_library %r requires srcs' % label)
308
309 implicit = implicit or []
310 deps = deps or []
311 headers = headers or []
312 generated_headers = generated_headers or []
313
314 if label in self.cc_libs:
315 raise RuntimeError('%s was already defined' % label)
316
317 self.cc_libs[label] = CcLibrary(label, srcs, implicit, deps, headers,
318 generated_headers)
319
320 def _TransitiveClosure(self, name, deps, unique_out):
321 """
322 Args:
323 name: for error messages
324 """
325 for label in deps:
326 if label in unique_out:
327 continue
328 unique_out.add(label)
329
330 try:
331 cc_lib = self.cc_libs[label]
332 except KeyError:
333 raise RuntimeError('Undefined label %s in %s' % (label, name))
334
335 self._TransitiveClosure(cc_lib.label, cc_lib.deps, unique_out)
336
337 def cc_binary(
338 self,
339 main_cc,
340 symlinks=None,
341 implicit=None, # for COMPILE action, not link action
342 deps=None,
343 matrix=None, # $compiler $variant
344 phony_prefix=None,
345 preprocessed=False,
346 bin_path=None, # default is _bin/$compiler-$variant/rel/path
347 ):
348 symlinks = symlinks or []
349 implicit = implicit or []
350 deps = deps or []
351 if not matrix:
352 raise RuntimeError("Config matrix required")
353
354 cc_bin = CcBinary(main_cc, symlinks, implicit, deps, matrix,
355 phony_prefix, preprocessed, bin_path)
356
357 self.cc_bins.append(cc_bin)
358
359 def WriteCcBinary(self, cc_bin):
360 c = cc_bin
361
362 out_deps = set()
363 self._TransitiveClosure(c.main_cc, c.deps, out_deps)
364 unique_deps = sorted(out_deps)
365
366 # save for SourcesForBinary()
367 self.cc_binary_deps[c.main_cc] = unique_deps
368
369 compile_imp = list(c.implicit)
370 for label in unique_deps:
371 cc_lib = self.cc_libs[label] # should exit
372 # compile actions of binaries that have ASDL label deps need the
373 # generated header as implicit dep
374 compile_imp.extend(cc_lib.generated_headers)
375
376 for config in c.matrix:
377 if len(config) == 2:
378 config = (config[0], config[1], None)
379
380 for label in unique_deps:
381 cc_lib = self.cc_libs[label] # should exit
382
383 cc_lib.MaybeWrite(self, config, c.preprocessed)
384
385 # Compile main object, maybe with IMPLICIT headers deps
386 main_obj = ObjPath(c.main_cc, config)
387 self.compile(main_obj,
388 c.main_cc,
389 c.deps,
390 config,
391 implicit=compile_imp)
392 if c.preprocessed:
393 self.compile('',
394 c.main_cc,
395 c.deps,
396 config,
397 implicit=compile_imp,
398 maybe_preprocess=True)
399
400 config_dir = ConfigDir(config)
401 bin_dir = '_bin/%s' % config_dir
402
403 if c.bin_path:
404 # e.g. _bin/cxx-dbg/oils_for_unix
405 bin_ = '%s/%s' % (bin_dir, c.bin_path)
406 else:
407 # e.g. _gen/mycpp/examples/classes.mycpp
408 rel_path, _ = os.path.splitext(c.main_cc)
409
410 # Put binary in _bin/cxx-dbg/mycpp/examples, not _bin/cxx-dbg/_gen/mycpp/examples
411 if rel_path.startswith('_gen/'):
412 rel_path = rel_path[len('_gen/'):]
413
414 bin_ = '%s/%s' % (bin_dir, rel_path)
415
416 # Link with OBJECT deps
417 self.link(bin_, main_obj, unique_deps, config)
418
419 # Make symlinks
420 for symlink in c.symlinks:
421 # Must explicitly specify bin_path to have a symlink, for now
422 assert c.bin_path is not None
423 self.n.build(['%s/%s' % (bin_dir, symlink)],
424 'symlink', [bin_],
425 variables=[('dir', bin_dir),
426 ('target', c.bin_path),
427 ('new', symlink)])
428 self.n.newline()
429
430 if c.phony_prefix:
431 key = '%s-%s' % (c.phony_prefix, config_dir)
432 if key not in self.phony:
433 self.phony[key] = []
434 self.phony[key].append(bin_)
435
436 def SourcesForBinary(self, main_cc):
437 """
438 Used for preprocessed metrics, release tarball, _build/oils.sh, etc.
439 """
440 deps = self.cc_binary_deps[main_cc]
441 sources = [main_cc]
442 for label in deps:
443 sources.extend(self.cc_libs[label].srcs)
444 return sources
445
446 def HeadersForBinary(self, main_cc):
447 deps = self.cc_binary_deps[main_cc]
448 headers = []
449 for label in deps:
450 headers.extend(self.cc_libs[label].headers)
451 headers.extend(self.cc_libs[label].generated_headers)
452 return headers
453
454 def asdl_library(self, asdl_path, deps=None, pretty_print_methods=True):
455
456 deps = deps or []
457
458 # SYSTEM header, _gen/asdl/hnode.asdl.h
459 deps.append('//asdl/hnode.asdl')
460 deps.append('//display/pretty.asdl')
461
462 # to create _gen/mycpp/examples/expr.asdl.h
463 prefix = '_gen/%s' % asdl_path
464
465 out_cc = prefix + '.cc'
466 out_header = prefix + '.h'
467
468 asdl_flags = ''
469
470 if pretty_print_methods:
471 outputs = [out_cc, out_header]
472 else:
473 outputs = [out_header]
474 asdl_flags += '--no-pretty-print-methods'
475
476 debug_mod = prefix + '_debug.py'
477 outputs.append(debug_mod)
478
479 # Generating syntax_asdl.h does NOT depend on hnode_asdl.h existing ...
480 self.n.build(outputs,
481 'asdl-cpp', [asdl_path],
482 implicit=['_bin/shwrap/asdl_main'],
483 variables=[
484 ('action', 'cpp'),
485 ('out_prefix', prefix),
486 ('asdl_flags', asdl_flags),
487 ('debug_mod', debug_mod),
488 ])
489 self.n.newline()
490
491 # ... But COMPILING anything that #includes it does.
492 # Note: assumes there's a build rule for this "system" ASDL schema
493
494 srcs = [out_cc] if pretty_print_methods else []
495 # Define lazy CC library
496 self.cc_library(
497 '//' + asdl_path,
498 srcs=srcs,
499 deps=deps,
500 # For compile_one steps of files that #include this ASDL file
501 generated_headers=[out_header],
502 )
503
504 def py_binary(self, main_py, deps_base_dir='_build/NINJA', template='py'):
505 """
506 Wrapper for Python script with dynamically discovered deps
507 """
508 rel_path, _ = os.path.splitext(main_py)
509 py_module = rel_path.replace(
510 '/', '.') # asdl/asdl_main.py -> asdl.asdl_main
511
512 deps_path = os.path.join(deps_base_dir, py_module, 'deps.txt')
513 with open(deps_path) as f:
514 deps = [line.strip() for line in f]
515
516 deps.remove(main_py) # raises ValueError if it's not there
517
518 basename = os.path.basename(rel_path)
519 self.n.build('_bin/shwrap/%s' % basename,
520 'write-shwrap', [main_py] + deps,
521 variables=[('template', template)])
522 self.n.newline()
523
524 def souffle_binary(self, souffle_cpp):
525 """
526 Compile souffle C++ into a native executable.
527 """
528 rel_path, _ = os.path.splitext(souffle_cpp)
529 basename = os.path.basename(rel_path)
530
531 souffle_obj = '_build/obj/datalog/%s.o' % basename
532 self.n.build([souffle_obj],
533 'compile_one',
534 souffle_cpp,
535 variables=[('compiler', 'cxx'), ('variant', 'opt'),
536 ('more_cxx_flags', "'-Ivendor -std=c++17'")])
537
538 souffle_bin = '_bin/datalog/%s' % basename
539 self.n.build([souffle_bin],
540 'link',
541 souffle_obj,
542 variables=[('compiler', 'cxx'), ('variant', 'opt'),
543 ('more_link_flags', "'-lstdc++fs'")])
544
545 self.n.newline()