kit

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

commit 0a585d8c2f0c9c8b47510de3a00305b74b0464e3
parent b264b52f4dfe675002249f74855d67fe720d7c29
Author: Ryan Sepassi <rsepassi@gmail.com>
Date:   Fri, 29 May 2026 14:27:39 -0700

build: generate compile_commands.json for clangd

clangd had no compilation database, so it parsed every file with bare
defaults (no -I paths, host libc) and emitted spurious "file not found" /
"unknown type" cascades.

Add scripts/gen_compile_commands.py, which mirrors the Makefile's per-dir
compile regimes -- freestanding -nostdinc + include roots for src/, lang/,
and driver/; hosted SDK flags for driver/env/; the rt include layout for
rt/lib/ -- and writes build/compile_commands.json. clangd auto-discovers a
compile_commands.json under build/, so no .clangd file or root symlink is
needed and the repo root stays clean (build/ is already git-ignored).

Add a `make compile-commands` target to regenerate it.

Diffstat:
MMakefile | 7+++++++
Ascripts/gen_compile_commands.py | 119+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
2 files changed, 126 insertions(+), 0 deletions(-)

diff --git a/Makefile b/Makefile @@ -397,6 +397,7 @@ DIST_TARBALL = build/dist/cfree.tar.gz bin \ dist \ format \ + compile-commands \ clean \ bootstrap \ bootstrap-debug \ @@ -554,6 +555,12 @@ dist: format: find src include driver lang test rt -path test/pp -prune -o \( -name '*.c' -o -name '*.h' \) -print | xargs clang-format -i --style=google +# Regenerate the clangd compilation database (build/compile_commands.json). +# clangd auto-discovers it under build/; re-run after adding/removing sources +# or after `make clean`. +compile-commands: + python3 scripts/gen_compile_commands.py + clean: rm -rf $(BUILD_DIR) diff --git a/scripts/gen_compile_commands.py b/scripts/gen_compile_commands.py @@ -0,0 +1,119 @@ +#!/usr/bin/env python3 +"""Generate compile_commands.json for clangd. + +This repo has no compilation database, so clangd parses every file with bare +defaults (no -I paths, host libc headers) and emits spurious "file not found" / +"unknown type" cascades. The flags here mirror the Makefile so clangd sees the +same translation unit the build does. + +Scope: the compiler + runtime proper -- src/, lang/, driver/, rt/lib/. test/ is +intentionally skipped: it mixes real unit tests with non-TU fixtures, and the +per-target flags vary, so listing it would create more false diagnostics than +it removes. Re-run this whenever sources are added or removed: + + make compile-commands # or: python3 scripts/gen_compile_commands.py + +Output goes to build/compile_commands.json. clangd automatically searches a +build/ subdirectory of the project root, so no .clangd file or root symlink is +needed. The file is machine-specific (absolute paths, host SDK) and lives under +the already-git-ignored build/ dir. Note: `make clean` removes build/, so +re-run this afterward. +""" + +import json +import os +import subprocess +import sys + +REPO = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) + +# Parse-relevant flags only: -I paths, language, freestanding/hosted mode. The +# build's -O/-W*/-fsanitize/-fvisibility/-MMD flags don't affect how clangd +# resolves headers or types, so they're omitted to keep diagnostics clean. +FREESTANDING = ["-std=c11", "-ffreestanding", "-nostdinc", "-Irt/include"] + + +def macos_sdk_path(): + try: + out = subprocess.run( + ["xcrun", "--show-sdk-path"], + capture_output=True, text=True, check=True, + ) + return out.stdout.strip() or None + except Exception: + return None + + +SDK = macos_sdk_path() if sys.platform == "darwin" else None + +# driver/env/*.c is the one hosted regime (DRIVER_ENV_CFLAGS = HOST_CFLAGS): +# real host SDK headers, no -nostdinc. Mirror env.mk's Darwin feature macros. +HOSTED = ["-std=c11"] +if SDK: + HOSTED += ["-isysroot", SDK] +HOSTED += ["-D_XOPEN_SOURCE=600", "-D_DARWIN_C_SOURCE=1", "-Iinclude", "-Ilang"] + +# rt/lib/*.c: freestanding runtime, its own include layout (RT_LIB_INCS + the +# lp64_le ABI headers for a 64-bit host). cfree compiles these per-target; this +# is a host-shaped approximation good enough for clangd to resolve headers. +RT = ["-std=c11", "-ffreestanding", "-nostdinc", + "-Irt/lib/include/common", "-Irt/lib/impl", + "-Irt/lib/include/lp64_le", "-Irt/include"] + + +def flags_for(rel): + """Return the include/mode flags for a source path relative to REPO.""" + if rel == "src/api/lang_registry.c": + # The one libcfree source that reaches into lang/ for "c/c.h" etc. + return FREESTANDING + ["-Iinclude", "-Ilang", "-Isrc"] + if rel.startswith("src/"): + return FREESTANDING + ["-Iinclude", "-Isrc"] + if rel.startswith("lang/cpp/"): + return FREESTANDING + ["-Iinclude", "-Ilang/cpp"] + if rel.startswith("lang/c/"): + return FREESTANDING + ["-Iinclude", "-Ilang/cpp", "-Ilang/c"] + if rel.startswith("lang/wasm/"): + return FREESTANDING + ["-Iinclude", "-Isrc", "-Ilang/wasm"] + if rel.startswith("lang/toy/"): + return FREESTANDING + ["-Iinclude", "-Ilang/toy"] + if rel.startswith("driver/env/"): + return list(HOSTED) + if rel.startswith("driver/"): + return FREESTANDING + ["-Iinclude", "-Ilang"] + if rel.startswith("rt/lib/"): + return list(RT) + return None + + +def main(): + roots = ["src", "lang", "driver", "rt/lib"] + entries = [] + for root in roots: + base = os.path.join(REPO, root) + for dirpath, _dirs, files in os.walk(base): + for name in files: + if not name.endswith(".c"): + continue + abspath = os.path.join(dirpath, name) + rel = os.path.relpath(abspath, REPO) + flags = flags_for(rel) + if flags is None: + continue + args = ["clang"] + flags + ["-c", rel, "-o", "/dev/null"] + entries.append({ + "directory": REPO, + "file": abspath, + "arguments": args, + }) + entries.sort(key=lambda e: e["file"]) + out_dir = os.path.join(REPO, "build") + os.makedirs(out_dir, exist_ok=True) + out = os.path.join(out_dir, "compile_commands.json") + with open(out, "w") as f: + json.dump(entries, f, indent=2) + f.write("\n") + print("wrote %s (%d entries)" % (out, len(entries))) + + +if __name__ == "__main__": + main()