| 1 | #!/usr/bin/env python2 | 
| 2 | """ | 
| 3 | ninja_lib.py | 
| 4 |  | 
| 5 | Runtime options: | 
| 6 |  | 
| 7 | CXXFLAGS     Additional flags to pass to the C++ compiler | 
| 8 |  | 
| 9 | Notes 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 | """ | 
| 26 | from __future__ import print_function | 
| 27 |  | 
| 28 | import collections | 
| 29 | import glob | 
| 30 | import os | 
| 31 | import sys | 
| 32 |  | 
| 33 |  | 
| 34 | def log(msg, *args): | 
| 35 | if args: | 
| 36 | msg = msg % args | 
| 37 | print(msg, file=sys.stderr) | 
| 38 |  | 
| 39 |  | 
| 40 | def 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 |  | 
| 47 | COMPILERS_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 |  | 
| 64 | GC_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 |  | 
| 86 | SMALL_TEST_MATRIX = [ | 
| 87 | ('cxx', 'asan'), | 
| 88 | ('cxx', 'ubsan'), | 
| 89 | ('clang', 'coverage'), | 
| 90 | ] | 
| 91 |  | 
| 92 |  | 
| 93 | def 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 |  | 
| 103 | def 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 | 
| 109 | CcBinary = collections.namedtuple( | 
| 110 | 'CcBinary', | 
| 111 | 'main_cc symlinks implicit deps matrix phony_prefix preprocessed bin_path') | 
| 112 |  | 
| 113 |  | 
| 114 | class 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 |  | 
| 174 | class 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() |