From f7dcb988eeeebaebb8438f312191e24319c0b913 Mon Sep 17 00:00:00 2001 From: Christopher Wellons Date: Tue, 14 Oct 2025 19:12:00 -0400 Subject: [PATCH] WIP New commands: compdb-cc and compdb-c++ --- Dockerfile | 5 +- src/compdb-cc.c | 661 ++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 665 insertions(+), 1 deletion(-) create mode 100644 src/compdb-cc.c diff --git a/Dockerfile b/Dockerfile index 1664dca..4e082d0 100644 --- a/Dockerfile +++ b/Dockerfile @@ -58,7 +58,7 @@ RUN sha256sum -c $PREFIX/src/SHA256SUMS \ && tar xjf vim-$VIM_VERSION.tar.bz2 COPY src/w64devkit.c src/w64devkit.ico src/libmemory.c src/libchkstk.S \ src/alias.c src/debugbreak.c src/pkg-config.c src/vc++filt.c \ - src/peports.c src/profile $PREFIX/src/ + src/peports.c src/compdb-cc.c src/profile $PREFIX/src/ ARG ARCH=x86_64-w64-mingw32 @@ -514,6 +514,9 @@ RUN printf "id ICON \"$PREFIX/src/w64devkit.ico\"" >w64devkit.rc \ && $ARCH-gcc -DEXE=pkg-config.exe -DCMD=pkg-config \ -Os -fno-asynchronous-unwind-tables -Wl,--gc-sections -s -nostdlib \ -o $PREFIX/bin/$ARCH-pkg-config.exe $PREFIX/src/alias.c -lkernel32 \ + && $ARCH-gcc -Os -s -nostartfiles -o $PREFIX/bin/compdb-cc.exe \ + $PREFIX/src/compdb-cc.c -lmemory \ + && cp $PREFIX/bin/compdb-cc.exe $PREFIX/bin/compdb-c++.exe \ && sed -i s/'\'/$ARCH/g $PREFIX/src/profile \ && mkdir -p $PREFIX/lib/pkgconfig \ && cp /mingw-w64-v$MINGW_VERSION/COPYING.MinGW-w64-runtime/COPYING.MinGW-w64-runtime.txt \ diff --git a/src/compdb-cc.c b/src/compdb-cc.c new file mode 100644 index 0000000..8853394 --- /dev/null +++ b/src/compdb-cc.c @@ -0,0 +1,661 @@ +// compdb-cc: JSON Compilation Database build wrapper +// +// Generates a compile_commands.json for arbitrary build systems by wrapping +// the compiler program and logging commands to a database. The database is +// UTF-8-encoded (assuming UTF-8 inputs on POSIX) with LF line endings on all +// platforms. Supports parallel builds. +// +// w64dk: $ cc -nostartfiles -o compdb-cc.exe compdb-cc.c +// unix: $ cc -o compdb-cc compdb-cc.c +// msvc: $ cl compdb-cc.c +// +// First, delete any existing database. This tool cannot update arbitrary +// databases, only those it has created and exclusively modified. Then run +// the build with $COMPDB_PATH set to the absolute path of the database: +// +// $ rm -f compile_commands.json +// $ COMPDB_PATH="$PWD"/compile_commands.json make -B CC=compdb-cc +// +// The wrapper may be called with different working directories by the build +// system, in which case an absolute path will be necessary in order to know +// the proper location. If $COMPDB_PATH is unset or empty, the wrapper runs +// the command without and database updates, and so it can be harmlessly left +// as the compiler during development. +// +// The underlying compiler is derived from the suffix, so compdb-cc runs cc, +// compdb-c++ runs c++, etc. Rename or symlink appropriately. +// +// Ref: https://clang.llvm.org/docs/JSONCompilationDatabase.html +// This is free and unencumbered software released into the public domain. +#include +#include +#include + +#if _MSC_VER +# define unreachable() __assume(0) +# pragma comment(linker, "/subsystem:console") +# pragma comment(lib, "kernel32") +# pragma comment(lib, "shell32") +# pragma comment(lib, "libvcruntime") +#endif + +#define lenof(a) (iz)(sizeof(a) / sizeof(*(a))) +#define affirm(c) while (!(c)) unreachable() +#define S(s) (Str){(u8 *)s, lenof(s)-1} +#define new(a, n, t) (t *)alloc(a, n, sizeof(t), _Alignof(t)) + +typedef uint8_t u8; +typedef int16_t i16; +typedef int32_t b32; +typedef int32_t i32; +typedef int64_t i64; +typedef ptrdiff_t iz; +typedef size_t uz; + + +// Platform API +// +// Platform layer opens $COMPDB_PATH, locks it, seeks SEEK_SIZE bytes +// from the end (if possible), and calls compdb(). The application layer +// calls plt_write() zero or more times. If returns with STATUS_OK, the +// platform layer unlocks the file and execs the remaining command line. + +enum { + STATUS_OK = 0, + STATUS_USAGE = 200, + STATUS_EXEC = 201, + STATUS_OPEN = 202, + STATUS_WRITE = 203, + STATUS_OOM = 204, + + SEEK_SIZE = -3, +}; + +typedef struct Plt Plt; +static b32 plt_write(Plt *, i32 fd, u8 *, i32); +static void plt_oom(); + + +// Application layer + +static uz touz(iz n) +{ + affirm(n >= 0); + return (uz)n; +} + +typedef struct { + u8 *beg; + u8 *end; +} Arena; + +static void *alloc(Arena *a, iz count, iz size, iz align) +{ + iz pad = (iz)-(uz)a->beg & (align - 1); + if (count >= (a->end - a->beg - pad)/size) { + plt_oom(); + } + u8 *r = a->beg + pad; + a->beg += pad + count*size; + return memset(r, 0, touz(size*count)); +} + +typedef struct { + u8 *data; + iz len; +} Str; + +static Str import(u8 *s) +{ + Str r = {0}; + r.data = s; + for (; r.data[r.len]; r.len++) {} + return r; +} + +static b32 equals(Str a, Str b) +{ + return a.len==b.len && !memcmp(a.data, b.data, touz(a.len)); +} + +static Str substring(Str s, iz i) +{ + affirm(i>=0 && i<=s.len); + s.data += i; + s.len -= i; + return s; +} + +static b32 endswith(Str s, Str suf) +{ + return s.len>=suf.len && equals(substring(s, s.len-suf.len), suf); +} + +typedef struct { + Plt *plt; + i32 len; + i32 fd; + b32 err; + u8 buf[1<<12]; +} Output; + +static Output *newoutput(Arena *a, Plt *plt, i32 fd) +{ + Output *b = new(a, 1, Output); + b->plt = plt; + b->fd = fd; + return b; +} + +static void flush(Output *b) +{ + if (!b->err && b->len) { + b->err = !plt_write(b->plt, b->fd, b->buf, b->len); + } + b->len = 0; +} + +static void print(Output *b, Str s) +{ + for (iz off = 0; !b->err && offbuf) - b->len; + i32 count = availbuf+b->len, s.data+off, touz(count)); + off += count; + b->len += count; + if (b->len == lenof(b->buf)) { + flush(b); + } + } +} + +static void printu8(Output *b, u8 c) +{ + print(b, (Str){&c, 1}); +} + +// Print a JSON-quoted representation of the string. +static void printq(Output *b, Str s) +{ + printu8(b, '"'); + for (iz i = 0; i < s.len; i++) { + u8 c = s.data[i]; + if (c == '\n') { + print(b, S("\\n")); + } else if (c == '\t') { + print(b, S("\\t")); + } else if (c == '\r') { + print(b, S("\\r")); + } else if (c=='\\' || c=='"') { + printu8(b, '\\'); + printu8(b, c); + } else if (c < ' ') { + print(b, S("\\u00")); + static u8 hex[] = "0123456789abcdef"; + printu8(b, hex[c>>4]); + printu8(b, hex[c&15]); + } else { + printu8(b, c); + } + } + printu8(b, '"'); +} + +typedef struct { + i32 argc; + Str *argv; + Str cwd; +} Config; + +static void perfile(Output *out, Config conf, Str file, b32 first) +{ + printu8(out, first ? '[' : ','); + print(out, S("\n {")); + + print(out, S("\"file\": ")); + printq(out, file); + + print(out, S(", \"directory\": ")); + printq(out, conf.cwd); + + print(out, S(", \"arguments\": [")); + for (i32 i = 0; i < conf.argc; i++) { + if (i) { + print(out, S(", ")); + } + printq(out, conf.argv[i]); + } + print(out, S("]}")); +} + +static b32 match(Str arg) +{ + return endswith(arg, S(".c")) + || endswith(arg, S(".C")) + || endswith(arg, S(".cc")) + || endswith(arg, S(".cpp")) + || endswith(arg, S(".s")) + || endswith(arg, S(".S")); +} + +static Str cmd_suffix(Str s) +{ + iz cut = s.len; + for (; cut && s.data[cut-1]!='-'; cut--) {} + return (Str){s.data+cut, s.len-cut}; +} + +static u8 usage[] = +"usage: compdb-cc [CMD ...]\n" +"example: COMPDB_PATH=\"$PWD\"/compile_commands.json make -B CC=compdb-cc\n" +" When $COMPDB_PATH is set, updates the named file with build commands.\n"; + +static i32 compdb(Plt *plt, Arena *a, i32 argc, u8 **argv, Str cwd, b32 first) +{ + Output *out = newoutput(a, plt, 1); + Output *err = newoutput(a, plt, 2); + + Config conf = {}; + conf.argc = argc; + conf.argv = new(a, argc, Str); + conf.cwd = cwd; + for (i32 i = 0; i < argc; i++) { + conf.argv[i] = import(argv[i]); + if (i == 0) { + conf.argv[i] = cmd_suffix(conf.argv[i]); + } + } + + if (argc < 2) { + print(err, S(usage)); + if (conf.argc) { + print(err, S(" This program wraps the compiler named \"")); + print(err, conf.argv[0]); + print(err, S("\".\n")); + } + flush(err); + return STATUS_USAGE; + } + + i32 count = 0; + for (i32 i = 1; i < argc; i++) { + Str arg = conf.argv[i]; + if (match(arg)) { + count++; + perfile(out, conf, arg, first); + first = 0; + } + } + + if (count) { + print(out, S("\n]\n")); + } + flush(out); + return out->err ? STATUS_WRITE : STATUS_OK; +} + + +// Platform layer + +#if _WIN32 +typedef uint16_t char16_t; +typedef char16_t c16; + +enum { + CP_UTF8 = 65001, + OPEN_ALWAYS = 4, + FILE_ATTRIBUTE_NORMAL = 0x80, + FILE_SHARE_ALL = 7, + GENERIC_WRITE = 0x40000000, + FILE_END = 2, + LOCKFILE_EXCLUSIVE_LOCK = 2, +}; + +typedef struct { + i32 cb; + uz a, b, c; + i32 d, e, f, g, h, i, j, k; + i16 l, m; + uz n, o, p, q; +} si; + +typedef struct { + uz process; + uz thread; + i32 pid; + i32 tid; +} pi; + +#define W32(r) __declspec(dllimport) r __stdcall +W32(b32) CloseHandle(uz); +W32(c16 **) CommandLineToArgvW(c16 *, i32 *); +W32(uz) CreateFileW(c16 *, i32, i32, uz, i32, i32, uz); +W32(b32) CreateProcessW(c16*,c16*,void*,void*,b32,i32,c16*,c16*,si*,pi*); +W32(void) ExitProcess(i32); +W32(c16 *) GetCommandLineW(); +W32(i32) GetCurrentDirectoryW(i32, c16 *); +W32(i32) GetEnvironmentVariableW(c16 *, c16 *, i32); +W32(i32) GetExitCodeProcess(uz, i32 *); +W32(uz) GetStdHandle(i32); +W32(b32) LockFileEx(uz, i32, i32, i32, i32, uz[4]); +W32(b32) SetFilePointerEx(uz, i64, i64 *, i32); +W32(i32) WaitForSingleObject(uz, i32); +W32(i32) WideCharToMultiByte(i32, i32, c16 *, i32, u8 *, i32, uz, uz); +W32(b32) WriteFile(uz, u8 *, i32, i32 *, uz); + +struct Plt { + uz handle[3]; + uz err; +}; + +static b32 plt_write(Plt *plt, i32 fd, u8 *buf, i32 len) +{ + return WriteFile(plt->handle[fd], buf, len, &(i32){}, 0); +} + +static void plt_oom() +{ + ExitProcess(STATUS_OOM); + unreachable(); +} + +#define U(s) (S16){s, lenof(s)-1} +typedef struct { + c16 *data; + iz len; +} S16; + +static S16 wimport(c16 *s) +{ + S16 r = {}; + r.data = s; + for (; r.data[r.len]; r.len++) {} + return r; +} + +static S16 wclone(Arena *a, S16 s) +{ + S16 r = s; + r.data = new(a, s.len, c16); + for (iz i = 0; i < r.len; i++) { + r.data[i] = s.data[i]; + } + return r; +} + +static S16 wconcat(Arena *a, S16 head, S16 tail) +{ + if (a->beg != (u8 *)(head.data+head.len)) { + head = wclone(a, head); + } + head.len += wclone(a, tail).len; + return head; +} + +static S16 wslice(S16 s, iz beg, iz end) +{ + affirm(beg >= 0 && beg <= end && end <= s.len); + s.data += beg; + s.len = end - beg; + return s; +} + +typedef struct { + S16 cmd; + S16 args; +} Args; + +static Args split(S16 s) +{ + Args r = {}; + iz beg = 0; + iz cut = 0; + + if (s.len && s.data[0] == '"') { + cut = beg = 1; + for (; cut < s.len; cut++) { + if (s.data[cut] == '"') { + break; + } + } + r.cmd = wslice(s, beg, cut); + cut += cut < s.len; + r.args = wslice(s, cut, s.len); + + } else { + for (; cut < s.len; cut++) { + if (s.data[cut]==' ' || s.data[cut]=='\t') { + break; + } + } + r.cmd = wslice(s, beg, cut); + r.args = wslice(s, cut, s.len); + } + return r; +} + +static S16 wcmd_suffix(S16 s) +{ + iz cut = s.len; + for (; cut && s.data[cut-1]!='-'; cut--) {} + return wslice(s, cut, s.len); +} + +static Str encode(Arena *a, c16 *s) +{ + Str r = {}; + i32 len = WideCharToMultiByte(CP_UTF8, 0, s, -1, 0, 0, 0, 0); + if (len) { + r.data = new(a, len, u8); + r.len = len - 1; + WideCharToMultiByte(CP_UTF8, 0, s, -1, r.data, len, 0, 0); + } + return r; +} + +static Str workingdir(Arena *a) +{ + i32 len = GetCurrentDirectoryW(0, 0); + c16 *buf = new(a, len, c16); + GetCurrentDirectoryW(len, buf); + return encode(a, buf); +} + +static c16 *envvar(Arena *a, c16 *key) +{ + i32 len = GetEnvironmentVariableW(key, 0, 0); + if (!len) { + return 0; + } + + c16 *buf = new(a, len, c16); + GetEnvironmentVariableW(key, buf, len); + return buf; +} + +static i32 exec(uz err, c16 *cmd) +{ + if (!*cmd) { + return STATUS_OK; + } + + si si = {sizeof(si)}; + pi pi = {}; + if (!CreateProcessW(0, cmd, 0, 0, 1, 0, 0, 0, &si, &pi)) { + Str msg = S("compdb-cc: CreateProcessW() failed to exec command\n"); + WriteFile(err, msg.data, (i32)msg.len, &(i32){}, 0); + return STATUS_EXEC; + } + + WaitForSingleObject(pi.process, -1); + + i32 r; + GetExitCodeProcess(pi.process, &r); + return r; +} + +static i32 run(Arena *a) +{ + uz err = GetStdHandle(-12); + + c16 *cmd = GetCommandLineW(); + i32 argc = 0; + c16 **wargv = CommandLineToArgvW(cmd, &argc); + u8 **argv = new(a, argc+1, u8 *); + for (i32 i = 0; i < argc; i++) { + argv[i] = encode(a, wargv[i]).data; + } + + Plt *plt = new(a, 1, Plt); + plt->handle[1] = (uz)-1; + plt->handle[2] = err; + + if (argc < 2) { + return compdb(plt, a, argc, argv, (Str){}, 0); + } + + Args args = split(wimport(cmd)); + S16 cc = wcmd_suffix(args.cmd); + if (cc.len == args.cmd.len) { + Str msg = S( + "compdb-cc: could not determine actual compiler from name\n" + ); + WriteFile(err, msg.data, (i32)msg.len, &(i32){}, 0); + return STATUS_EXEC; + } + S16 realcmd = wconcat(a, cc, args.args); + realcmd = wconcat(a, realcmd, U(u"\0")); + + c16 *path = envvar(a, u"COMPDB_PATH"); + if (!path || !*path) { + return exec(err, realcmd.data); + } + + Str cwd = workingdir(a); + + i32 access = GENERIC_WRITE; + i32 share = FILE_SHARE_ALL; + i32 create = OPEN_ALWAYS; + i32 attr = FILE_ATTRIBUTE_NORMAL; + plt->handle[1] = CreateFileW(path, access, share, 0, create, attr, 0); + if (plt->handle[1] == (uz)-1) { + Str msg = S("compdb-cc: CreateFileW() failed to open database\n"); + WriteFile(err, msg.data, (i32)msg.len, &(i32){}, 0); + return STATUS_OPEN; + } + LockFileEx(plt->handle[1], LOCKFILE_EXCLUSIVE_LOCK, 0, -1, -1, (uz[4]){}); + + b32 first = !SetFilePointerEx(plt->handle[1], SEEK_SIZE, 0, FILE_END); + i32 status = compdb(plt, a, argc, argv, cwd, first); + if (status) { + return status; + } + + CloseHandle(plt->handle[1]); // unlock + return exec(err, realcmd.data); +} + +i32 __stdcall mainCRTStartup() +{ + static u8 mem[1<<21]; + Arena a = {mem, mem+lenof(mem)}; + i32 r = run(&a); + ExitProcess(r); + unreachable(); +} + + +#else // POSIX +#include +#include +#include +#include +#include +#include + +struct Plt { + int fds[3]; +}; + +static b32 plt_write(Plt *plt, i32 fd, u8 *buf, i32 len) +{ + return write(plt->fds[fd], buf, touz(len)) == len; +} + +static void plt_oom() +{ + exit(STATUS_OOM); +} + +static int exec(int argc, char **argv) +{ + if (argc < 1) { + return STATUS_OK; + } + execvp(argv[0], argv); + fprintf(stderr, "compdb-cc: execvp(%s): %s\n", argv[1], strerror(errno)); + return STATUS_EXEC; +} + +static Str workingdir(Arena *a) +{ + Str r = {}; + if (getcwd((char *)a->beg, touz(a->end-a->beg))) { + r = import(a->beg); + a->beg += r.len + 1; + } + return r; +} + +int main(int argc, char **argv) +{ + static u8 mem[1<<21]; + Arena a = {mem, mem+lenof(mem)}; + + Plt plt = {{0, 1, 2}}; + + if (argc < 2) { + return compdb(&plt, &a, argc, (u8 **)argv, (Str){}, 0); + } + + Str argv0 = import((u8 *)argv[0]); + Str cc = cmd_suffix(argv0); + if (cc.len == argv0.len) { + fprintf( + stderr, + "compdb-cc: could not determine actual compiler from name, %s\n", + argv[0] + ); + return STATUS_EXEC; + } + + char **new_argv = new(&a, argc+1, char *); + new_argv[0] = (char *)cc.data; + for (int i = 1; i < argc; i++) { + new_argv[i] = argv[i]; + } + + char *path = getenv("COMPDB_PATH"); + if (!path || !*path) { + return exec(argc, new_argv); + } + + Str cwd = workingdir(&a); + + plt.fds[1] = open(path, O_CREAT|O_RDWR, 0666); + if (plt.fds[1] == -1) { + fprintf(stderr, "compdb-cc: open(%s): %s\n", path, strerror(errno)); + return STATUS_OPEN; + } + flock(plt.fds[1], LOCK_EX); + + b32 first = lseek(plt.fds[1], SEEK_SIZE, SEEK_END) == -1; + i32 status = compdb(&plt, &a, argc, (u8 **)argv, cwd, first); + if (status) { + return status; + } + + close(plt.fds[1]); // unlock + return exec(argc, new_argv); +} +#endif