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

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