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:
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()