kit

kit
git clone https://git.ryansepassi.com/git/kit.git
Log | Files | Refs | README

gen_compile_commands.py (5558B)


      1 #!/usr/bin/env python3
      2 """Generate compile_commands.json for clangd.
      3 
      4 This repo has no compilation database, so clangd parses every file with bare
      5 defaults (no -I paths, host libc headers) and emits spurious "file not found" /
      6 "unknown type" cascades. The flags here mirror the Makefile so clangd sees the
      7 same translation unit the build does.
      8 
      9 Scope: the compiler + runtime proper -- src/, lang/, driver/, rt/lib/. test/ is
     10 intentionally skipped: it mixes real unit tests with non-TU fixtures, and the
     11 per-target flags vary, so listing it would create more false diagnostics than
     12 it removes. Re-run this whenever sources are added or removed:
     13 
     14     make compile-commands        # or: python3 scripts/gen_compile_commands.py
     15 
     16 Output goes to build/compile_commands.json. clangd automatically searches a
     17 build/ subdirectory of the project root, so no .clangd file or root symlink is
     18 needed. The file is machine-specific (absolute paths, host SDK) and lives under
     19 the already-git-ignored build/ dir. Note: `make clean` removes build/, so
     20 re-run this afterward.
     21 """
     22 
     23 import json
     24 import os
     25 import subprocess
     26 import sys
     27 
     28 REPO = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
     29 
     30 # Parse-relevant flags only: -I paths, language, freestanding/hosted mode. The
     31 # build's -O/-W*/-fsanitize/-fvisibility/-MMD flags don't affect how clangd
     32 # resolves headers or types, so they're omitted to keep diagnostics clean.
     33 FREESTANDING = ["-std=c11", "-ffreestanding", "-nostdinc", "-Irt/include"]
     34 
     35 
     36 def macos_sdk_path():
     37     try:
     38         out = subprocess.run(
     39             ["xcrun", "--show-sdk-path"],
     40             capture_output=True, text=True, check=True,
     41         )
     42         return out.stdout.strip() or None
     43     except Exception:
     44         return None
     45 
     46 
     47 SDK = macos_sdk_path() if sys.platform == "darwin" else None
     48 
     49 # driver/env/*.c is the one hosted regime (DRIVER_ENV_CFLAGS = HOST_CFLAGS):
     50 # real host SDK headers, no -nostdinc. Mirror env.mk's Darwin feature macros.
     51 HOSTED = ["-std=c11"]
     52 if SDK:
     53     HOSTED += ["-isysroot", SDK]
     54 HOSTED += ["-D_XOPEN_SOURCE=600", "-D_DARWIN_C_SOURCE=1",
     55            "-Iinclude", "-Ilang", "-Idriver", "-Idriver/lib"]
     56 
     57 # rt/lib/*.c: freestanding runtime, its own include layout (RT_LIB_INCS + the
     58 # lp64_le ABI headers for a 64-bit host). kit compiles these per-target; this
     59 # is a host-shaped approximation good enough for clangd to resolve headers.
     60 RT = ["-std=c11", "-ffreestanding", "-nostdinc",
     61       "-Irt/lib/include/common", "-Irt/lib/impl",
     62       "-Irt/lib/include/lp64_le", "-Irt/include"]
     63 
     64 
     65 def flags_for(rel):
     66     """Return the include/mode flags for a source path relative to REPO."""
     67     if rel == "src/api/lang_registry.c":
     68         # The one libkit source that reaches into lang/ for "c/c.h" etc.
     69         return FREESTANDING + ["-Iinclude", "-Ilang", "-Isrc"]
     70     if rel.startswith("src/"):
     71         return FREESTANDING + ["-Iinclude", "-Isrc"]
     72     if rel.startswith("lang/cpp/"):
     73         return FREESTANDING + ["-Iinclude", "-Ilang/cpp"]
     74     if rel.startswith("lang/c/"):
     75         return FREESTANDING + ["-Iinclude", "-Ilang/cpp", "-Ilang/c"]
     76     if rel.startswith("lang/wasm/"):
     77         return FREESTANDING + ["-Iinclude", "-Isrc", "-Ilang/wasm"]
     78     if rel.startswith("lang/toy/"):
     79         return FREESTANDING + ["-Iinclude", "-Ilang/toy"]
     80     if rel.startswith("driver/env/"):
     81         return list(HOSTED)
     82     if rel.startswith("driver/"):
     83         return FREESTANDING + ["-Iinclude", "-Ilang", "-Idriver", "-Idriver/lib"]
     84     if rel.startswith("rt/lib/"):
     85         return list(RT)
     86     return None
     87 
     88 
     89 def main():
     90     roots = ["src", "lang", "driver", "rt/lib"]
     91     entries = []
     92     unmapped = []
     93     for root in roots:
     94         base = os.path.join(REPO, root)
     95         for dirpath, _dirs, files in os.walk(base):
     96             for name in files:
     97                 if not name.endswith(".c"):
     98                     continue
     99                 abspath = os.path.join(dirpath, name)
    100                 rel = os.path.relpath(abspath, REPO)
    101                 flags = flags_for(rel)
    102                 if flags is None:
    103                     # Every .c under the walked roots must map to a flag set.
    104                     # A None here means a new source tree (e.g. a new lang/
    105                     # frontend or source root) was added without a matching
    106                     # branch in flags_for(). Fail loudly rather than silently
    107                     # dropping the file from the database -- silent drops are
    108                     # how this script drifts out of sync with the Makefile.
    109                     unmapped.append(rel)
    110                     continue
    111                 args = ["clang"] + flags + ["-c", rel, "-o", "/dev/null"]
    112                 entries.append({
    113                     "directory": REPO,
    114                     "file": abspath,
    115                     "arguments": args,
    116                 })
    117     if unmapped:
    118         sys.stderr.write(
    119             "error: %d source file(s) under the walked roots have no flag "
    120             "mapping in flags_for(); add a branch (or exclude them) so the "
    121             "compile database stays in sync with the Makefile:\n" % len(unmapped))
    122         for rel in sorted(unmapped):
    123             sys.stderr.write("  %s\n" % rel)
    124         sys.exit(1)
    125     entries.sort(key=lambda e: e["file"])
    126     out_dir = os.path.join(REPO, "build")
    127     os.makedirs(out_dir, exist_ok=True)
    128     out = os.path.join(out_dir, "compile_commands.json")
    129     with open(out, "w") as f:
    130         json.dump(entries, f, indent=2)
    131         f.write("\n")
    132     print("wrote %s (%d entries)" % (out, len(entries)))
    133 
    134 
    135 if __name__ == "__main__":
    136     main()