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

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