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

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