kit

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

commit 19bc7463d0bd485b162826f043b722cad87b1450
parent 4bd240e557f9f843cb8a17214553a72219faddca
Author: Ryan Sepassi <rsepassi@gmail.com>
Date:   Tue,  2 Jun 2026 16:49:44 -0700

driver: add install command for drop-in toolchain symlinks

kit install [OPTIONS] DIR [TOOL...] lays down one link per tool in DIR,
each pointing at the running kit binary (symlinks on POSIX, hard links on
Windows). Basename dispatch already routes a link named cc/ld/nm to the
matching tool, so the links work as a drop-in toolchain with no dispatch
change.

Default set is the binutils/compiler toolchain plus the standard-named byte
utils (xxd, cmp); --all installs everything and an explicit TOOL list selects
specific tools. Flags: -s/-H link type, -f force-replace, -n dry-run,
-v verbose. The tool table in main.c gains a DriverToolGroup tag (the single
source of truth) exposed via driver_tool_count/name/groups/find.

New host helpers in driver/env: driver_self_exe_path (one impl per OS:
/proc/self/exe, _NSGetExecutablePath, KERN_PROC_PATHNAME, GetModuleFileNameW)
plus POSIX-shared/Windows driver_create_symlink/hardlink, driver_remove_file,
and driver_path_lexists. Covered by 32 new scenarios in test/driver/run.sh.

Diffstat:
MMakefile | 3+++
MREADME.md | 3++-
Mdoc/DRIVER.md | 14+++++++++++---
Adriver/cmd/install.c | 247+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mdriver/driver.h | 22++++++++++++++++++++++
Mdriver/env.h | 29+++++++++++++++++++++++++++++
Mdriver/env/freebsd.c | 31+++++++++++++++++++++++++++++++
Mdriver/env/linux.c | 29+++++++++++++++++++++++++++++
Mdriver/env/macos.c | 40++++++++++++++++++++++++++++++++++++++++
Mdriver/env/posix.c | 24++++++++++++++++++++++++
Mdriver/env/windows.c | 119+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mdriver/main.c | 100+++++++++++++++++++++++++++++++++++++++++++++++++++++++++----------------------
Minclude/kit/config.h | 1+
Mtest/driver/run.sh | 64++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
14 files changed, 695 insertions(+), 31 deletions(-)

diff --git a/Makefile b/Makefile @@ -352,6 +352,9 @@ endif ifeq ($(KIT_TOOL_CHECK_ENABLED),1) DRIVER_TOOL_SRCS += driver/cmd/cc.c endif +ifeq ($(KIT_TOOL_INSTALL_ENABLED),1) +DRIVER_TOOL_SRCS += driver/cmd/install.c +endif ifeq ($(KIT_TOOL_CPP_ENABLED),1) DRIVER_TOOL_SRCS += driver/cmd/cpp.c endif diff --git a/README.md b/README.md @@ -27,7 +27,8 @@ It features: addr2line, strings - Standalone gzip and LZ4-frame compression (`compress`), interoperable with stock `gzip`/`lz4` -- A single multi-call binary +- A single multi-call binary, with an `install` command that drops per-tool + symlinks (hard links on Windows) into a directory for drop-in toolchain use - Debug info generation and consumption (DWARF) - An interactive debugger - Header dependency generation diff --git a/doc/DRIVER.md b/doc/DRIVER.md @@ -1,8 +1,9 @@ # DRIVER The `kit` multitool is the toolchain's only executable: a single binary that -dispatches to ~24 named tools (compiler, assembler, linker, archive/object -utilities, byte utilities, JIT runner, debugger, emulator, packager). It is also the first and +dispatches to ~26 named tools (compiler, assembler, linker, archive/object +utilities, byte utilities, JIT runner, debugger, emulator, packager, and an +`install` command that lays down the per-tool links). It is also the first and canonical *consumer* of libkit — it depends only on the public API under `include/kit/`, never on `src/`. Everything that the OS provides (heap, file I/O, executable memory, threads, signals, time, entropy) enters libkit through @@ -84,6 +85,7 @@ tool reaches into compiler internals. |------|------| | `cc` | C compiler driver: compile, optionally link; preprocess (`-E`), dep-emit (`-M*`), `-shared`. GCC flag subset. Resolves `-l`/`-L` to concrete archive paths. | | `check` | Run the C frontend checks with no code emission. | +| `install` | Lay down per-tool links (symlinks; hard links on Windows) in a target dir so the toolchain works under bare names (`cc`, `ld`, `nm`, …). Default set is the toolchain + standard-named byte utils; `--all` / explicit names override. | | `cpp` | Standalone preprocessor (alias for `cc -E` without link scaffolding). | | `as` | Assemble one GAS-subset text source to a relocatable object. | | `ld` | Link objects/archives into an executable, shared library, or relocatable object; parses `-T` scripts into structured form. | @@ -202,7 +204,13 @@ freestanding tools need but can't make themselves: `driver_printf`/`errf`, path existence/mtime, `mkdir -p`, directory walks, stdin slurp, an `$EDITOR` temp-file round-trip, a raw-mode line editor with history/completion for the `dbg` REPL, SIGINT install/restore, monotonic time, CSPRNG bytes (for `pkg` -key generation), and a `dlsym` resolver so JITed code can call host libc. +key generation), a `dlsym` resolver so JITed code can call host libc, and (for +`install`) self-executable-path resolution plus symlink / hard-link / unlink / +no-follow-existence primitives. The self-path resolver is the one helper with a +genuine per-OS divergence — `/proc/self/exe` (Linux), `_NSGetExecutablePath` +(macOS), `KERN_PROC_PATHNAME` (FreeBSD), `GetModuleFileNameW` (Windows) — so it +lives one-impl-per-OS alongside `driver_default_hosted_dirs`; the link/unlink +helpers are POSIX-shared with a Windows twin. ### One TU per concern, zero `#ifdef` diff --git a/driver/cmd/install.c b/driver/cmd/install.c @@ -0,0 +1,247 @@ +#include <kit/core.h> +#include <stddef.h> +#include <stdint.h> +#include <string.h> + +#include "driver.h" +#include "env.h" + +/* `kit install` — busybox-style toolchain installer. Populates a target + * directory with one entry per kit tool, each pointing at the running kit + * binary, so the tools can be invoked by their bare names (cc, ld, nm, ...) + * as a drop-in toolchain. Entries are symlinks on POSIX and hard links on + * Windows (where unprivileged symlinks aren't generally available); either + * can be forced with -s/-H. + * + * Multi-call dispatch in main.c resolves a link named `cc` to the cc tool by + * argv[0]'s basename, so no extra wiring is needed for the links to work. + * + * The default set (no TOOL args, no --all) is the tools tagged with a non-zero + * DriverToolGroup in the centralized table: the binutils/compiler toolchain + * plus the standard-named byte utilities (xxd, cmp). */ + +#define INSTALL_TOOL "install" + +void driver_help_install(void) { + driver_printf( + "kit install — symlink the kit tools into a directory\n" + "\n" + "USAGE\n" + " kit install [OPTIONS] DIR [TOOL...]\n" + "\n" + "DESCRIPTION\n" + " Populates DIR with one entry per kit tool, each pointing at the\n" + " running kit binary, so the tools can be invoked by their bare\n" + " names (cc, ld, nm, ...) as a drop-in toolchain. Entries are\n" + " symlinks on POSIX and hard links on Windows by default.\n" + "\n" + " With no TOOL given, installs the default set: the binutils/compiler\n" + " toolchain (cc cpp as ld ar ranlib strip objcopy objdump nm size\n" + " addr2line strings) plus the standard-named byte utilities (xxd cmp).\n" + " Use --all for every tool, or name specific TOOLs to install just\n" + " those.\n" + "\n" + "OPTIONS\n" + " -s, --symlink create symlinks (default except on Windows)\n" + " -H, --hardlink create hard links (default on Windows)\n" + " -a, --all install every tool compiled into this binary\n" + " -f, --force replace existing entries\n" + " -n, --dry-run print what would be done; change nothing\n" + " -v, --verbose print each link as it is created\n" + " -h, --help show this help\n" + "\n" + "EXIT CODES\n" + " 0 success 1 one or more links failed 2 bad usage\n"); +} + +/* Join DIR + "/" + NAME into a freshly allocated, NUL-terminated path. Returns + * NULL on allocation failure; on success stores the allocation size in + * *out_size for driver_free. */ +static char* install_join(DriverEnv* env, const char* dir, const char* name, + size_t* out_size) { + size_t dl = driver_strlen(dir); + size_t nl = driver_strlen(name); + int slash = dl > 0 && dir[dl - 1u] != '/'; + size_t size = dl + (slash ? 1u : 0u) + nl + 1u; + size_t off; + char* p = (char*)driver_alloc(env, size); + if (!p) return NULL; + driver_memcpy(p, dir, dl); + off = dl; + if (slash) p[off++] = '/'; + driver_memcpy(p + off, name, nl); + off += nl; + p[off] = '\0'; + *out_size = size; + return p; +} + +/* Create one link DIR/NAME -> self. Returns 0 on success, nonzero on failure + * (already emitted a diagnostic). Honors dry-run (prints, no change), force + * (replace existing), and verbose. */ +static int install_one(DriverEnv* env, const char* dir, const char* name, + const char* self, int want_hard, int force, int dry, + int verbose) { + size_t path_size = 0; + char* link_path = install_join(env, dir, name, &path_size); + int rc = 0; + + if (!link_path) { + driver_errf(INSTALL_TOOL, "out of memory building path for %s", name); + return 1; + } + + if (driver_path_lexists(link_path)) { + if (!force) { + driver_errf(INSTALL_TOOL, "%s already exists (use -f to overwrite)", + link_path); + rc = 1; + goto done; + } + if (!dry && driver_remove_file(link_path) != 0) { + driver_errf(INSTALL_TOOL, "cannot replace %s", link_path); + rc = 1; + goto done; + } + } + + if (dry) { + driver_printf("%s -> %s (dry-run)\n", link_path, self); + goto done; + } + + rc = want_hard ? driver_create_hardlink(self, link_path) + : driver_create_symlink(self, link_path); + if (rc != 0) { + driver_errf(INSTALL_TOOL, "failed to %s %s", + want_hard ? "hard-link" : "symlink", link_path); + rc = 1; + goto done; + } + if (verbose) driver_printf("%s -> %s\n", link_path, self); + +done: + driver_free(env, link_path, path_size); + return rc; +} + +int driver_install(int argc, char** argv) { + DriverEnv env; + const char* dir = NULL; + const char** explicit_tools = NULL; + int nexplicit = 0; + int want_hard = -1; /* -1 = pick by host OS; 0 = symlink; 1 = hard link */ + int all = 0, force = 0, dry = 0, verbose = 0, opts_done = 0; + char* self = NULL; + size_t self_size = 0; + unsigned done_count = 0, failures = 0; + int i, rc = 2; + + if (driver_argv_wants_help(argc, argv, 1)) { + driver_help_install(); + return 0; + } + + driver_env_init(&env); + + explicit_tools = + (const char**)driver_alloc(&env, (size_t)argc * sizeof(*explicit_tools)); + if (!explicit_tools) { + driver_errf(INSTALL_TOOL, "out of memory"); + rc = 1; + goto done; + } + + for (i = 1; i < argc; ++i) { + const char* a = argv[i]; + if (!opts_done && driver_streq(a, "--")) { + opts_done = 1; + continue; + } + if (!opts_done && a[0] == '-' && a[1] != '\0') { + if (driver_streq(a, "-s") || driver_streq(a, "--symlink")) { + want_hard = 0; + } else if (driver_streq(a, "-H") || driver_streq(a, "--hardlink")) { + want_hard = 1; + } else if (driver_streq(a, "-a") || driver_streq(a, "--all")) { + all = 1; + } else if (driver_streq(a, "-f") || driver_streq(a, "--force")) { + force = 1; + } else if (driver_streq(a, "-n") || driver_streq(a, "--dry-run")) { + dry = 1; + } else if (driver_streq(a, "-v") || driver_streq(a, "--verbose")) { + verbose = 1; + } else { + driver_errf(INSTALL_TOOL, "unknown option: %s", a); + goto done; + } + continue; + } + if (!dir) + dir = a; + else + explicit_tools[nexplicit++] = a; + } + + if (!dir) { + driver_errf(INSTALL_TOOL, "missing target directory"); + goto done; + } + + /* Validate explicit tool names up front so a typo fails before we touch the + * filesystem. */ + for (i = 0; i < nexplicit; ++i) { + if (driver_tool_find(explicit_tools[i]) < 0) { + driver_errf(INSTALL_TOOL, "no such tool: %s", explicit_tools[i]); + goto done; + } + } + + if (want_hard < 0) + want_hard = (driver_host_target().os == KIT_OS_WINDOWS) ? 1 : 0; + + if (driver_self_exe_path(&env, &self, &self_size) != 0) { + driver_errf(INSTALL_TOOL, "cannot determine path to the kit binary"); + rc = 1; + goto done; + } + + if (!dry && driver_mkdir_p(&env, dir) != 0) { + driver_errf(INSTALL_TOOL, "cannot create directory %s", dir); + rc = 1; + goto done; + } + + if (nexplicit > 0) { + for (i = 0; i < nexplicit; ++i) { + if (install_one(&env, dir, explicit_tools[i], self, want_hard, force, dry, + verbose) != 0) + ++failures; + else + ++done_count; + } + } else { + unsigned n = driver_tool_count(); + unsigned j; + for (j = 0; j < n; ++j) { + if (!all && driver_tool_groups(j) == 0) continue; + if (install_one(&env, dir, driver_tool_name(j), self, want_hard, force, + dry, verbose) != 0) + ++failures; + else + ++done_count; + } + } + + driver_printf("install: %s %u tool%s in %s\n", + dry ? "would install" : "installed", done_count, + done_count == 1u ? "" : "s", dir); + rc = failures ? 1 : 0; + +done: + if (self) driver_free(&env, self, self_size); + if (explicit_tools) + driver_free(&env, explicit_tools, (size_t)argc * sizeof(*explicit_tools)); + driver_env_fini(&env); + return rc; +} diff --git a/driver/driver.h b/driver/driver.h @@ -38,6 +38,7 @@ int driver_main(int argc, char** argv); /* Direct entry per tool. Each lives in driver/<tool>.c. */ int driver_cc(int argc, char** argv); int driver_check(int argc, char** argv); +int driver_install(int argc, char** argv); int driver_cpp(int argc, char** argv); int driver_as(int argc, char** argv); int driver_ld(int argc, char** argv); @@ -68,6 +69,7 @@ int driver_mc(int argc, char** argv); * headers — only --help triggers help there). */ void driver_help_cc(void); void driver_help_check(void); +void driver_help_install(void); void driver_help_cpp(void); void driver_help_as(void); void driver_help_ld(void); @@ -97,6 +99,26 @@ void driver_help_mc(void); * the multi-call dispatch. Writes to stdout. */ void driver_help_top(void); +/* Tool grouping, used by `install` to pick a default set without + * duplicating the tool list. The centralized table in main.c tags every + * row; the groups with a non-zero bit make up the default install set + * (the binutils/compiler toolchain plus the byte utilities whose names + * match standard commands). */ +typedef enum DriverToolGroup { + DRIVER_GROUP_OTHER = 0, + DRIVER_GROUP_TOOLCHAIN = 1u << 0, /* binutils + compiler driver */ + DRIVER_GROUP_BYTEUTIL = 1u << 1, /* standard-named byte utilities */ +} DriverToolGroup; + +/* Read-only views over the centralized tool table (main.c), so the + * `install` tool always reflects exactly the tools compiled into this + * binary. Indices are stable within a process and run 0..count-1. */ +unsigned driver_tool_count(void); +const char* driver_tool_name(unsigned index); /* NULL if out of range */ +unsigned driver_tool_groups(unsigned index); /* 0 if out of range */ +/* Index of the tool named `name`, or -1 if there is no such tool. */ +int driver_tool_find(const char* name); + /* Returns 1 if `arg` is "--help" or "-help". The short "-h" is treated * as a help request by every tool except objdump (where it means * "section headers"); callers that want to honour it should test it diff --git a/driver/env.h b/driver/env.h @@ -166,6 +166,35 @@ int driver_path_mtime_ns(const char* path, int64_t* out); /* Create a directory and any missing parents. Returns 0 on success. */ int driver_mkdir_p(DriverEnv*, const char* path); +/* Resolve the absolute path of the running kit executable into a freshly + * heap-allocated, NUL-terminated buffer (*out / *out_size); free it with + * driver_free(env, *out, *out_size). Returns 0 on success, nonzero on + * failure (out untouched). Per-OS: /proc/self/exe (Linux), _NSGetExecutablePath + * + realpath (macOS), KERN_PROC_PATHNAME sysctl (FreeBSD), GetModuleFileNameW + * (Windows). Used by `install` to point freshly created links at the binary. */ +int driver_self_exe_path(DriverEnv*, char** out, size_t* out_size); + +/* Create a symbolic link named `link_path` that resolves to `target`. Returns + * 0 on success. POSIX uses symlink(2); Windows uses CreateSymbolicLinkW, which + * may require privilege or Developer Mode (so `install` defaults to hard links + * on Windows). */ +int driver_create_symlink(const char* target, const char* link_path); + +/* Create a hard link named `link_path` referring to the same file as `target`. + * Returns 0 on success. POSIX uses link(2); Windows uses CreateHardLinkW. Both + * require `target` and `link_path` to live on the same filesystem/volume. */ +int driver_create_hardlink(const char* target, const char* link_path); + +/* Remove the file or symlink at `path`. Returns 0 when the entry was removed or + * was already absent, nonzero on any other failure. POSIX unlink(2) / Windows + * DeleteFileW. */ +int driver_remove_file(const char* path); + +/* Test whether a name exists at `path` WITHOUT following symlinks, so a + * dangling symlink still counts as existing. Returns nonzero on existence. + * POSIX lstat / Windows GetFileAttributesW. */ +int driver_path_lexists(const char* path); + /* Set a linked binary output's final mode according to the active umask. * Returns 0 on success, nonzero on chmod failure. */ int driver_mark_executable_output(const char* path); diff --git a/driver/env/freebsd.c b/driver/env/freebsd.c @@ -15,6 +15,8 @@ #include <string.h> #include <sys/mman.h> #include <sys/stat.h> +#include <sys/sysctl.h> +#include <sys/types.h> #include <unistd.h> #include "env_posix.h" @@ -136,6 +138,35 @@ void os_host_target_fill(KitTarget* t) { t->obj = KIT_OBJ_ELF; } +/* ---------------- self executable path ---------------- */ +/* The KERN_PROC_PATHNAME sysctl returns the absolute path of a process's text + * image; pid -1 selects the calling process. A first call with a NULL buffer + * reports the required length (including the NUL). */ +int driver_self_exe_path(DriverEnv* env, char** out, size_t* out_size) { + int mib[4]; + size_t len = 0; + size_t cap; + char* buf; + + if (!env || !out || !out_size) return 1; + mib[0] = CTL_KERN; + mib[1] = KERN_PROC; + mib[2] = KERN_PROC_PATHNAME; + mib[3] = -1; + if (sysctl(mib, 4, NULL, &len, NULL, 0) != 0 || len == 0) return 1; + cap = len; + buf = (char*)driver_alloc(env, cap); + if (!buf) return 1; + if (sysctl(mib, 4, buf, &len, NULL, 0) != 0) { + driver_free(env, buf, cap); + return 1; + } + buf[cap - 1u] = '\0'; /* defensive: ensure termination */ + *out = buf; + *out_size = cap; + return 0; +} + /* ---------------- default hosted dirs probe ---------------- */ /* FreeBSD base system is flat: headers in /usr/include, crt + libc in /usr/lib * and /lib (libc.so.7 lives in /lib). Host target only. UNTESTED on the macOS diff --git a/driver/env/linux.c b/driver/env/linux.c @@ -141,6 +141,35 @@ void os_host_target_fill(KitTarget* t) { t->obj = KIT_OBJ_ELF; } +/* ---------------- self executable path ---------------- */ +/* /proc/self/exe is a kernel-maintained symlink to the running binary; reading + * it yields the canonical absolute path. readlink does not NUL-terminate and a + * truncated read is indistinguishable from an exact fit, so grow until the + * result fits with room for the terminator. */ +int driver_self_exe_path(DriverEnv* env, char** out, size_t* out_size) { + size_t cap = 256; + if (!env || !out || !out_size) return 1; + for (;;) { + char* buf = (char*)driver_alloc(env, cap); + ssize_t n; + if (!buf) return 1; + n = readlink("/proc/self/exe", buf, cap); + if (n < 0) { + driver_free(env, buf, cap); + return 1; + } + if ((size_t)n < cap) { + buf[n] = '\0'; + *out = buf; + *out_size = cap; + return 0; + } + driver_free(env, buf, cap); + if (cap >= (size_t)1 << 20) return 1; /* implausible path length */ + cap *= 2; + } +} + /* ---------------- default hosted dirs probe ---------------- */ /* Linux multiarch triple for the supported 64-bit arches; NULL otherwise. */ static const char* linux_multiarch_triple(KitArchKind arch) { diff --git a/driver/env/macos.c b/driver/env/macos.c @@ -8,6 +8,7 @@ #include <dlfcn.h> #include <libkern/OSCacheControl.h> +#include <mach-o/dyld.h> #include <mach/mach.h> #include <mach/mach_vm.h> #include <mach/vm_map.h> @@ -128,6 +129,45 @@ void os_host_target_fill(KitTarget* t) { t->obj = KIT_OBJ_MACHO; } +/* ---------------- self executable path ---------------- */ +/* _NSGetExecutablePath yields the path as invoked (may contain symlinks or + * `..`); realpath canonicalizes it to a stable absolute path so installed + * links keep resolving regardless of how kit was launched. */ +int driver_self_exe_path(DriverEnv* env, char** out, size_t* out_size) { + char stackbuf[1024]; + char* nsbuf = stackbuf; + uint32_t cap = (uint32_t)sizeof stackbuf; + size_t heap_cap = 0; + char* resolved; + size_t size; + + if (!env || !out || !out_size) return 1; + if (_NSGetExecutablePath(nsbuf, &cap) != 0) { + /* cap was set to the required size; allocate and retry once. */ + heap_cap = cap; + nsbuf = (char*)driver_alloc(env, heap_cap); + if (!nsbuf) return 1; + if (_NSGetExecutablePath(nsbuf, &cap) != 0) { + driver_free(env, nsbuf, heap_cap); + return 1; + } + } + resolved = realpath(nsbuf, NULL); /* malloc'd; caller frees with free() */ + if (heap_cap) driver_free(env, nsbuf, heap_cap); + if (!resolved) return 1; + + size = driver_strlen(resolved) + 1u; + *out = (char*)driver_alloc(env, size); + if (!*out) { + free(resolved); + return 1; + } + driver_memcpy(*out, resolved, size); + *out_size = size; + free(resolved); + return 0; +} + /* ---------------- default hosted dirs probe ---------------- */ /* With no --sysroot or KIT_SYSROOT, locate the macOS SDK by stat'ing the * canonical Command Line Tools and Xcode.app SDK roots -- the same locations diff --git a/driver/env/posix.c b/driver/env/posix.c @@ -391,6 +391,30 @@ int driver_mark_executable_output(const char* path) { return chmod(path, mode) == 0 ? 0 : 1; } +/* ---------------- link helpers (install) ---------------- */ + +int driver_create_symlink(const char* target, const char* link_path) { + if (!target || !link_path) return 1; + return symlink(target, link_path) == 0 ? 0 : 1; +} + +int driver_create_hardlink(const char* target, const char* link_path) { + if (!target || !link_path) return 1; + return link(target, link_path) == 0 ? 0 : 1; +} + +int driver_remove_file(const char* path) { + if (!path) return 1; + if (unlink(path) == 0) return 0; + return errno == ENOENT ? 0 : 1; /* already absent is success */ +} + +int driver_path_lexists(const char* path) { + struct stat sb; + if (!path) return 0; + return lstat(path, &sb) == 0; +} + static char* driver_join_path(DriverEnv* env, const char* a, const char* b) { size_t al = kit_slice_cstr(a).len; size_t bl = kit_slice_cstr(b).len; diff --git a/driver/env/windows.c b/driver/env/windows.c @@ -589,6 +589,125 @@ int driver_mark_executable_output(const char* path) { return 0; } +/* ---------------- self executable path ---------------- */ +/* GetModuleFileNameW(NULL) reports the path of the running image. A return + * equal to the buffer size means truncation (older Windows doesn't fail), so + * grow until the result fits, then narrow to UTF-8. */ +int driver_self_exe_path(DriverEnv* env, char** out, size_t* out_size) { + DWORD cap = 256; + wchar_t* wbuf = NULL; + char* narrowed; + size_t size; + if (!env || !out || !out_size) return 1; + for (;;) { + wchar_t* nb = (wchar_t*)realloc(wbuf, (size_t)cap * sizeof(wchar_t)); + DWORD n; + if (!nb) { + free(wbuf); + return 1; + } + wbuf = nb; + n = GetModuleFileNameW(NULL, wbuf, cap); + if (n == 0) { + free(wbuf); + return 1; + } + if (n < cap) break; /* fit: n excludes the terminator on success */ + if (cap >= (1u << 20)) { + free(wbuf); + return 1; + } + cap *= 2; + } + narrowed = narrow(wbuf); /* malloc'd UTF-8 */ + free(wbuf); + if (!narrowed) return 1; + size = driver_strlen(narrowed) + 1u; + *out = (char*)driver_alloc(env, size); + if (!*out) { + free(narrowed); + return 1; + } + driver_memcpy(*out, narrowed, size); + *out_size = size; + free(narrowed); + return 0; +} + +/* ---------------- link helpers (install) ---------------- */ + +#ifndef SYMBOLIC_LINK_FLAG_ALLOW_UNPRIVILEGED_CREATE +#define SYMBOLIC_LINK_FLAG_ALLOW_UNPRIVILEGED_CREATE 0x2 +#endif + +int driver_create_symlink(const char* target, const char* link_path) { + wchar_t* wtarget; + wchar_t* wlink; + BOOLEAN ok; + if (!target || !link_path) return 1; + wtarget = widen(target); + wlink = widen(link_path); + if (!wtarget || !wlink) { + free(wtarget); + free(wlink); + return 1; + } + /* Prefer the unprivileged (Developer Mode) flag; fall back without it for + * older hosts that reject the unknown flag. */ + ok = CreateSymbolicLinkW(wlink, wtarget, + SYMBOLIC_LINK_FLAG_ALLOW_UNPRIVILEGED_CREATE); + if (!ok) ok = CreateSymbolicLinkW(wlink, wtarget, 0); + free(wtarget); + free(wlink); + return ok ? 0 : 1; +} + +int driver_create_hardlink(const char* target, const char* link_path) { + wchar_t* wtarget; + wchar_t* wlink; + BOOL ok; + if (!target || !link_path) return 1; + wtarget = widen(target); + wlink = widen(link_path); + if (!wtarget || !wlink) { + free(wtarget); + free(wlink); + return 1; + } + ok = CreateHardLinkW(wlink, wtarget, NULL); + free(wtarget); + free(wlink); + return ok ? 0 : 1; +} + +int driver_remove_file(const char* path) { + wchar_t* wpath; + BOOL ok; + if (!path) return 1; + wpath = widen(path); + if (!wpath) return 1; + ok = DeleteFileW(wpath); + if (!ok) { + DWORD err = GetLastError(); + if (err == ERROR_FILE_NOT_FOUND || err == ERROR_PATH_NOT_FOUND) ok = TRUE; + } + free(wpath); + return ok ? 0 : 1; +} + +int driver_path_lexists(const char* path) { + wchar_t* wpath; + DWORD attr; + if (!path) return 0; + wpath = widen(path); + if (!wpath) return 0; + /* GetFileAttributesW does not traverse reparse points, so a dangling + * symlink reports its own attributes rather than failing. */ + attr = GetFileAttributesW(wpath); + free(wpath); + return attr != INVALID_FILE_ATTRIBUTES; +} + static char* driver_join_path(DriverEnv* env, const char* a, const char* b) { size_t al = kit_slice_cstr(a).len; size_t bl = kit_slice_cstr(b).len; diff --git a/driver/main.c b/driver/main.c @@ -18,114 +18,160 @@ typedef struct DriverToolDesc { DriverToolMain main; DriverToolHelp help; const char* summary; + /* DriverToolGroup bits; 0 => not in the default install set */ + unsigned groups; } DriverToolDesc; static const DriverToolDesc driver_tools[] = { #if KIT_TOOL_CC_ENABLED {"cc", driver_cc, driver_help_cc, - "Compile (and link) C sources, with cpp / dep-emit / -shared modes"}, + "Compile (and link) C sources, with cpp / dep-emit / -shared modes", + DRIVER_GROUP_TOOLCHAIN}, #endif #if KIT_TOOL_CHECK_ENABLED {"check", driver_check, driver_help_check, - "Run C frontend checks without emitting code"}, + "Run C frontend checks without emitting code", DRIVER_GROUP_OTHER}, +#endif +#if KIT_TOOL_INSTALL_ENABLED + {"install", driver_install, driver_help_install, + "Symlink the kit tools into a dir for drop-in toolchain use", + DRIVER_GROUP_OTHER}, #endif #if KIT_TOOL_CPP_ENABLED {"cpp", driver_cpp, driver_help_cpp, - "Standalone C preprocessor (alias for `cc -E` minus link scaffold)"}, + "Standalone C preprocessor (alias for `cc -E` minus link scaffold)", + DRIVER_GROUP_TOOLCHAIN}, #endif #if KIT_TOOL_AS_ENABLED {"as", driver_as, driver_help_as, - "Assemble a GAS-subset text source into a relocatable object"}, + "Assemble a GAS-subset text source into a relocatable object", + DRIVER_GROUP_TOOLCHAIN}, #endif #if KIT_TOOL_LD_ENABLED {"ld", driver_ld, driver_help_ld, - "Link objects/archives into an executable or shared library"}, + "Link objects/archives into an executable or shared library", + DRIVER_GROUP_TOOLCHAIN}, #endif #if KIT_TOOL_AR_ENABLED {"ar", driver_ar, driver_help_ar, - "Create / modify / list / extract POSIX `ar` archives"}, + "Create / modify / list / extract POSIX `ar` archives", + DRIVER_GROUP_TOOLCHAIN}, #endif #if KIT_TOOL_RANLIB_ENABLED {"ranlib", driver_ranlib, driver_help_ranlib, - "Refresh the symbol index of an `ar` archive"}, + "Refresh the symbol index of an `ar` archive", DRIVER_GROUP_TOOLCHAIN}, #endif #if KIT_TOOL_STRIP_ENABLED {"strip", driver_strip, driver_help_strip, - "Drop debug sections and/or symbols from a .o or .a"}, + "Drop debug sections and/or symbols from a .o or .a", + DRIVER_GROUP_TOOLCHAIN}, #endif #if KIT_TOOL_OBJCOPY_ENABLED {"objcopy", driver_objcopy, driver_help_objcopy, - "Copy and transform an object file (rename / remove / format)"}, + "Copy and transform an object file (rename / remove / format)", + DRIVER_GROUP_TOOLCHAIN}, #endif #if KIT_TOOL_OBJDUMP_ENABLED {"objdump", driver_objdump, driver_help_objdump, - "Dump sections, symbols, disassembly, hex, and relocations"}, + "Dump sections, symbols, disassembly, hex, and relocations", + DRIVER_GROUP_TOOLCHAIN}, #endif #if KIT_TOOL_RUN_ENABLED {"run", driver_run, driver_help_run, - "JIT-compile inputs and invoke the entry symbol in-process"}, + "JIT-compile inputs and invoke the entry symbol in-process", + DRIVER_GROUP_OTHER}, #endif #if KIT_TOOL_DBG_ENABLED {"dbg", driver_dbg, driver_help_dbg, - "Interactive JIT debugger (REPL on top of the JIT image)"}, + "Interactive JIT debugger (REPL on top of the JIT image)", + DRIVER_GROUP_OTHER}, #endif #if KIT_TOOL_EMU_ENABLED {"emu", driver_emu, driver_help_emu, - "Run a guest user-mode ELF (aarch64/riscv64) on the host"}, + "Run a guest user-mode ELF (aarch64/riscv64) on the host", + DRIVER_GROUP_OTHER}, #endif #if KIT_TOOL_NM_ENABLED - {"nm", driver_nm, driver_help_nm, "List symbols from object files"}, + {"nm", driver_nm, driver_help_nm, "List symbols from object files", + DRIVER_GROUP_TOOLCHAIN}, #endif #if KIT_TOOL_SIZE_ENABLED {"size", driver_size, driver_help_size, - "Display section sizes of object files"}, + "Display section sizes of object files", DRIVER_GROUP_TOOLCHAIN}, #endif #if KIT_TOOL_ADDR2LINE_ENABLED {"addr2line", driver_addr2line, driver_help_addr2line, - "Translate addresses to file:line using debug info"}, + "Translate addresses to file:line using debug info", + DRIVER_GROUP_TOOLCHAIN}, #endif #if KIT_TOOL_STRINGS_ENABLED {"strings", driver_strings, driver_help_strings, - "Print printable character sequences found in a file"}, + "Print printable character sequences found in a file", + DRIVER_GROUP_TOOLCHAIN}, #endif #if KIT_TOOL_CAS_ENABLED {"cas", driver_cas, driver_help_cas, - "Store, inspect, verify, and materialize kit CAS blobs and trees"}, + "Store, inspect, verify, and materialize kit CAS blobs and trees", + DRIVER_GROUP_OTHER}, #endif #if KIT_TOOL_PKG_ENABLED {"pkg", driver_pkg, driver_help_pkg, - "Bundle, sign, verify, and unpack distributable .kpkg packages"}, + "Bundle, sign, verify, and unpack distributable .kpkg packages", + DRIVER_GROUP_OTHER}, #endif #if KIT_TOOL_XXD_ENABLED {"xxd", driver_xxd, driver_help_xxd, - "Hex dump any file (and reverse a dump back to binary)"}, + "Hex dump any file (and reverse a dump back to binary)", + DRIVER_GROUP_BYTEUTIL}, #endif #if KIT_TOOL_CMP_ENABLED - {"cmp", driver_cmp, driver_help_cmp, "Compare two files byte by byte"}, + {"cmp", driver_cmp, driver_help_cmp, "Compare two files byte by byte", + DRIVER_GROUP_BYTEUTIL}, #endif #if KIT_TOOL_HASH_ENABLED {"hash", driver_hash, driver_help_hash, - "Hash files with SHA-256, BLAKE2b, or CRC-32"}, + "Hash files with SHA-256, BLAKE2b, or CRC-32", DRIVER_GROUP_OTHER}, #endif #if KIT_TOOL_COMPRESS_ENABLED {"compress", driver_compress, driver_help_compress, - "Compress or decompress data (gzip, lz4 frame)"}, + "Compress or decompress data (gzip, lz4 frame)", DRIVER_GROUP_OTHER}, #endif #if KIT_TOOL_DISAS_ENABLED {"disas", driver_disas, driver_help_disas, - "Disassemble raw machine-code bytes for a target arch"}, + "Disassemble raw machine-code bytes for a target arch", + DRIVER_GROUP_OTHER}, #endif #if KIT_TOOL_MC_ENABLED {"mc", driver_mc, driver_help_mc, - "Assemble one instruction and show its machine-code encoding"}, + "Assemble one instruction and show its machine-code encoding", + DRIVER_GROUP_OTHER}, #endif - {NULL, NULL, NULL, NULL}, + {NULL, NULL, NULL, NULL, DRIVER_GROUP_OTHER}, }; -static unsigned driver_tool_count(void) { +unsigned driver_tool_count(void) { return (unsigned)(sizeof driver_tools / sizeof driver_tools[0]) - 1u; } +const char* driver_tool_name(unsigned index) { + if (index >= driver_tool_count()) return NULL; + return driver_tools[index].name; +} + +unsigned driver_tool_groups(unsigned index) { + if (index >= driver_tool_count()) return 0u; + return driver_tools[index].groups; +} + +int driver_tool_find(const char* name) { + unsigned i; + if (!name) return -1; + for (i = 0; i < driver_tool_count(); ++i) { + if (driver_streq(name, driver_tools[i].name)) return (int)i; + } + return -1; +} + static const DriverToolDesc* find_tool(const char* name) { unsigned i; for (i = 0; i < driver_tool_count(); ++i) { diff --git a/include/kit/config.h b/include/kit/config.h @@ -103,6 +103,7 @@ * the driver/<tool>.c objects included in the kit binary. */ #define KIT_TOOL_CC_ENABLED 1 #define KIT_TOOL_CHECK_ENABLED 1 +#define KIT_TOOL_INSTALL_ENABLED 1 #define KIT_TOOL_CPP_ENABLED 1 #define KIT_TOOL_AS_ENABLED 1 #define KIT_TOOL_LD_ENABLED 1 diff --git a/test/driver/run.sh b/test/driver/run.sh @@ -959,5 +959,69 @@ else not_ok "cc-emit-ir-requires-opt" "$work/ir-o0-wrong.diag" fi +# ---- install: symlink the kit tools into a dir ---- +# Default set: toolchain + standard-named byte utils, each link -> the binary. +inst_dir="$work/inst" +run_ok "install-default" "$KIT" install "$inst_dir" +for t in cc cpp as ld ar ranlib strip objcopy objdump nm size addr2line \ + strings xxd cmp; do + assert_file_exists "install-has-$t" "$inst_dir/$t" +done +is_executable "install-cc-executable" "$inst_dir/cc" +# A symlink to the binary has identical bytes, and dispatches by basename. +same_file "install-cc-points-at-kit" "$KIT" "$inst_dir/cc" +run_ok "install-symlink-dispatches" "$inst_dir/cc" --help + +# Non-default tools (e.g. mc) are excluded unless --all selects everything. +if [ -e "$inst_dir/mc" ]; then + echo "mc present in the default set" > "$work/install-mc.diag" + not_ok "install-default-excludes-mc" "$work/install-mc.diag" +else + ok "install-default-excludes-mc" +fi +inst_all="$work/inst-all" +run_ok "install-all" "$KIT" install --all "$inst_all" +assert_file_exists "install-all-has-mc" "$inst_all/mc" + +# Existing entries are an error without -f, replaced with it. +run_fail "install-existing-without-force" "$KIT" install "$inst_dir" +run_ok "install-existing-with-force" "$KIT" install -f "$inst_dir" + +# An explicit tool list installs exactly those names; an unknown name and a +# missing directory are usage errors. +inst_pick="$work/inst-pick" +run_ok "install-explicit" "$KIT" install "$inst_pick" nm ld +assert_file_exists "install-explicit-nm" "$inst_pick/nm" +if [ -e "$inst_pick/cc" ]; then + echo "cc installed despite an explicit 'nm ld' list" \ + > "$work/install-pick.diag" + not_ok "install-explicit-only-listed" "$work/install-pick.diag" +else + ok "install-explicit-only-listed" +fi +run_fail "install-unknown-tool" "$KIT" install "$work/inst-bad" bogustool +run_fail "install-missing-dir" "$KIT" install + +# A hard link shares the binary's inode (the `-ef` test compares st_dev+st_ino). +inst_hard="$work/inst-hard" +run_ok "install-hardlink" "$KIT" install -H "$inst_hard" nm +if [ "$inst_hard/nm" -ef "$KIT" ]; then + ok "install-hardlink-same-inode" +else + echo "hard link does not share the binary's inode" \ + > "$work/install-hard.diag" + not_ok "install-hardlink-same-inode" "$work/install-hard.diag" +fi + +# Dry-run reports intent but changes nothing (the target dir stays absent). +inst_dry="$work/inst-dry" +run_ok "install-dry-run" "$KIT" install -n "$inst_dry" +if [ -e "$inst_dry" ]; then + echo "dry-run created $inst_dry" > "$work/install-dry.diag" + not_ok "install-dry-run-no-changes" "$work/install-dry.diag" +else + ok "install-dry-run-no-changes" +fi + kit_summary driver-cc kit_exit