commit cbb7b541f827b20f0a97e30525e762e4bbdbb2c2
parent 00b46f1426b781000d7beda4b4812737c12eca2c
Author: Ryan Sepassi <rsepassi@gmail.com>
Date: Fri, 17 Jul 2026 17:53:18 -0700
exec: unify subprocess execution behind a config-struct KitExec
Introduce <kit/exec.h>: a neutral, config-struct-driven subprocess vtable
(KitExecOpts spawn + wait/poll/kill) shared by every libkit subsystem that
launches child processes. A single driver implementation in
driver/env/exec_{posix,windows}.c (driver_exec) replaces the prior
per-subsystem exec paths and their two fork/exec/pipe implementations.
- KitBuildExec/KitBuildProc/KitBuildKV become aliases of
KitExec/KitExecProc/KitExecKV. The coordinator's spawn + spawn_capture fold
into one config spawn (search_path, cwd, capture-to-buffer or capture-to-file);
wait gains an optional captured-stdout buffer. Call sites in runner/remote/
bundle and the build/tar drivers build a KitExecOpts.
- build_host_{posix,windows}.c drop their db_exec_* and set
out->exec = driver_exec(env).
- POSIX forks + execvp (PATH search) or execve (exact); Windows keeps
CreateProcessW (Unicode) and adds PATH search + pipe capture.
- exec_{posix,windows}.c compile when BUILD or MAKE.
Verified on POSIX: test-driver-build (274) and test-driver-build-coord (352)
pass; the KitExec side also builds and links in isolation (make disabled).
Windows exec compiles only in the VM lane.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Cu5rjrydtNn6LAEwLYqQeB
Diffstat:
13 files changed, 870 insertions(+), 586 deletions(-)
diff --git a/driver/cmd/build_coord.c b/driver/cmd/build_coord.c
@@ -1490,11 +1490,18 @@ static int build_cmd_workspace(int argc, char** argv, int cmd_index) {
tar_argv[4] = kit_slice_cstr(out);
tar_argv[5] = kit_slice_cstr(".");
proc = NULL;
- if (bh.exec.spawn(bh.exec.user, tar_argv, 6u, envv, 1u,
- kit_slice_cstr(root), &proc) != 0)
- continue;
+ {
+ KitExecOpts eo;
+ memset(&eo, 0, sizeof eo);
+ eo.argv = tar_argv;
+ eo.argc = 6u;
+ eo.env = envv;
+ eo.nenv = 1u;
+ eo.cwd = kit_slice_cstr(root);
+ if (bh.exec.spawn(bh.exec.user, &eo, &proc) != 0) continue;
+ }
ran = 1;
- if (bh.exec.wait(bh.exec.user, proc, &exit_code) != 0) {
+ if (bh.exec.wait(bh.exec.user, proc, &exit_code, NULL, NULL) != 0) {
driver_build_host_fini(&bh);
driver_env_fini(&env);
return 1;
diff --git a/driver/env.h b/driver/env.h
@@ -4,6 +4,7 @@
#include <kit/compile.h>
#include <kit/core.h>
#include <kit/dbg.h>
+#include <kit/exec.h>
#include <kit/jit.h>
#include <kit/os.h>
#include <stdarg.h>
@@ -333,6 +334,12 @@ const char* driver_getenv(const char* name);
* are libc-owned and remain valid until the process environment is mutated. */
const char* const* driver_environ(void);
+/* Build a KitExec (<kit/exec.h>) backed by this DriverEnv's heap: the shared
+ * config-struct subprocess interface used by the build coordinator and `kit
+ * make`. The returned value borrows `env` (its `user`); do not outlive env.
+ * Implemented in driver/env/exec_{posix,windows}.c. */
+KitExec driver_exec(DriverEnv* env);
+
/* Read all of stdin into a freshly-allocated buffer. On success returns 1
* and stores the buffer/size in out_data/out_size; the caller frees via
* driver_free(env, *out_data, *out_size). Returns 0 on read failure or
diff --git a/driver/env/build_host_posix.c b/driver/env/build_host_posix.c
@@ -19,10 +19,6 @@
#define DRIVER_BUILD_FRAME_MAX 65536u
-struct KitBuildProc {
- pid_t pid;
-};
-
struct KitBuildListener {
int fd;
char path[sizeof(((struct sockaddr_un*)0)->sun_path)];
@@ -145,169 +141,6 @@ static char* db_slice_dup(DriverEnv* env, KitSlice s) {
return out;
}
-static char* db_cwd_join(DriverEnv* env, KitSlice path) {
- char cwd[KIT_BUILD_PATH_MAX];
- char* out;
- size_t nc, np;
- if (!path.s || path.len == 0u) return NULL;
- if (path.s[0] == '/') return db_slice_dup(env, path);
- if (!getcwd(cwd, sizeof cwd)) return NULL;
- nc = strlen(cwd);
- np = path.len;
- out = (char*)driver_alloc(env, nc + 1u + np + 1u);
- if (!out) return NULL;
- memcpy(out, cwd, nc);
- out[nc] = '/';
- memcpy(out + nc + 1u, path.s, np);
- out[nc + 1u + np] = '\0';
- return out;
-}
-
-static void db_free_strv(DriverEnv* env, char** v, size_t n) {
- size_t i;
- if (!v) return;
- for (i = 0; i < n; ++i) {
- if (v[i]) driver_free(env, v[i], strlen(v[i]) + 1u);
- }
- driver_free(env, v, (n + 1u) * sizeof *v);
-}
-
-static int db_exec_spawn_common(void* user, const KitSlice* argv, size_t argc,
- const KitBuildKV* envv, size_t nenv,
- KitSlice cwd, KitSlice stdout_path,
- KitSlice stderr_path, int capture,
- KitBuildProc** out) {
- DriverBuildHost* bh = (DriverBuildHost*)user;
- char** av = NULL;
- char** ev = NULL;
- char* cwd_s = NULL;
- char* stdout_s = NULL;
- char* stderr_s = NULL;
- KitBuildProc* proc = NULL;
- pid_t pid;
- size_t i;
- if (!bh || !bh->env || !argv || argc == 0u || !out) return 1;
- *out = NULL;
- av = (char**)driver_alloc(bh->env, (argc + 1u) * sizeof *av);
- ev = (char**)driver_alloc(bh->env, (nenv + 1u) * sizeof *ev);
- if (!av || !ev) goto err;
- memset(av, 0, (argc + 1u) * sizeof *av);
- memset(ev, 0, (nenv + 1u) * sizeof *ev);
- for (i = 0; i < argc; ++i) {
- av[i] = i == 0u ? db_cwd_join(bh->env, argv[i])
- : db_slice_dup(bh->env, argv[i]);
- if (!av[i]) goto err;
- }
- for (i = 0; i < nenv; ++i) {
- size_t nk = envv[i].key.len, nv = envv[i].value.len;
- ev[i] = (char*)driver_alloc(bh->env, nk + 1u + nv + 1u);
- if (!ev[i]) goto err;
- memcpy(ev[i], envv[i].key.s, nk);
- ev[i][nk] = '=';
- memcpy(ev[i] + nk + 1u, envv[i].value.s, nv);
- ev[i][nk + 1u + nv] = '\0';
- }
- cwd_s = db_slice_dup(bh->env, cwd);
- if (capture) {
- stdout_s = db_slice_dup(bh->env, stdout_path);
- stderr_s = db_slice_dup(bh->env, stderr_path);
- }
- proc = (KitBuildProc*)driver_alloc_zeroed(bh->env, sizeof *proc);
- if (!cwd_s || !proc || (capture && (!stdout_s || !stderr_s))) goto err;
- pid = fork();
- if (pid < 0) goto err;
- if (pid == 0) {
- if (capture) {
- int out_fd = open(stdout_s, O_WRONLY | O_CREAT | O_TRUNC, 0666);
- int err_fd = open(stderr_s, O_WRONLY | O_CREAT | O_TRUNC, 0666);
- if (out_fd < 0 || err_fd < 0) _exit(127);
- if (dup2(out_fd, STDOUT_FILENO) < 0 ||
- dup2(err_fd, STDERR_FILENO) < 0)
- _exit(127);
- close(out_fd);
- close(err_fd);
- }
- if (chdir(cwd_s) != 0) _exit(127);
- execve(av[0], av, ev);
- _exit(127);
- }
- proc->pid = pid;
- *out = proc;
- db_free_strv(bh->env, av, argc);
- db_free_strv(bh->env, ev, nenv);
- driver_free(bh->env, cwd_s, strlen(cwd_s) + 1u);
- if (stdout_s) driver_free(bh->env, stdout_s, strlen(stdout_s) + 1u);
- if (stderr_s) driver_free(bh->env, stderr_s, strlen(stderr_s) + 1u);
- return 0;
-
-err:
- db_free_strv(bh->env, av, argc);
- db_free_strv(bh->env, ev, nenv);
- if (cwd_s) driver_free(bh->env, cwd_s, strlen(cwd_s) + 1u);
- if (stdout_s) driver_free(bh->env, stdout_s, strlen(stdout_s) + 1u);
- if (stderr_s) driver_free(bh->env, stderr_s, strlen(stderr_s) + 1u);
- if (proc) driver_free(bh->env, proc, sizeof *proc);
- return 1;
-}
-
-static int db_exec_spawn(void* user, const KitSlice* argv, size_t argc,
- const KitBuildKV* envv, size_t nenv, KitSlice cwd,
- KitBuildProc** out) {
- return db_exec_spawn_common(user, argv, argc, envv, nenv, cwd,
- KIT_SLICE_NULL, KIT_SLICE_NULL, 0, out);
-}
-
-static int db_exec_spawn_capture(void* user, const KitSlice* argv, size_t argc,
- const KitBuildKV* envv, size_t nenv,
- KitSlice cwd, KitSlice stdout_path,
- KitSlice stderr_path, KitBuildProc** out) {
- return db_exec_spawn_common(user, argv, argc, envv, nenv, cwd, stdout_path,
- stderr_path, 1, out);
-}
-
-static int db_exec_wait(void* user, KitBuildProc* proc, int* exit_code) {
- DriverBuildHost* bh = (DriverBuildHost*)user;
- int st = 0;
- if (!bh || !proc || !exit_code) return 1;
- while (waitpid(proc->pid, &st, 0) < 0) {
- if (errno != EINTR) return 1;
- }
- if (WIFEXITED(st)) {
- *exit_code = WEXITSTATUS(st);
- } else {
- *exit_code = 128;
- }
- driver_free(bh->env, proc, sizeof *proc);
- return 0;
-}
-
-static int db_exec_poll(void* user, KitBuildProc* proc, int* done,
- int* exit_code) {
- DriverBuildHost* bh = (DriverBuildHost*)user;
- int st = 0;
- pid_t r;
- if (!bh || !proc || !done || !exit_code) return 1;
- *done = 0;
- do {
- r = waitpid(proc->pid, &st, WNOHANG);
- } while (r < 0 && errno == EINTR);
- if (r < 0) return 1;
- if (r == 0) return 0;
- *done = 1;
- if (WIFEXITED(st)) {
- *exit_code = WEXITSTATUS(st);
- } else {
- *exit_code = 128;
- }
- driver_free(bh->env, proc, sizeof *proc);
- return 0;
-}
-
-static void db_exec_kill(void* user, KitBuildProc* proc) {
- (void)user;
- if (proc) kill(proc->pid, SIGTERM);
-}
-
static int db_full_write(int fd, const uint8_t* p, size_t n) {
size_t off = 0;
while (off < n) {
@@ -531,12 +364,7 @@ int driver_build_host_init(DriverBuildHost* out, DriverEnv* env) {
out->store_io.sync_path = db_store_sync_path;
out->store_io.list_dir = db_store_list_dir;
out->store_io.user = out;
- out->exec.spawn = db_exec_spawn;
- out->exec.spawn_capture = db_exec_spawn_capture;
- out->exec.wait = db_exec_wait;
- out->exec.poll = db_exec_poll;
- out->exec.kill = db_exec_kill;
- out->exec.user = out;
+ out->exec = driver_exec(env);
out->transport.listen = db_transport_listen;
out->transport.accept = db_transport_accept;
out->transport.try_accept = db_transport_try_accept;
diff --git a/driver/env/build_host_windows.c b/driver/env/build_host_windows.c
@@ -18,10 +18,6 @@
#define DRIVER_BUILD_FRAME_MAX 65536u
#define DRIVER_BUILD_PIPE_TIMEOUT_MS 10u
-struct KitBuildProc {
- HANDLE process;
-};
-
struct KitBuildListener {
char name[KIT_BUILD_PATH_MAX];
HANDLE pipe;
@@ -87,42 +83,6 @@ static int db_is_abs_path(const char* path) {
path[1] == ':';
}
-static char* db_cwd_join(DriverEnv* env, KitSlice path) {
- DWORD need;
- wchar_t* wpath;
- wchar_t* wbuf = NULL;
- char* p;
- char* out;
- size_t n;
- if (!path.s || path.len == 0u) return NULL;
- p = db_slice_dup(env, path);
- if (!p) return NULL;
- if (db_is_abs_path(p)) return p;
- wpath = db_widen(p);
- driver_free(env, p, path.len + 1u);
- if (!wpath) return NULL;
- need = GetFullPathNameW(wpath, 0, NULL, NULL);
- if (need == 0) {
- free(wpath);
- return NULL;
- }
- wbuf = (wchar_t*)malloc((size_t)need * sizeof(*wbuf));
- if (!wbuf || GetFullPathNameW(wpath, need, wbuf, NULL) == 0) {
- free(wbuf);
- free(wpath);
- return NULL;
- }
- free(wpath);
- p = db_narrow(wbuf);
- free(wbuf);
- if (!p) return NULL;
- n = strlen(p) + 1u;
- out = (char*)driver_alloc(env, n);
- if (out) memcpy(out, p, n);
- free(p);
- return out;
-}
-
static int db_store_rename(void* user, KitSlice from, KitSlice to) {
char f[KIT_BUILD_PATH_MAX], t[KIT_BUILD_PATH_MAX];
wchar_t *wf, *wt;
@@ -259,312 +219,6 @@ static int db_store_list_dir(void* user, KitSlice path, KitBuildDirFn cb,
return last == ERROR_NO_MORE_FILES ? 0 : 1;
}
-static void db_free_strv(DriverEnv* env, char** v, size_t n) {
- size_t i;
- if (!v) return;
- for (i = 0; i < n; ++i)
- if (v[i]) driver_free(env, v[i], strlen(v[i]) + 1u);
- driver_free(env, v, (n + 1u) * sizeof *v);
-}
-
-static wchar_t** db_widen_strv(char** v, size_t n) {
- wchar_t** out;
- size_t i;
- out = (wchar_t**)calloc(n + 1u, sizeof(*out));
- if (!out) return NULL;
- for (i = 0; i < n; ++i) {
- out[i] = db_widen(v[i]);
- if (!out[i]) {
- while (i > 0u) free(out[--i]);
- free(out);
- return NULL;
- }
- }
- return out;
-}
-
-static void db_free_wstrv(wchar_t** v, size_t n) {
- size_t i;
- if (!v) return;
- for (i = 0; i < n; ++i) free(v[i]);
- free(v);
-}
-
-static int db_arg_needs_quote(const wchar_t* s) {
- if (!s || !s[0]) return 1;
- for (; *s; ++s)
- if (*s == L' ' || *s == L'\t' || *s == L'"') return 1;
- return 0;
-}
-
-static size_t db_arg_quoted_len(const wchar_t* s) {
- size_t len = 0;
- size_t slash = 0;
- int quote = db_arg_needs_quote(s);
- if (!quote) return (size_t)lstrlenW(s);
- len = 2u;
- for (; *s; ++s) {
- if (*s == L'\\') {
- ++slash;
- ++len;
- } else if (*s == L'"') {
- len += slash + 2u;
- slash = 0;
- } else {
- slash = 0;
- ++len;
- }
- }
- return len + slash;
-}
-
-static wchar_t* db_arg_quote_into(wchar_t* out, const wchar_t* s) {
- size_t slash = 0;
- int quote = db_arg_needs_quote(s);
- if (!quote) {
- while (*s) *out++ = *s++;
- return out;
- }
- *out++ = L'"';
- for (; *s; ++s) {
- if (*s == L'\\') {
- *out++ = *s;
- ++slash;
- } else if (*s == L'"') {
- while (slash--) *out++ = L'\\';
- *out++ = L'\\';
- *out++ = L'"';
- slash = 0;
- } else {
- *out++ = *s;
- slash = 0;
- }
- }
- while (slash--) *out++ = L'\\';
- *out++ = L'"';
- return out;
-}
-
-static wchar_t* db_command_line(wchar_t** argv, size_t argc) {
- size_t i, len = 1u;
- wchar_t* out;
- wchar_t* p;
- for (i = 0; i < argc; ++i)
- len += db_arg_quoted_len(argv[i]) + (i ? 1u : 0u);
- out = (wchar_t*)malloc(len * sizeof(*out));
- if (!out) return NULL;
- p = out;
- for (i = 0; i < argc; ++i) {
- if (i) *p++ = L' ';
- p = db_arg_quote_into(p, argv[i]);
- }
- *p = L'\0';
- return out;
-}
-
-static int db_wenv_cmp(const void* a, const void* b) {
- const wchar_t* const* aa = (const wchar_t* const*)a;
- const wchar_t* const* bb = (const wchar_t* const*)b;
- return lstrcmpiW(*aa, *bb);
-}
-
-static wchar_t* db_env_block(wchar_t** envv, size_t nenv) {
- size_t i, total = 2u, off = 0;
- wchar_t* block;
- qsort(envv, nenv, sizeof(*envv), db_wenv_cmp);
- for (i = 0; i < nenv; ++i) total += (size_t)lstrlenW(envv[i]) + 1u;
- block = (wchar_t*)malloc(total * sizeof(*block));
- if (!block) return NULL;
- for (i = 0; i < nenv; ++i) {
- size_t n = (size_t)lstrlenW(envv[i]) + 1u;
- memcpy(block + off, envv[i], n * sizeof(*block));
- off += n;
- }
- block[off++] = L'\0';
- block[off] = L'\0';
- return block;
-}
-
-static int db_exec_spawn_common(void* user, const KitSlice* argv, size_t argc,
- const KitBuildKV* envv, size_t nenv,
- KitSlice cwd, KitSlice stdout_path,
- KitSlice stderr_path, int capture,
- KitBuildProc** out) {
- DriverBuildHost* bh = (DriverBuildHost*)user;
- char** av = NULL;
- char** ev = NULL;
- wchar_t** wav = NULL;
- wchar_t** wev = NULL;
- wchar_t* cmdline = NULL;
- wchar_t* envblock = NULL;
- char* cwd_s = NULL;
- wchar_t* wcwd = NULL;
- KitBuildProc* proc = NULL;
- HANDLE stdout_h = INVALID_HANDLE_VALUE;
- HANDLE stderr_h = INVALID_HANDLE_VALUE;
- HANDLE stdin_h = INVALID_HANDLE_VALUE;
- STARTUPINFOW si;
- PROCESS_INFORMATION pi;
- SECURITY_ATTRIBUTES sa;
- DWORD flags = CREATE_UNICODE_ENVIRONMENT;
- size_t i;
- if (!bh || !bh->env || !argv || argc == 0u || !out) return 1;
- *out = NULL;
- av = (char**)driver_alloc(bh->env, (argc + 1u) * sizeof *av);
- ev = (char**)driver_alloc(bh->env, (nenv + 1u) * sizeof *ev);
- if (!av || !ev) goto err;
- memset(av, 0, (argc + 1u) * sizeof *av);
- memset(ev, 0, (nenv + 1u) * sizeof *ev);
- for (i = 0; i < argc; ++i) {
- av[i] = i == 0u ? db_cwd_join(bh->env, argv[i])
- : db_slice_dup(bh->env, argv[i]);
- if (!av[i]) goto err;
- }
- for (i = 0; i < nenv; ++i) {
- size_t nk = envv[i].key.len, nv = envv[i].value.len;
- ev[i] = (char*)driver_alloc(bh->env, nk + 1u + nv + 1u);
- if (!ev[i]) goto err;
- memcpy(ev[i], envv[i].key.s, nk);
- ev[i][nk] = '=';
- memcpy(ev[i] + nk + 1u, envv[i].value.s, nv);
- ev[i][nk + 1u + nv] = '\0';
- }
- wav = db_widen_strv(av, argc);
- wev = db_widen_strv(ev, nenv);
- if (!wav || !wev) goto err;
- cmdline = db_command_line(wav, argc);
- envblock = db_env_block(wev, nenv);
- cwd_s = db_slice_dup(bh->env, cwd);
- wcwd = db_widen(cwd_s);
- proc = (KitBuildProc*)driver_alloc_zeroed(bh->env, sizeof *proc);
- if (!cmdline || !envblock || !cwd_s || !wcwd || !proc) goto err;
-
- memset(&si, 0, sizeof si);
- memset(&pi, 0, sizeof pi);
- si.cb = sizeof si;
- if (capture) {
- char* stdout_s = db_slice_dup(bh->env, stdout_path);
- char* stderr_s = db_slice_dup(bh->env, stderr_path);
- wchar_t* wout = stdout_s ? db_widen(stdout_s) : NULL;
- wchar_t* werr = stderr_s ? db_widen(stderr_s) : NULL;
- wchar_t* wnull = db_widen("NUL");
- if (stdout_s) driver_free(bh->env, stdout_s, strlen(stdout_s) + 1u);
- if (stderr_s) driver_free(bh->env, stderr_s, strlen(stderr_s) + 1u);
- if (!wout || !werr || !wnull) {
- free(wout);
- free(werr);
- free(wnull);
- goto err;
- }
- memset(&sa, 0, sizeof sa);
- sa.nLength = sizeof sa;
- sa.bInheritHandle = TRUE;
- stdout_h = CreateFileW(wout, GENERIC_WRITE, FILE_SHARE_READ, &sa,
- CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL);
- stderr_h = CreateFileW(werr, GENERIC_WRITE, FILE_SHARE_READ, &sa,
- CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL);
- stdin_h = CreateFileW(wnull, GENERIC_READ, FILE_SHARE_READ, &sa,
- OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
- free(wout);
- free(werr);
- free(wnull);
- if (stdout_h == INVALID_HANDLE_VALUE || stderr_h == INVALID_HANDLE_VALUE ||
- stdin_h == INVALID_HANDLE_VALUE)
- goto err;
- si.dwFlags = STARTF_USESTDHANDLES;
- si.hStdInput = stdin_h;
- si.hStdOutput = stdout_h;
- si.hStdError = stderr_h;
- }
- if (!CreateProcessW(wav[0], cmdline, NULL, NULL, capture ? TRUE : FALSE,
- flags, envblock, wcwd, &si, &pi))
- goto err;
- CloseHandle(pi.hThread);
- if (stdout_h != INVALID_HANDLE_VALUE) CloseHandle(stdout_h);
- if (stderr_h != INVALID_HANDLE_VALUE) CloseHandle(stderr_h);
- if (stdin_h != INVALID_HANDLE_VALUE) CloseHandle(stdin_h);
- proc->process = pi.hProcess;
- *out = proc;
- db_free_wstrv(wav, argc);
- db_free_wstrv(wev, nenv);
- db_free_strv(bh->env, av, argc);
- db_free_strv(bh->env, ev, nenv);
- driver_free(bh->env, cwd_s, strlen(cwd_s) + 1u);
- free(wcwd);
- free(cmdline);
- free(envblock);
- return 0;
-
-err:
- if (stdout_h != INVALID_HANDLE_VALUE) CloseHandle(stdout_h);
- if (stderr_h != INVALID_HANDLE_VALUE) CloseHandle(stderr_h);
- if (stdin_h != INVALID_HANDLE_VALUE) CloseHandle(stdin_h);
- if (proc) driver_free(bh->env, proc, sizeof *proc);
- if (cwd_s) driver_free(bh->env, cwd_s, strlen(cwd_s) + 1u);
- free(wcwd);
- free(cmdline);
- free(envblock);
- db_free_wstrv(wav, argc);
- db_free_wstrv(wev, nenv);
- db_free_strv(bh->env, av, argc);
- db_free_strv(bh->env, ev, nenv);
- return 1;
-}
-
-static int db_exec_spawn(void* user, const KitSlice* argv, size_t argc,
- const KitBuildKV* envv, size_t nenv, KitSlice cwd,
- KitBuildProc** out) {
- return db_exec_spawn_common(user, argv, argc, envv, nenv, cwd,
- KIT_SLICE_NULL, KIT_SLICE_NULL, 0, out);
-}
-
-static int db_exec_spawn_capture(void* user, const KitSlice* argv, size_t argc,
- const KitBuildKV* envv, size_t nenv,
- KitSlice cwd, KitSlice stdout_path,
- KitSlice stderr_path, KitBuildProc** out) {
- return db_exec_spawn_common(user, argv, argc, envv, nenv, cwd, stdout_path,
- stderr_path, 1, out);
-}
-
-static int db_proc_exit_code(HANDLE h, int* exit_code) {
- DWORD code;
- if (!GetExitCodeProcess(h, &code)) return 1;
- *exit_code = code > 255u ? 128 : (int)code;
- return 0;
-}
-
-static int db_exec_wait(void* user, KitBuildProc* proc, int* exit_code) {
- DriverBuildHost* bh = (DriverBuildHost*)user;
- if (!bh || !proc || !exit_code) return 1;
- if (WaitForSingleObject(proc->process, INFINITE) != WAIT_OBJECT_0)
- return 1;
- if (db_proc_exit_code(proc->process, exit_code) != 0) return 1;
- CloseHandle(proc->process);
- driver_free(bh->env, proc, sizeof *proc);
- return 0;
-}
-
-static int db_exec_poll(void* user, KitBuildProc* proc, int* done,
- int* exit_code) {
- DriverBuildHost* bh = (DriverBuildHost*)user;
- DWORD r;
- if (!bh || !proc || !done || !exit_code) return 1;
- *done = 0;
- r = WaitForSingleObject(proc->process, 0);
- if (r == WAIT_TIMEOUT) return 0;
- if (r != WAIT_OBJECT_0) return 1;
- if (db_proc_exit_code(proc->process, exit_code) != 0) return 1;
- CloseHandle(proc->process);
- driver_free(bh->env, proc, sizeof *proc);
- *done = 1;
- return 0;
-}
-
-static void db_exec_kill(void* user, KitBuildProc* proc) {
- (void)user;
- if (proc) TerminateProcess(proc->process, 128u);
-}
-
static int db_pipe_start(KitBuildListener* l) {
wchar_t* wname;
BOOL ok;
@@ -819,12 +473,7 @@ int driver_build_host_init(DriverBuildHost* out, DriverEnv* env) {
out->store_io.sync_path = db_store_sync_path;
out->store_io.list_dir = db_store_list_dir;
out->store_io.user = out;
- out->exec.spawn = db_exec_spawn;
- out->exec.spawn_capture = db_exec_spawn_capture;
- out->exec.wait = db_exec_wait;
- out->exec.poll = db_exec_poll;
- out->exec.kill = db_exec_kill;
- out->exec.user = out;
+ out->exec = driver_exec(env);
out->transport.listen = db_transport_listen;
out->transport.accept = db_transport_accept;
out->transport.try_accept = db_transport_try_accept;
diff --git a/driver/env/exec_posix.c b/driver/env/exec_posix.c
@@ -0,0 +1,283 @@
+/*
+ * POSIX implementation of the shared KitExec subprocess interface
+ * (<kit/exec.h>). One config-struct spawn serves both the build coordinator
+ * (hermetic: exact argv[0], explicit env, mandatory cwd, capture to files) and
+ * `kit make` (PATH-searched shell, ambient-style env, optional cwd, capture to
+ * a buffer). The user pointer is a DriverEnv* (for its heap).
+ */
+#include <errno.h>
+#include <fcntl.h>
+#include <signal.h>
+#include <stdint.h>
+#include <stdio.h>
+#include <string.h>
+#include <sys/wait.h>
+#include <unistd.h>
+
+#include <kit/exec.h>
+
+#include "env.h"
+
+struct KitExecProc {
+ pid_t pid;
+ int read_fd; /* stdout capture pipe read end, or -1 */
+};
+
+static char* ex_slice_dup(DriverEnv* env, KitSlice s) {
+ char* out = (char*)driver_alloc(env, s.len + 1u);
+ if (!out) return NULL;
+ if (s.len) memcpy(out, s.s, s.len);
+ out[s.len] = '\0';
+ return out;
+}
+
+/* Join a relative argv[0] to the current directory (exact/execve path). */
+static char* ex_cwd_join(DriverEnv* env, KitSlice path) {
+ char cwd[4096];
+ char* out;
+ size_t nc, np;
+ if (!path.s || path.len == 0u) return NULL;
+ if (path.s[0] == '/') return ex_slice_dup(env, path);
+ if (!getcwd(cwd, sizeof cwd)) return NULL;
+ nc = strlen(cwd);
+ np = path.len;
+ out = (char*)driver_alloc(env, nc + 1u + np + 1u);
+ if (!out) return NULL;
+ memcpy(out, cwd, nc);
+ out[nc] = '/';
+ memcpy(out + nc + 1u, path.s, np);
+ out[nc + 1u + np] = '\0';
+ return out;
+}
+
+static void ex_free_strv(DriverEnv* env, char** v, size_t n) {
+ size_t i;
+ if (!v) return;
+ for (i = 0; i < n; ++i)
+ if (v[i]) driver_free(env, v[i], strlen(v[i]) + 1u);
+ driver_free(env, v, (n + 1u) * sizeof *v);
+}
+
+static int ex_reap(pid_t pid, int* exit_code) {
+ int st;
+ for (;;) {
+ if (waitpid(pid, &st, 0) < 0) {
+ if (errno == EINTR) continue;
+ return 1;
+ }
+ break;
+ }
+ if (WIFEXITED(st))
+ *exit_code = WEXITSTATUS(st);
+ else if (WIFSIGNALED(st))
+ *exit_code = -WTERMSIG(st);
+ else
+ *exit_code = 1;
+ return 0;
+}
+
+static int ex_spawn(void* user, const KitExecOpts* opts, KitExecProc** out) {
+ DriverEnv* env = (DriverEnv*)user;
+ char** av = NULL;
+ char** ev = NULL;
+ char* cwd_s = NULL;
+ char* stdout_s = NULL;
+ char* stderr_s = NULL;
+ KitExecProc* proc = NULL;
+ int pipefd[2] = {-1, -1};
+ pid_t pid;
+ size_t i;
+
+ if (!env || !opts || !opts->argv || opts->argc == 0u || !out) return 1;
+ *out = NULL;
+
+ av = (char**)driver_alloc(env, (opts->argc + 1u) * sizeof *av);
+ ev = (char**)driver_alloc(env, (opts->nenv + 1u) * sizeof *ev);
+ if (!av || !ev) goto err;
+ memset(av, 0, (opts->argc + 1u) * sizeof *av);
+ memset(ev, 0, (opts->nenv + 1u) * sizeof *ev);
+ for (i = 0; i < opts->argc; ++i) {
+ av[i] = (i == 0u && !opts->search_path) ? ex_cwd_join(env, opts->argv[i])
+ : ex_slice_dup(env, opts->argv[i]);
+ if (!av[i]) goto err;
+ }
+ for (i = 0; i < opts->nenv; ++i) {
+ size_t nk = opts->env[i].key.len, nv = opts->env[i].value.len;
+ ev[i] = (char*)driver_alloc(env, nk + 1u + nv + 1u);
+ if (!ev[i]) goto err;
+ memcpy(ev[i], opts->env[i].key.s, nk);
+ ev[i][nk] = '=';
+ memcpy(ev[i] + nk + 1u, opts->env[i].value.s, nv);
+ ev[i][nk + 1u + nv] = '\0';
+ }
+ if (opts->cwd.len) {
+ cwd_s = ex_slice_dup(env, opts->cwd);
+ if (!cwd_s) goto err;
+ }
+ if (opts->stdout_path.len) {
+ stdout_s = ex_slice_dup(env, opts->stdout_path);
+ if (!stdout_s) goto err;
+ }
+ if (opts->stderr_path.len) {
+ stderr_s = ex_slice_dup(env, opts->stderr_path);
+ if (!stderr_s) goto err;
+ }
+ proc = (KitExecProc*)driver_alloc_zeroed(env, sizeof *proc);
+ if (!proc) goto err;
+ proc->read_fd = -1;
+
+ if (opts->capture_stdout) {
+ if (pipe(pipefd) != 0) goto err;
+ } else {
+ /* Flush the parent's buffered stdout so a recipe echo precedes the child's
+ * own output on the shared fd. */
+ fflush(stdout);
+ }
+
+ pid = fork();
+ if (pid < 0) goto err;
+ if (pid == 0) {
+ extern char** environ;
+ if (opts->capture_stdout) {
+ close(pipefd[0]);
+ if (dup2(pipefd[1], STDOUT_FILENO) < 0) _exit(127);
+ close(pipefd[1]);
+ } else if (stdout_s) {
+ int fd = open(stdout_s, O_WRONLY | O_CREAT | O_TRUNC, 0666);
+ if (fd < 0 || dup2(fd, STDOUT_FILENO) < 0) _exit(127);
+ close(fd);
+ }
+ if (stderr_s) {
+ int fd = open(stderr_s, O_WRONLY | O_CREAT | O_TRUNC, 0666);
+ if (fd < 0 || dup2(fd, STDERR_FILENO) < 0) _exit(127);
+ close(fd);
+ }
+ if (cwd_s && chdir(cwd_s) != 0) _exit(127);
+ if (opts->search_path) {
+ environ = ev; /* execvp draws PATH and the child env from here */
+ execvp(av[0], av);
+ } else {
+ execve(av[0], av, ev);
+ }
+ _exit(127);
+ }
+ proc->pid = pid;
+ if (opts->capture_stdout) {
+ close(pipefd[1]);
+ proc->read_fd = pipefd[0];
+ }
+ *out = proc;
+ ex_free_strv(env, av, opts->argc);
+ ex_free_strv(env, ev, opts->nenv);
+ if (cwd_s) driver_free(env, cwd_s, strlen(cwd_s) + 1u);
+ if (stdout_s) driver_free(env, stdout_s, strlen(stdout_s) + 1u);
+ if (stderr_s) driver_free(env, stderr_s, strlen(stderr_s) + 1u);
+ return 0;
+
+err:
+ if (pipefd[0] >= 0) close(pipefd[0]);
+ if (pipefd[1] >= 0) close(pipefd[1]);
+ ex_free_strv(env, av, opts ? opts->argc : 0u);
+ ex_free_strv(env, ev, opts ? opts->nenv : 0u);
+ if (cwd_s) driver_free(env, cwd_s, strlen(cwd_s) + 1u);
+ if (stdout_s) driver_free(env, stdout_s, strlen(stdout_s) + 1u);
+ if (stderr_s) driver_free(env, stderr_s, strlen(stderr_s) + 1u);
+ if (proc) driver_free(env, proc, sizeof *proc);
+ return 1;
+}
+
+static int ex_wait(void* user, KitExecProc* proc, int* exit_code, uint8_t** out,
+ size_t* out_len) {
+ DriverEnv* env = (DriverEnv*)user;
+ uint8_t* buf = NULL;
+ size_t len = 0;
+ size_t cap = 0;
+ int oom = 0;
+ int rc;
+
+ if (!env || !proc || !exit_code) return 1;
+ if (out) *out = NULL;
+ if (out_len) *out_len = 0;
+
+ if (proc->read_fd >= 0) {
+ for (;;) {
+ uint8_t tmp[4096];
+ ssize_t n = read(proc->read_fd, tmp, sizeof tmp);
+ if (n < 0) {
+ if (errno == EINTR) continue;
+ break;
+ }
+ if (n == 0) break;
+ /* Keep draining after an allocation failure so the child never blocks. */
+ if (!oom && len + (size_t)n > cap) {
+ size_t ncap = cap ? cap : 8192;
+ uint8_t* nb;
+ while (ncap < len + (size_t)n) ncap *= 2;
+ nb = env->heap->realloc(env->heap, buf, cap, ncap, 8);
+ if (!nb) {
+ env->heap->free(env->heap, buf, cap);
+ buf = NULL;
+ cap = 0;
+ len = 0;
+ oom = 1;
+ } else {
+ buf = nb;
+ cap = ncap;
+ }
+ }
+ if (!oom) {
+ memcpy(buf + len, tmp, (size_t)n);
+ len += (size_t)n;
+ }
+ }
+ close(proc->read_fd);
+ }
+
+ rc = ex_reap(proc->pid, exit_code);
+ driver_free(env, proc, sizeof *proc);
+ if (out) {
+ *out = buf;
+ if (out_len) *out_len = len;
+ } else if (buf) {
+ env->heap->free(env->heap, buf, cap);
+ }
+ return rc;
+}
+
+static int ex_poll(void* user, KitExecProc* proc, int* done, int* exit_code) {
+ DriverEnv* env = (DriverEnv*)user;
+ int st;
+ pid_t r;
+ if (!env || !proc || !done || !exit_code) return 1;
+ *done = 0;
+ do {
+ r = waitpid(proc->pid, &st, WNOHANG);
+ } while (r < 0 && errno == EINTR);
+ if (r < 0) return 1;
+ if (r == 0) return 0;
+ *done = 1;
+ if (WIFEXITED(st))
+ *exit_code = WEXITSTATUS(st);
+ else if (WIFSIGNALED(st))
+ *exit_code = -WTERMSIG(st);
+ else
+ *exit_code = 1;
+ if (proc->read_fd >= 0) close(proc->read_fd);
+ driver_free(env, proc, sizeof *proc);
+ return 0;
+}
+
+static void ex_kill(void* user, KitExecProc* proc) {
+ (void)user;
+ if (proc) kill(proc->pid, SIGTERM);
+}
+
+KitExec driver_exec(DriverEnv* env) {
+ KitExec ex;
+ ex.spawn = ex_spawn;
+ ex.wait = ex_wait;
+ ex.poll = ex_poll;
+ ex.kill = ex_kill;
+ ex.user = env;
+ return ex;
+}
diff --git a/driver/env/exec_windows.c b/driver/env/exec_windows.c
@@ -0,0 +1,441 @@
+/*
+ * Windows implementation of the shared KitExec subprocess interface
+ * (<kit/exec.h>), via CreateProcessW so Unicode argv/env/paths are preserved.
+ * One config-struct spawn serves both the build coordinator (hermetic: exact
+ * argv[0], explicit env, mandatory cwd, capture to files) and `kit make`
+ * (PATH-searched shell, optional cwd, capture to a buffer). The user pointer is
+ * a DriverEnv* (for its heap).
+ *
+ * NOTE: this path compiles for Windows but is exercised only in the Windows VM
+ * test lane; primary development/testing happens on POSIX.
+ */
+#ifndef WIN32_LEAN_AND_MEAN
+#define WIN32_LEAN_AND_MEAN
+#endif
+#ifndef _WIN32_WINNT
+#define _WIN32_WINNT 0x0601
+#endif
+// clang-format off
+#include <windows.h>
+// clang-format on
+
+#include <stdint.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+#include <kit/exec.h>
+
+#include "env.h"
+
+struct KitExecProc {
+ HANDLE process;
+ HANDLE read_pipe; /* stdout capture pipe read end, or INVALID_HANDLE_VALUE */
+};
+
+static wchar_t* ex_widen(const char* utf8) {
+ int need;
+ wchar_t* w;
+ if (!utf8) return NULL;
+ need = MultiByteToWideChar(CP_UTF8, 0, utf8, -1, NULL, 0);
+ if (need <= 0) return NULL;
+ w = (wchar_t*)malloc((size_t)need * sizeof(*w));
+ if (!w) return NULL;
+ if (MultiByteToWideChar(CP_UTF8, 0, utf8, -1, w, need) <= 0) {
+ free(w);
+ return NULL;
+ }
+ return w;
+}
+
+static char* ex_slice_dup(DriverEnv* env, KitSlice s) {
+ char* out = (char*)driver_alloc(env, s.len + 1u);
+ if (!out) return NULL;
+ if (s.len) memcpy(out, s.s, s.len);
+ out[s.len] = '\0';
+ return out;
+}
+
+static char* ex_cwd_join(DriverEnv* env, KitSlice path) {
+ char cwd[4096];
+ char* out;
+ size_t nc, np;
+ if (!path.s || path.len == 0u) return NULL;
+ if (path.s[0] == '/' || path.s[0] == '\\' ||
+ (path.len >= 2u && path.s[1] == ':'))
+ return ex_slice_dup(env, path);
+ if (!GetCurrentDirectoryA(sizeof cwd, cwd)) return NULL;
+ nc = strlen(cwd);
+ np = path.len;
+ out = (char*)driver_alloc(env, nc + 1u + np + 1u);
+ if (!out) return NULL;
+ memcpy(out, cwd, nc);
+ out[nc] = '\\';
+ memcpy(out + nc + 1u, path.s, np);
+ out[nc + 1u + np] = '\0';
+ return out;
+}
+
+static void ex_free_strv(DriverEnv* env, char** v, size_t n) {
+ size_t i;
+ if (!v) return;
+ for (i = 0; i < n; ++i)
+ if (v[i]) driver_free(env, v[i], strlen(v[i]) + 1u);
+ driver_free(env, v, (n + 1u) * sizeof *v);
+}
+
+static wchar_t** ex_widen_strv(char** v, size_t n) {
+ wchar_t** out = (wchar_t**)calloc(n + 1u, sizeof(*out));
+ size_t i;
+ if (!out) return NULL;
+ for (i = 0; i < n; ++i) {
+ out[i] = ex_widen(v[i]);
+ if (!out[i]) {
+ while (i > 0u) free(out[--i]);
+ free(out);
+ return NULL;
+ }
+ }
+ return out;
+}
+
+static void ex_free_wstrv(wchar_t** v, size_t n) {
+ size_t i;
+ if (!v) return;
+ for (i = 0; i < n; ++i) free(v[i]);
+ free(v);
+}
+
+static int ex_arg_needs_quote(const wchar_t* s) {
+ if (!s || !s[0]) return 1;
+ for (; *s; ++s)
+ if (*s == L' ' || *s == L'\t' || *s == L'"') return 1;
+ return 0;
+}
+
+static size_t ex_arg_quoted_len(const wchar_t* s) {
+ size_t len, slash = 0;
+ if (!ex_arg_needs_quote(s)) return (size_t)lstrlenW(s);
+ len = 2u;
+ for (; *s; ++s) {
+ if (*s == L'\\') {
+ ++slash;
+ ++len;
+ } else if (*s == L'"') {
+ len += slash + 2u;
+ slash = 0;
+ } else {
+ slash = 0;
+ ++len;
+ }
+ }
+ return len + slash;
+}
+
+static wchar_t* ex_arg_quote_into(wchar_t* out, const wchar_t* s) {
+ size_t slash = 0;
+ if (!ex_arg_needs_quote(s)) {
+ while (*s) *out++ = *s++;
+ return out;
+ }
+ *out++ = L'"';
+ for (; *s; ++s) {
+ if (*s == L'\\') {
+ *out++ = *s;
+ ++slash;
+ } else if (*s == L'"') {
+ while (slash--) *out++ = L'\\';
+ *out++ = L'\\';
+ *out++ = L'"';
+ slash = 0;
+ } else {
+ *out++ = *s;
+ slash = 0;
+ }
+ }
+ while (slash--) *out++ = L'\\';
+ *out++ = L'"';
+ return out;
+}
+
+static wchar_t* ex_command_line(wchar_t** argv, size_t argc) {
+ size_t i, len = 1u;
+ wchar_t *out, *p;
+ for (i = 0; i < argc; ++i)
+ len += ex_arg_quoted_len(argv[i]) + (i ? 1u : 0u);
+ out = (wchar_t*)malloc(len * sizeof(*out));
+ if (!out) return NULL;
+ p = out;
+ for (i = 0; i < argc; ++i) {
+ if (i) *p++ = L' ';
+ p = ex_arg_quote_into(p, argv[i]);
+ }
+ *p = L'\0';
+ return out;
+}
+
+static int ex_wenv_cmp(const void* a, const void* b) {
+ return lstrcmpiW(*(const wchar_t* const*)a, *(const wchar_t* const*)b);
+}
+
+static wchar_t* ex_env_block(wchar_t** envv, size_t nenv) {
+ size_t i, total = 2u, off = 0;
+ wchar_t* block;
+ qsort(envv, nenv, sizeof(*envv), ex_wenv_cmp);
+ for (i = 0; i < nenv; ++i) total += (size_t)lstrlenW(envv[i]) + 1u;
+ block = (wchar_t*)malloc(total * sizeof(*block));
+ if (!block) return NULL;
+ for (i = 0; i < nenv; ++i) {
+ size_t n = (size_t)lstrlenW(envv[i]) + 1u;
+ memcpy(block + off, envv[i], n * sizeof(*block));
+ off += n;
+ }
+ block[off++] = L'\0';
+ block[off] = L'\0';
+ return block;
+}
+
+static int ex_proc_exit_code(HANDLE h, int* exit_code) {
+ DWORD code;
+ if (!GetExitCodeProcess(h, &code)) return 1;
+ *exit_code = code > 255u ? 128 : (int)code;
+ return 0;
+}
+
+static int ex_spawn(void* user, const KitExecOpts* opts, KitExecProc** out) {
+ DriverEnv* env = (DriverEnv*)user;
+ char** av = NULL;
+ char** ev = NULL;
+ wchar_t** wav = NULL;
+ wchar_t** wev = NULL;
+ wchar_t* cmdline = NULL;
+ wchar_t* envblock = NULL;
+ char* cwd_s = NULL;
+ wchar_t* wcwd = NULL;
+ KitExecProc* proc = NULL;
+ HANDLE pipe_r = INVALID_HANDLE_VALUE;
+ HANDLE pipe_w = INVALID_HANDLE_VALUE;
+ HANDLE fout = INVALID_HANDLE_VALUE;
+ HANDLE ferr = INVALID_HANDLE_VALUE;
+ HANDLE fnull = INVALID_HANDLE_VALUE;
+ STARTUPINFOW si;
+ PROCESS_INFORMATION pi;
+ BOOL inherit = FALSE;
+ size_t i;
+
+ if (!env || !opts || !opts->argv || opts->argc == 0u || !out) return 1;
+ *out = NULL;
+ av = (char**)driver_alloc(env, (opts->argc + 1u) * sizeof *av);
+ ev = (char**)driver_alloc(env, (opts->nenv + 1u) * sizeof *ev);
+ if (!av || !ev) goto err;
+ memset(av, 0, (opts->argc + 1u) * sizeof *av);
+ memset(ev, 0, (opts->nenv + 1u) * sizeof *ev);
+ for (i = 0; i < opts->argc; ++i) {
+ av[i] = (i == 0u && !opts->search_path) ? ex_cwd_join(env, opts->argv[i])
+ : ex_slice_dup(env, opts->argv[i]);
+ if (!av[i]) goto err;
+ }
+ for (i = 0; i < opts->nenv; ++i) {
+ size_t nk = opts->env[i].key.len, nv = opts->env[i].value.len;
+ ev[i] = (char*)driver_alloc(env, nk + 1u + nv + 1u);
+ if (!ev[i]) goto err;
+ memcpy(ev[i], opts->env[i].key.s, nk);
+ ev[i][nk] = '=';
+ memcpy(ev[i] + nk + 1u, opts->env[i].value.s, nv);
+ ev[i][nk + 1u + nv] = '\0';
+ }
+ wav = ex_widen_strv(av, opts->argc);
+ wev = ex_widen_strv(ev, opts->nenv);
+ if (!wav || !wev) goto err;
+ cmdline = ex_command_line(wav, opts->argc);
+ envblock = ex_env_block(wev, opts->nenv);
+ if (opts->cwd.len) {
+ cwd_s = ex_slice_dup(env, opts->cwd);
+ if (cwd_s) wcwd = ex_widen(cwd_s);
+ }
+ proc = (KitExecProc*)driver_alloc_zeroed(env, sizeof *proc);
+ if (!cmdline || !envblock || !proc || (opts->cwd.len && !wcwd)) goto err;
+ proc->read_pipe = INVALID_HANDLE_VALUE;
+
+ memset(&si, 0, sizeof si);
+ memset(&pi, 0, sizeof pi);
+ si.cb = sizeof si;
+
+ if (opts->capture_stdout) {
+ SECURITY_ATTRIBUTES sa;
+ memset(&sa, 0, sizeof sa);
+ sa.nLength = sizeof sa;
+ sa.bInheritHandle = TRUE;
+ if (!CreatePipe(&pipe_r, &pipe_w, &sa, 0)) goto err;
+ SetHandleInformation(pipe_r, HANDLE_FLAG_INHERIT, 0);
+ si.dwFlags = STARTF_USESTDHANDLES;
+ si.hStdInput = GetStdHandle(STD_INPUT_HANDLE);
+ si.hStdOutput = pipe_w;
+ si.hStdError = GetStdHandle(STD_ERROR_HANDLE);
+ inherit = TRUE;
+ } else if (opts->stdout_path.len) {
+ SECURITY_ATTRIBUTES sa;
+ wchar_t* wo = NULL;
+ wchar_t* we = NULL;
+ wchar_t* wn = NULL;
+ char* o = ex_slice_dup(env, opts->stdout_path);
+ char* e = opts->stderr_path.len ? ex_slice_dup(env, opts->stderr_path) : NULL;
+ if (o) wo = ex_widen(o);
+ if (e) we = ex_widen(e);
+ wn = ex_widen("NUL");
+ if (o) driver_free(env, o, strlen(o) + 1u);
+ if (e) driver_free(env, e, strlen(e) + 1u);
+ memset(&sa, 0, sizeof sa);
+ sa.nLength = sizeof sa;
+ sa.bInheritHandle = TRUE;
+ if (wo)
+ fout = CreateFileW(wo, GENERIC_WRITE, FILE_SHARE_READ, &sa, CREATE_ALWAYS,
+ FILE_ATTRIBUTE_NORMAL, NULL);
+ if (we)
+ ferr = CreateFileW(we, GENERIC_WRITE, FILE_SHARE_READ, &sa, CREATE_ALWAYS,
+ FILE_ATTRIBUTE_NORMAL, NULL);
+ fnull = CreateFileW(wn, GENERIC_READ, FILE_SHARE_READ, &sa, OPEN_EXISTING,
+ FILE_ATTRIBUTE_NORMAL, NULL);
+ free(wo);
+ free(we);
+ free(wn);
+ if (fout == INVALID_HANDLE_VALUE || fnull == INVALID_HANDLE_VALUE) goto err;
+ si.dwFlags = STARTF_USESTDHANDLES;
+ si.hStdInput = fnull;
+ si.hStdOutput = fout;
+ si.hStdError = ferr != INVALID_HANDLE_VALUE ? ferr : fout;
+ inherit = TRUE;
+ } else {
+ fflush(stdout);
+ }
+
+ if (!CreateProcessW(opts->search_path ? NULL : wav[0], cmdline, NULL, NULL,
+ inherit, CREATE_UNICODE_ENVIRONMENT, envblock, wcwd, &si,
+ &pi))
+ goto err;
+ CloseHandle(pi.hThread);
+ if (pipe_w != INVALID_HANDLE_VALUE) CloseHandle(pipe_w);
+ if (fout != INVALID_HANDLE_VALUE) CloseHandle(fout);
+ if (ferr != INVALID_HANDLE_VALUE) CloseHandle(ferr);
+ if (fnull != INVALID_HANDLE_VALUE) CloseHandle(fnull);
+ proc->process = pi.hProcess;
+ proc->read_pipe = pipe_r;
+ *out = proc;
+ ex_free_wstrv(wav, opts->argc);
+ ex_free_wstrv(wev, opts->nenv);
+ ex_free_strv(env, av, opts->argc);
+ ex_free_strv(env, ev, opts->nenv);
+ if (cwd_s) driver_free(env, cwd_s, strlen(cwd_s) + 1u);
+ free(wcwd);
+ free(cmdline);
+ free(envblock);
+ return 0;
+
+err:
+ if (pipe_r != INVALID_HANDLE_VALUE) CloseHandle(pipe_r);
+ if (pipe_w != INVALID_HANDLE_VALUE) CloseHandle(pipe_w);
+ if (fout != INVALID_HANDLE_VALUE) CloseHandle(fout);
+ if (ferr != INVALID_HANDLE_VALUE) CloseHandle(ferr);
+ if (fnull != INVALID_HANDLE_VALUE) CloseHandle(fnull);
+ if (proc) driver_free(env, proc, sizeof *proc);
+ if (cwd_s) driver_free(env, cwd_s, strlen(cwd_s) + 1u);
+ free(wcwd);
+ free(cmdline);
+ free(envblock);
+ ex_free_wstrv(wav, opts ? opts->argc : 0u);
+ ex_free_wstrv(wev, opts ? opts->nenv : 0u);
+ ex_free_strv(env, av, opts ? opts->argc : 0u);
+ ex_free_strv(env, ev, opts ? opts->nenv : 0u);
+ return 1;
+}
+
+static int ex_wait(void* user, KitExecProc* proc, int* exit_code, uint8_t** out,
+ size_t* out_len) {
+ DriverEnv* env = (DriverEnv*)user;
+ uint8_t* buf = NULL;
+ size_t len = 0;
+ size_t cap = 0;
+ int oom = 0;
+
+ if (!env || !proc || !exit_code) return 1;
+ if (out) *out = NULL;
+ if (out_len) *out_len = 0;
+
+ if (proc->read_pipe != INVALID_HANDLE_VALUE) {
+ for (;;) {
+ uint8_t tmp[4096];
+ DWORD n = 0;
+ if (!ReadFile(proc->read_pipe, tmp, (DWORD)sizeof tmp, &n, NULL) || n == 0)
+ break;
+ if (!oom && len + n > cap) {
+ size_t ncap = cap ? cap : 8192;
+ uint8_t* nb;
+ while (ncap < len + n) ncap *= 2;
+ nb = env->heap->realloc(env->heap, buf, cap, ncap, 8);
+ if (!nb) {
+ env->heap->free(env->heap, buf, cap);
+ buf = NULL;
+ cap = 0;
+ len = 0;
+ oom = 1;
+ } else {
+ buf = nb;
+ cap = ncap;
+ }
+ }
+ if (!oom) {
+ memcpy(buf + len, tmp, n);
+ len += n;
+ }
+ }
+ CloseHandle(proc->read_pipe);
+ }
+
+ if (WaitForSingleObject(proc->process, INFINITE) != WAIT_OBJECT_0 ||
+ ex_proc_exit_code(proc->process, exit_code) != 0) {
+ CloseHandle(proc->process);
+ driver_free(env, proc, sizeof *proc);
+ if (buf) env->heap->free(env->heap, buf, cap);
+ return 1;
+ }
+ CloseHandle(proc->process);
+ driver_free(env, proc, sizeof *proc);
+ if (out) {
+ *out = buf;
+ if (out_len) *out_len = len;
+ } else if (buf) {
+ env->heap->free(env->heap, buf, cap);
+ }
+ return 0;
+}
+
+static int ex_poll(void* user, KitExecProc* proc, int* done, int* exit_code) {
+ DriverEnv* env = (DriverEnv*)user;
+ DWORD r;
+ if (!env || !proc || !done || !exit_code) return 1;
+ *done = 0;
+ r = WaitForSingleObject(proc->process, 0);
+ if (r == WAIT_TIMEOUT) return 0;
+ if (r != WAIT_OBJECT_0 || ex_proc_exit_code(proc->process, exit_code) != 0)
+ return 1;
+ CloseHandle(proc->process);
+ if (proc->read_pipe != INVALID_HANDLE_VALUE) CloseHandle(proc->read_pipe);
+ driver_free(env, proc, sizeof *proc);
+ *done = 1;
+ return 0;
+}
+
+static void ex_kill(void* user, KitExecProc* proc) {
+ (void)user;
+ if (proc) TerminateProcess(proc->process, 128u);
+}
+
+KitExec driver_exec(DriverEnv* env) {
+ KitExec ex;
+ ex.spawn = ex_spawn;
+ ex.wait = ex_wait;
+ ex.poll = ex_poll;
+ ex.kill = ex_kill;
+ ex.user = env;
+ return ex;
+}
diff --git a/include/kit/build_coord.h b/include/kit/build_coord.h
@@ -3,6 +3,7 @@
#include <kit/cas.h>
#include <kit/core.h>
+#include <kit/exec.h>
#include <kit/package.h>
#include <stddef.h>
#include <stdint.h>
@@ -64,11 +65,8 @@
typedef struct KitBuildCoordinator KitBuildCoordinator;
/* One key=value pair: a propagated-config entry, a `need` overlay, or a recipe
- * environment variable. */
-typedef struct KitBuildKV {
- KitSlice key;
- KitSlice value;
-} KitBuildKV;
+ * environment variable. Same shape as KitExecKV (<kit/exec.h>). */
+typedef KitExecKV KitBuildKV;
/* ------------------------------------------------------------------ *
* Host vtables
@@ -121,39 +119,15 @@ typedef struct KitBuildTransport {
void* user;
} KitBuildTransport;
-/* A spawned host process (recipe or fetch recipe). Opaque. */
-typedef struct KitBuildProc KitBuildProc;
-
-/* Process control for recipes and the remote fetch recipe. `argv` is the
- * process argument vector; `env` is the process's COMPLETE environment — the
- * host sets exactly these pairs (formatting each as "KEY=VALUE") and does NOT
- * inherit its own ambient environment. A clean env is what makes a recipe's
- * inputs complete: ambient PATH/locale/etc. would be undeclared inputs that
- * break determinism and cross-machine trace reuse. The coordinator builds the
- * set from KIT_BUILD_* plus the build's declared `env.`-prefixed config
- * (KIT_BUILD_ENV_PREFIX), which are tracked, so the environment is cache-
- * visible. Each spawn counts as one job against the parallelism limit. */
-typedef struct KitBuildExec {
- int (*spawn)(void* user, const KitSlice* argv, size_t argc,
- const KitBuildKV* env, size_t nenv, KitSlice cwd,
- KitBuildProc** out);
- /* Optional captured spawn, used by test execution. stdout/stderr are
- * redirected to the supplied host paths. Hosts that cannot support capture
- * leave this NULL; kit_build_test then returns KIT_UNSUPPORTED. */
- int (*spawn_capture)(void* user, const KitSlice* argv, size_t argc,
- const KitBuildKV* env, size_t nenv, KitSlice cwd,
- KitSlice stdout_path, KitSlice stderr_path,
- KitBuildProc** out);
- /* Block until `proc` exits; *exit_code receives its status; frees `proc`. */
- int (*wait)(void* user, KitBuildProc* proc, int* exit_code);
- /* Optional nonblocking status check for recipe processes. On exit, sets
- * *done=1, stores the exit status, and frees `proc` exactly as wait does.
- * Otherwise sets *done=0 and leaves `proc` live. */
- int (*poll)(void* user, KitBuildProc* proc, int* done, int* exit_code);
- /* Best-effort terminate (cancel on cycle/abort); `wait` still reaps. */
- void (*kill)(void* user, KitBuildProc* proc);
- void* user;
-} KitBuildExec;
+/* Process execution for recipes and the fetch recipe uses the shared
+ * config-struct interface KitExec (<kit/exec.h>). The coordinator drives it in
+ * hermetic mode: exact argv[0] (KitExecOpts.search_path = 0), an explicit
+ * COMPLETE environment (no ambient inheritance, so a recipe's inputs stay
+ * complete and cache-visible), a mandatory cwd, and test output captured to
+ * files (stdout_path/stderr_path). Each spawn counts as one job against the
+ * parallelism limit. */
+typedef KitExecProc KitBuildProc;
+typedef KitExec KitBuildExec;
/* Reports one directory entry name during a store sweep. Return non-zero to
* abort the walk. */
diff --git a/include/kit/exec.h b/include/kit/exec.h
@@ -0,0 +1,68 @@
+#ifndef KIT_EXEC_H
+#define KIT_EXEC_H
+
+#include <kit/core.h>
+#include <stddef.h>
+#include <stdint.h>
+
+/*
+ * Host process execution: a single config-struct-driven subprocess interface
+ * shared by every libkit subsystem that launches child processes (the build
+ * coordinator's recipes/tests, `kit make`'s recipes and `!=` / $(shell ...)).
+ * The library never spawns processes itself; a host supplies this vtable.
+ *
+ * One spawn covers every case via KitExecOpts: PATH-searched or exact argv[0],
+ * an explicit child environment, an optional working directory, and an optional
+ * stdout disposition (inherit, capture to a buffer, or redirect to files). The
+ * async spawn/wait/poll/kill shape lets a host run children in parallel;
+ * synchronous callers simply spawn then wait immediately.
+ */
+
+/* An opaque spawned process. Created by spawn; freed by wait or poll on exit. */
+typedef struct KitExecProc KitExecProc;
+
+/* One NAME=VALUE environment entry (borrowed slices). */
+typedef struct KitExecKV {
+ KitSlice key;
+ KitSlice value;
+} KitExecKV;
+
+typedef struct KitExecOpts {
+ const KitSlice* argv; /* argument vector; argv[0] is the program */
+ size_t argc;
+ const KitExecKV* env; /* the child's COMPLETE environment (no ambient merge) */
+ size_t nenv;
+ KitSlice cwd; /* working directory; empty => inherit the caller's */
+ /* Resolve argv[0] via PATH taken from `env` (execvp-style). When false,
+ * argv[0] is used as an exact path (a relative path is joined to cwd). */
+ int search_path;
+ /* Capture the child's stdout into a buffer, retrieved from wait's out/out_len.
+ * Mutually exclusive with stdout_path. */
+ int capture_stdout;
+ /* Redirect the child's stdout/stderr to these files (empty => inherit). Used
+ * for recorded test output. */
+ KitSlice stdout_path;
+ KitSlice stderr_path;
+} KitExecOpts;
+
+typedef struct KitExec {
+ /* Spawn a child per `opts`. On success *out holds a live process handle.
+ * Returns 0 on success, non-zero on spawn failure. */
+ int (*spawn)(void* user, const KitExecOpts* opts, KitExecProc** out);
+ /* Block until `proc` exits; *exit_code receives the exit code (>= 0) or the
+ * negated terminating signal (< 0); frees `proc`. If the spawn requested
+ * capture_stdout and out/out_len are non-NULL, they receive a buffer of the
+ * child's stdout allocated from the host's heap (the caller frees it through
+ * that heap); pass NULL/NULL to discard captured output. */
+ int (*wait)(void* user, KitExecProc* proc, int* exit_code, uint8_t** out,
+ size_t* out_len);
+ /* Optional non-blocking status check. On exit sets *done=1, stores the exit
+ * status, and frees `proc` as wait does; otherwise sets *done=0 and leaves
+ * `proc` live. (Captured output, if any, is only retrievable via wait.) */
+ int (*poll)(void* user, KitExecProc* proc, int* done, int* exit_code);
+ /* Best-effort terminate; `wait` still reaps. */
+ void (*kill)(void* user, KitExecProc* proc);
+ void* user;
+} KitExec;
+
+#endif
diff --git a/mk/driver_srcs.mk b/mk/driver_srcs.mk
@@ -89,8 +89,10 @@ DRIVER_SRCS += $(call need-any,CC CHECK BUILD_EXE BUILD_LIB BUILD_OBJ,driver/lib
DRIVER_SRCS += $(call need-any,CAS PKG BUILD UPDATE,driver/lib/dist_host.c)
ifeq ($(HOST_OS),windows)
DRIVER_SRCS += $(call need-any,BUILD,driver/env/build_host_windows.c)
+DRIVER_SRCS += $(call need-any,BUILD MAKE,driver/env/exec_windows.c)
else
DRIVER_SRCS += $(call need-any,BUILD,driver/env/build_host_posix.c)
+DRIVER_SRCS += $(call need-any,BUILD MAKE,driver/env/exec_posix.c)
endif
DRIVER_SRCS += $(call need-any,INSTALL UPDATE,driver/lib/install_links.c)
DRIVER_SRCS += $(call need-any,UPDATE,driver/release_key.c)
diff --git a/src/api/build_coord.c b/src/api/build_coord.c
@@ -58,7 +58,7 @@ KitStatus kit_build_test(KitBuildCoordinator* c, const KitBuildRequest* req,
BuildTestResolved r;
char target[BUILD_TARGET_MAX];
if (!c || !req || !out) return KIT_INVALID;
- if (!c->host.exec || !c->host.exec->spawn_capture) return KIT_UNSUPPORTED;
+ if (!c->host.exec || !c->host.exec->spawn) return KIT_UNSUPPORTED;
if (build_coord_canonical_target(c, req->target, KIT_SLICE_NULL,
KIT_SLICE_NULL, target) != BUILD_OK)
return KIT_INVALID;
diff --git a/src/build/bundle.c b/src/build/bundle.c
@@ -1506,12 +1506,17 @@ static int trace_render_argv(const KitContext* ctx, KitSlice tmpl,
static int run_fetch(const KitBuildExec* exec, const KitSlice* argv,
size_t argc, KitSlice cwd) {
KitBuildProc* proc = NULL;
+ KitExecOpts eo;
int exit_code = 1;
if (!exec || !exec->spawn || !exec->wait || !argv || argc == 0u)
return BUILD_ERR;
- if (exec->spawn(exec->user, argv, argc, NULL, 0u, cwd, &proc) != 0 || !proc)
+ memset(&eo, 0, sizeof eo);
+ eo.argv = argv;
+ eo.argc = argc;
+ eo.cwd = cwd;
+ if (exec->spawn(exec->user, &eo, &proc) != 0 || !proc) return BUILD_ERR;
+ if (exec->wait(exec->user, proc, &exit_code, NULL, NULL) != 0)
return BUILD_ERR;
- if (exec->wait(exec->user, proc, &exit_code) != 0) return BUILD_ERR;
return exit_code == 0 ? BUILD_OK : BUILD_ERR;
}
diff --git a/src/build/remote.c b/src/build/remote.c
@@ -252,12 +252,17 @@ static int verify_bytes(const uint8_t* data, size_t len,
static int run_fetch(const KitBuildExec* exec, const KitSlice* argv,
size_t argc, KitSlice cwd) {
KitBuildProc* proc = NULL;
+ KitExecOpts eo;
int exit_code = 1;
if (!exec || !exec->spawn || !exec->wait || !argv || argc == 0u)
return BUILD_ERR;
- if (exec->spawn(exec->user, argv, argc, NULL, 0u, cwd, &proc) != 0 || !proc)
+ memset(&eo, 0, sizeof eo);
+ eo.argv = argv;
+ eo.argc = argc;
+ eo.cwd = cwd;
+ if (exec->spawn(exec->user, &eo, &proc) != 0 || !proc) return BUILD_ERR;
+ if (exec->wait(exec->user, proc, &exit_code, NULL, NULL) != 0)
return BUILD_ERR;
- if (exec->wait(exec->user, proc, &exit_code) != 0) return BUILD_ERR;
return exit_code == 0 ? BUILD_OK : BUILD_ERR;
}
diff --git a/src/build/runner.c b/src/build/runner.c
@@ -335,7 +335,8 @@ static int service_recipe_connections(KitBuildCoordinator* c,
return BUILD_ERR;
}
c->host.transport->close(c->host.transport->user, conn);
- if (c->host.exec->wait(c->host.exec->user, *proc, exit_code) != 0)
+ if (c->host.exec->wait(c->host.exec->user, *proc, exit_code, NULL, NULL) !=
+ 0)
return BUILD_ERR;
*proc = NULL;
return BUILD_OK;
@@ -483,10 +484,17 @@ static int build_run_recipe_impl(KitBuildCoordinator* c, KitSlice target,
++nenv;
}
- if (c->host.exec->spawn(c->host.exec->user, proc_argv, argc, env, nenv,
- kit_slice_cstr(recipe.workspace_root), &proc) != 0 ||
- !proc)
- goto out_cleanup;
+ {
+ KitExecOpts eo;
+ memset(&eo, 0, sizeof eo);
+ eo.argv = proc_argv;
+ eo.argc = argc;
+ eo.env = env;
+ eo.nenv = nenv;
+ eo.cwd = kit_slice_cstr(recipe.workspace_root);
+ if (c->host.exec->spawn(c->host.exec->user, &eo, &proc) != 0 || !proc)
+ goto out_cleanup;
+ }
build_coord_stat_bump(c, BUILD_STAT_RECIPE_RUN);
if (service_recipe_connections(c, listener, &proc, target, cfg, chain, &log,
&exit_code) != BUILD_OK)
@@ -508,7 +516,7 @@ static int build_run_recipe_impl(KitBuildCoordinator* c, KitSlice target,
out_kill:
if (proc && c->host.exec->kill) c->host.exec->kill(c->host.exec->user, proc);
if (proc && c->host.exec->wait)
- (void)c->host.exec->wait(c->host.exec->user, proc, &exit_code);
+ (void)c->host.exec->wait(c->host.exec->user, proc, &exit_code, NULL, NULL);
proc = NULL;
out_cleanup:
if (conn) c->host.transport->close(c->host.transport->user, conn);
@@ -544,7 +552,7 @@ static int build_run_test_recipe_impl(KitBuildCoordinator* c, KitSlice target,
BuildDepLog log;
if (!c || !cfg || !argv || !out || !c->host.exec || !c->host.transport ||
- !c->host.exec->spawn_capture)
+ !c->host.exec->spawn)
return BUILD_ERR;
endpoint[0] = '\0';
sandbox[0] = '\0';
@@ -607,12 +615,19 @@ static int build_run_test_recipe_impl(KitBuildCoordinator* c, KitSlice target,
++nenv;
}
- if (c->host.exec->spawn_capture(
- c->host.exec->user, proc_argv, argc, env, nenv,
- kit_slice_cstr(recipe.workspace_root), kit_slice_cstr(stdout_path),
- kit_slice_cstr(stderr_path), &proc) != 0 ||
- !proc)
- goto out_cleanup;
+ {
+ KitExecOpts eo;
+ memset(&eo, 0, sizeof eo);
+ eo.argv = proc_argv;
+ eo.argc = argc;
+ eo.env = env;
+ eo.nenv = nenv;
+ eo.cwd = kit_slice_cstr(recipe.workspace_root);
+ eo.stdout_path = kit_slice_cstr(stdout_path);
+ eo.stderr_path = kit_slice_cstr(stderr_path);
+ if (c->host.exec->spawn(c->host.exec->user, &eo, &proc) != 0 || !proc)
+ goto out_cleanup;
+ }
build_coord_stat_bump(c, BUILD_STAT_TEST_RUN);
if (service_recipe_connections(c, listener, &proc, target, cfg, chain, &log,
&exit_code) != BUILD_OK)
@@ -643,7 +658,7 @@ static int build_run_test_recipe_impl(KitBuildCoordinator* c, KitSlice target,
out_kill:
if (proc && c->host.exec->kill) c->host.exec->kill(c->host.exec->user, proc);
if (proc && c->host.exec->wait)
- (void)c->host.exec->wait(c->host.exec->user, proc, &exit_code);
+ (void)c->host.exec->wait(c->host.exec->user, proc, &exit_code, NULL, NULL);
proc = NULL;
out_cleanup:
if (conn) c->host.transport->close(c->host.transport->user, conn);