kit

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

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:
Mdriver/cmd/build_coord.c | 15+++++++++++----
Mdriver/env.h | 7+++++++
Mdriver/env/build_host_posix.c | 174+------------------------------------------------------------------------------
Mdriver/env/build_host_windows.c | 353+------------------------------------------------------------------------------
Adriver/env/exec_posix.c | 283+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Adriver/env/exec_windows.c | 441+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Minclude/kit/build_coord.h | 50++++++++++++--------------------------------------
Ainclude/kit/exec.h | 68++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mmk/driver_srcs.mk | 2++
Msrc/api/build_coord.c | 2+-
Msrc/build/bundle.c | 9+++++++--
Msrc/build/remote.c | 9+++++++--
Msrc/build/runner.c | 43+++++++++++++++++++++++++++++--------------
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);