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:
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