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

518 lines, 303 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
46 ('cxx', 'asan+gcalways'),
47 ('cxx', 'asan32+gcalways'),
48
49 ('cxx', 'ubsan'),
50
51 #('clang', 'asan'),
52 ('clang', 'dbg'), # compile-quickly
53 ('clang', 'opt'), # for comparisons
54 ('clang', 'ubsan'), # finds different bugs
55 ('clang', 'coverage'),
56]
57
58GC_PERF_VARIANTS = [
59 ('cxx', 'opt+bumpleak'),
60 ('cxx', 'opt+bumproot'),
61
62 ('cxx', 'opt+bumpsmall'),
63 ('cxx', 'asan+bumpsmall'),
64
65 ('cxx', 'opt+nopool'),
66
67 # TODO: should be binary with different files
68 ('cxx', 'opt+cheney'),
69
70 ('cxx', 'opt+tcmalloc'),
71
72 # For tracing allocations, or debugging
73 ('cxx', 'uftrace'),
74
75 # Test performance of 32-bit build. (It uses less memory usage, but can be
76 # slower.)
77 ('cxx', 'opt32'),
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 bin_path')
106
107
108class CcLibrary(object):
109 """
110 Life cycle:
111
112 1. A cc_library is first created
113 2. A cc_binary can depend on it
114 - maybe writing rules, and ensuring uniques per configuration
115 3. The link step needs the list of objects
116 4. The tarball needs the list of sources for binary
117 """
118
119 def __init__(self, label, srcs, implicit, deps, headers, 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('', src, self.deps, config, 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 def __init__(self, n):
202 self.n = n # direct ninja writer
203
204 self.cc_bins = [] # list of CcBinary() objects to write
205 self.cc_libs = {} # label -> CcLibrary object
206 self.cc_binary_deps = {} # main_cc -> list of LABELS
207 self.phony = {} # list of phony targets
208
209 def AddPhony(self, phony_to_add):
210 self.phony.update(phony_to_add)
211
212 def WritePhony(self):
213 for name in sorted(self.phony):
214 targets = self.phony[name]
215 if targets:
216 self.n.build([name], 'phony', targets)
217 self.n.newline()
218
219 def WriteRules(self):
220 for cc_bin in self.cc_bins:
221 self.WriteCcBinary(cc_bin)
222
223 def compile(self, out_obj, in_cc, deps, config, implicit=None, maybe_preprocess=False):
224 """ .cc -> compiler -> .o """
225
226 implicit = implicit or []
227
228 compiler, variant, more_cxx_flags = config
229 if more_cxx_flags is None:
230 flags_str = "''"
231 else:
232 assert "'" not in more_cxx_flags, more_cxx_flags # can't handle single quotes
233 flags_str = "'%s'" % more_cxx_flags
234
235 v = [('compiler', compiler), ('variant', variant), ('more_cxx_flags', flags_str)]
236 if maybe_preprocess:
237 # Limit it to certain configs
238 if more_cxx_flags is None and variant in ('dbg', 'opt'):
239 pre = '_build/preprocessed/%s-%s/%s' % (compiler, variant, in_cc)
240 self.n.build(pre, 'preprocess', [in_cc], implicit=implicit, variables=v)
241 else:
242 self.n.build([out_obj], 'compile_one', [in_cc], implicit=implicit, variables=v)
243
244 self.n.newline()
245
246 def link(self, out_bin, main_obj, deps, config):
247 """ list of .o -> linker -> executable, along with stripped version """
248 compiler, variant, _ = config
249
250 assert isinstance(out_bin, str), out_bin
251 assert isinstance(main_obj, str), main_obj
252
253 objects = [main_obj]
254 for label in deps:
255 key = (label, compiler, variant)
256 try:
257 cc_lib = self.cc_libs[label]
258 except KeyError:
259 raise RuntimeError("Couldn't resolve label %r" % label)
260
261 o = cc_lib.obj_lookup[config]
262 objects.extend(o)
263
264 v = [('compiler', compiler), ('variant', variant), ('more_link_flags', "''")]
265 self.n.build([out_bin], 'link', objects, variables=v)
266 self.n.newline()
267
268 # Strip any .opt binaries
269 if variant.startswith('opt') or variant.startswith('opt32'):
270 stripped = out_bin + '.stripped'
271 symbols = out_bin + '.symbols'
272 self.n.build([stripped, symbols], 'strip', [out_bin])
273 self.n.newline()
274
275 def comment(self, s):
276 self.n.comment(s)
277 self.n.newline()
278
279 def cc_library(self, label,
280 srcs = None,
281 implicit = None,
282 deps = None,
283 # note: headers is only used for tarball manifest, not compiler command line
284 headers = None,
285 generated_headers = None):
286
287 # srcs = [] is allowed for _gen/asdl/hnode.asdl.h
288 if srcs is None:
289 raise RuntimeError('cc_library %r requires srcs' % label)
290
291 implicit = implicit or []
292 deps = deps or []
293 headers = headers or []
294 generated_headers = generated_headers or []
295
296 if label in self.cc_libs:
297 raise RuntimeError('%s was already defined' % label)
298
299 self.cc_libs[label] = CcLibrary(label, srcs, implicit, deps,
300 headers, generated_headers)
301
302 def _TransitiveClosure(self, name, deps, unique_out):
303 """
304 Args:
305 name: for error messages
306 """
307 for label in deps:
308 if label in unique_out:
309 continue
310 unique_out.add(label)
311
312 try:
313 cc_lib = self.cc_libs[label]
314 except KeyError:
315 raise RuntimeError('Undefined label %s in %s' % (label, name))
316
317 self._TransitiveClosure(cc_lib.label, cc_lib.deps, unique_out)
318
319 def cc_binary(self, main_cc,
320 symlinks = None,
321 implicit = None, # for COMPILE action, not link action
322 deps = None,
323 matrix = None, # $compiler $variant
324 phony_prefix = None,
325 preprocessed = False,
326 bin_path = None, # default is _bin/$compiler-$variant/rel/path
327 ):
328 symlinks = symlinks or []
329 implicit = implicit or []
330 deps = deps or []
331 if not matrix:
332 raise RuntimeError("Config matrix required")
333
334 cc_bin = CcBinary(main_cc, symlinks, implicit, deps, matrix, phony_prefix,
335 preprocessed, bin_path)
336
337 self.cc_bins.append(cc_bin)
338
339 def WriteCcBinary(self, cc_bin):
340 c = cc_bin
341
342 out_deps = set()
343 self._TransitiveClosure(c.main_cc, c.deps, out_deps)
344 unique_deps = sorted(out_deps)
345
346 # save for SourcesForBinary()
347 self.cc_binary_deps[c.main_cc] = unique_deps
348
349 compile_imp = list(c.implicit)
350 for label in unique_deps:
351 cc_lib = self.cc_libs[label] # should exit
352 # compile actions of binaries that have ASDL label deps need the
353 # generated header as implicit dep
354 compile_imp.extend(cc_lib.generated_headers)
355
356 for config in c.matrix:
357 if len(config) == 2:
358 config = (config[0], config[1], None)
359
360 for label in unique_deps:
361 cc_lib = self.cc_libs[label] # should exit
362
363 cc_lib.MaybeWrite(self, config, c.preprocessed)
364
365 # Compile main object, maybe with IMPLICIT headers deps
366 main_obj = ObjPath(c.main_cc, config)
367 self.compile(main_obj, c.main_cc, c.deps, config, implicit=compile_imp)
368 if c.preprocessed:
369 self.compile('', c.main_cc, c.deps, config, implicit=compile_imp,
370 maybe_preprocess=True)
371
372 config_dir = ConfigDir(config)
373 bin_dir = '_bin/%s' % config_dir
374
375 if c.bin_path:
376 # e.g. _bin/cxx-dbg/oils_for_unix
377 bin_ = '%s/%s' % (bin_dir, c.bin_path)
378 else:
379 # e.g. _gen/mycpp/examples/classes.mycpp
380 rel_path, _ = os.path.splitext(c.main_cc)
381
382 # Put binary in _bin/cxx-dbg/mycpp/examples, not _bin/cxx-dbg/_gen/mycpp/examples
383 if rel_path.startswith('_gen/'):
384 rel_path = rel_path[len('_gen/'):]
385
386 bin_= '%s/%s' % (bin_dir, rel_path)
387
388 # Link with OBJECT deps
389 self.link(bin_, main_obj, unique_deps, config)
390
391 # Make symlinks
392 for symlink in c.symlinks:
393 # Must explicitly specify bin_path to have a symlink, for now
394 assert c.bin_path is not None
395 self.n.build(
396 ['%s/%s' % (bin_dir, symlink)],
397 'symlink',
398 [bin_],
399 variables = [('dir', bin_dir), ('target', c.bin_path), ('new', symlink)])
400 self.n.newline()
401
402 if c.phony_prefix:
403 key = '%s-%s' % (c.phony_prefix, config_dir)
404 if key not in self.phony:
405 self.phony[key] = []
406 self.phony[key].append(bin_)
407
408 def SourcesForBinary(self, main_cc):
409 """
410 Used for preprocessed metrics, release tarball, _build/oils.sh, etc.
411 """
412 deps = self.cc_binary_deps[main_cc]
413 sources = [main_cc]
414 for label in deps:
415 sources.extend(self.cc_libs[label].srcs)
416 return sources
417
418 def HeadersForBinary(self, main_cc):
419 deps = self.cc_binary_deps[main_cc]
420 headers = []
421 for label in deps:
422 headers.extend(self.cc_libs[label].headers)
423 headers.extend(self.cc_libs[label].generated_headers)
424 return headers
425
426 def asdl_library(self, asdl_path, deps = None,
427 pretty_print_methods=True):
428
429 deps = deps or []
430
431 # SYSTEM header, _gen/asdl/hnode.asdl.h
432 deps.append('//asdl/hnode.asdl')
433
434 # to create _gen/mycpp/examples/expr.asdl.h
435 prefix = '_gen/%s' % asdl_path
436
437 out_cc = prefix + '.cc'
438 out_header = prefix + '.h'
439
440 asdl_flags = ''
441
442 if pretty_print_methods:
443 outputs = [out_cc, out_header]
444 else:
445 outputs = [out_header]
446 asdl_flags += '--no-pretty-print-methods'
447
448 debug_mod = prefix + '_debug.py'
449 outputs.append(debug_mod)
450
451 # Generating syntax_asdl.h does NOT depend on hnode_asdl.h existing ...
452 self.n.build(outputs, 'asdl-cpp', [asdl_path],
453 implicit = ['_bin/shwrap/asdl_main'],
454 variables = [
455 ('action', 'cpp'),
456 ('out_prefix', prefix),
457 ('asdl_flags', asdl_flags),
458 ('debug_mod', debug_mod),
459 ])
460 self.n.newline()
461
462 # ... But COMPILING anything that #includes it does.
463 # Note: assumes there's a build rule for this "system" ASDL schema
464
465 srcs = [out_cc] if pretty_print_methods else []
466 # Define lazy CC library
467 self.cc_library(
468 '//' + asdl_path,
469 srcs = srcs,
470 deps = deps,
471 # For compile_one steps of files that #include this ASDL file
472 generated_headers = [out_header],
473 )
474
475 def py_binary(self, main_py, deps_base_dir='_build/NINJA', template='py'):
476 """
477 Wrapper for Python script with dynamically discovered deps
478 """
479 rel_path, _ = os.path.splitext(main_py)
480 py_module = rel_path.replace('/', '.') # asdl/asdl_main.py -> asdl.asdl_main
481
482 deps_path = os.path.join(deps_base_dir, py_module, 'deps.txt')
483 with open(deps_path) as f:
484 deps = [line.strip() for line in f]
485
486 deps.remove(main_py) # raises ValueError if it's not there
487
488 basename = os.path.basename(rel_path)
489 self.n.build('_bin/shwrap/%s' % basename, 'write-shwrap', [main_py] + deps,
490 variables=[('template', template)])
491 self.n.newline()
492
493 def souffle_binary(self, souffle_cpp):
494 """
495 Compile a souffle C++ into a native executable.
496 """
497 rel_path, _ = os.path.splitext(souffle_cpp)
498 basename = os.path.basename(rel_path)
499
500 souffle_obj = '_build/obj/datalog/%s.o' % basename
501 self.n.build(
502 [souffle_obj], 'compile_one', souffle_cpp,
503 variables=[
504 ('compiler', 'cxx'),
505 ('variant', 'opt'),
506 ('more_cxx_flags', "'-Ivendor -std=c++17'")
507 ])
508
509 souffle_bin = '_bin/datalog/%s' % basename
510 self.n.build(
511 [souffle_bin], 'link', souffle_obj,
512 variables=[
513 ('compiler', 'cxx'),
514 ('variant', 'opt'),
515 ('more_link_flags', "'-lstdc++fs'")
516 ])
517
518 self.n.newline()