#!/usr/bin/env python3 """generates a Ninja build file usage: python3 configure.py python3 configure.py --asan # enable address/undefined sanitizers python3 configure.py --debug # -O0 instead of -O2 python3 configure.py --selinux # enable SELinux integration python3 configure.py --compile-commands # also write build/compile_commands.json """ import argparse import json import os import shlex import shutil import sys from pathlib import Path ROOT = Path(__file__).resolve().parent SRC = ROOT / "src" BUILD = ROOT / "build" CXX = os.environ.get("CXX", "g++") CXXFLAGS = ["-std=c++20", "-O2", "-g"] WARNINGS = ["-Wall", "-Wextra", "-Wpedantic", "-Wshadow", "-Wconversion", "-Wnull-dereference"] linkflags = [] def discover_sources(): return sorted( p.name for p in SRC.glob("*.cpp") if not p.name.endswith("_main.cpp") and not p.name.startswith(".") ) def write_makefile_convenience(): make = ROOT / "Makefile" make.write_text( """# convenience wrapper around configure.py + ninja. # # make -> configure + build # make configure -> regenerate build.ninja # make test -> build + run the host-side unit tests # make clean -> remove build dir # make format -> clang-format all sources (if available) # .PHONY: all configure test clean format all: configure \t@ninja -C build configure: \t@python3 configure.py test: configure \t@ninja -C build unit_tests && ./build/unit_tests clean: \t@rm -rf build format: \t@command -v clang-format >/dev/null && clang-format -i src/*.cpp src/*.hpp || true """, encoding="utf-8", ) def emit_ninja(cxx, cxxflags, dst, write_cc=False): sources = discover_sources() objs = ["obj/" + Path(s).stem + ".o" for s in sources] target = "bajia" ctl_target = "bctl" # bctl_main.cpp is a second shipped binary (the control client), # excluded from the init objects by the *_main.cpp convention. ctl_source = SRC / "bctl_main.cpp" ctl_obj = "obj/bctl_main.o" # host-side unit tests: the test TU #includes src/*.cpp directly, so a # single object covers it. test_source = ROOT / "tests" / "unit_tests.cpp" test_obj = "obj/unit_tests.o" # (source, output) pairs for compile_commands.json. cc_entries = [(SRC / s, o) for s, o in zip(sources, objs)] cc_entries.append((ctl_source, ctl_obj)) cc_entries.append((test_source, test_obj)) rule_cxx = ( "rule cxx\n" " command = {cxx} {cxxflags} -MMD -MF $out.d -c $in -o $out\n" " depfile = $out.d\n" " deps = gcc\n" " description = CXX $out\n" ).format(cxx=cxx, cxxflags=" ".join(cxxflags)) rule_link = ( "rule link\n" " command = {cxx} {ldflags} $in -o $out -lpthread\n" " description = LINK $out\n" ).format(cxx=cxx, ldflags=" ".join(linkflags)) lines = ["# generated by configure.py -- do not edit", "ninja_required_version = 1.8", ""] lines.append(rule_cxx) lines.append(rule_link) lines.append('build {target}: link {objs}'.format(target=target, objs=" ".join(objs))) lines.append('build {ctl}: link {ctl_obj}'.format(ctl=ctl_target, ctl_obj=ctl_obj)) lines.append('build unit_tests: link {test_obj}'.format(test_obj=test_obj)) lines.append("") for o, s in zip(objs, sources): lines.append('build {o}: cxx {src}/{s}'.format(o=o, src=SRC, s=s)) lines.append('build {ctl_obj}: cxx {ctl_src}'.format(ctl_obj=ctl_obj, ctl_src=ctl_source)) lines.append('build {test_obj}: cxx {test_src}'.format(test_obj=test_obj, test_src=test_source)) lines.append("") lines.append( 'build all: phony {target} {ctl}'.format(target=target, ctl=ctl_target)) lines.append('build test: phony unit_tests') lines.append("default all") lines.append("") if write_cc: write_compile_commands(cxx, cxxflags, cc_entries) (BUILD / "obj").mkdir(exist_ok=True) dst.write_text("\n".join(lines) + "\n", encoding="utf-8") def write_compile_commands(cxx, cxxflags, src_out_pairs): """Write build/compile_commands.json for clangd / clang-tidy.""" entries = [] for src, out in src_out_pairs: args = [cxx] + list(cxxflags) + ["-c", str(src), "-o", out] entries.append({ "directory": str(BUILD.resolve()), "file": str(src.resolve()), "output": out, "arguments": args, "command": shlex.join(args), }) (BUILD / "compile_commands.json").write_text( json.dumps(entries, indent=1) + "\n", encoding="utf-8") def parse_args(): p = argparse.ArgumentParser(description="Configure bajia build (generates build.ninja)") p.add_argument("--asan", action="store_true", help="enable address + UB sanitizers") p.add_argument("--debug", action="store_true", help="disable -O2, enable -O0") p.add_argument("--clean", action="store_true", help="remove build dir") p.add_argument("--selinux", action="store_true", help="enable SELinux integration (needs libselinux-dev; " "see tools/run_vm.py --selinux for a VM test path)") p.add_argument("--compile-commands", action="store_true", help="write build/compile_commands.json (for clangd)") return p.parse_args() def main(): args = parse_args() if args.clean: shutil.rmtree(BUILD, ignore_errors=True) print("removed build/") return 0 if not shutil.which("ninja"): print("error: ninja not found in PATH", file=sys.stderr) return 1 # Wipe stale objects when compiler flags change: ninja doesn't track # cflags, so a re-run with a different --selinux/--asan/--debug profile # would otherwise link a half-stale tree. stamp = {"selinux": args.selinux, "asan": args.asan, "debug": args.debug} stamp_file = BUILD / ".configure.json" if stamp_file.exists(): try: prev = json.loads(stamp_file.read_text()) except (json.JSONDecodeError, OSError): prev = None if prev != stamp: print("build configuration changed; cleaning build/") shutil.rmtree(BUILD, ignore_errors=True) BUILD.mkdir(exist_ok=True) cxxflags = list(CXXFLAGS) global linkflags linkflags = [] if args.debug: cxxflags = [f for f in cxxflags if f != "-O2"] cxxflags += ["-O0"] if args.asan: cxxflags += ["-fsanitize=address,undefined", "-fno-omit-frame-pointer"] linkflags += ["-fsanitize=address,undefined"] if args.selinux: cxxflags += ["-DBAJIA_SELINUX"] linkflags += ["-lselinux"] cxxflags = [f for f in cxxflags if f not in WARNINGS] cxxflags = WARNINGS + cxxflags emit_ninja(CXX, cxxflags, BUILD / "build.ninja", write_cc=args.compile_commands) write_makefile_convenience() stamp_file.write_text(json.dumps(stamp, indent=1) + "\n", encoding="utf-8") print(f"configured {len(discover_sources())} sources -> build/build.ninja") if args.compile_commands: print("wrote build/compile_commands.json") print("run: ninja -C build") return 0 if __name__ == "__main__": sys.exit(main())