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