Template
feat(ext): add extension discovery and loading
Extension discovery (plan todo 20): scans --ext-dir dirs, STUPIDTOOLS_EXT, the XDG user dir and a builtin-ext dir for *.lua modules, runs each in the sandboxed Lua runtime, and enumerates its registrations; the builtin C/C++ modules load first via st_ext_init_builtins. Fail-fast with an error naming file and line on any malformed/sandbox-violating module. The CLI gains a repeatable --ext-dir flag; with flag or env present main.c runs discovery and prints 'loaded extension: ...' lines from actual registrations (interim wiring until todo 23). Note: src/Makefile.am gained the ext + vendored Lua sources because the binary otherwise cannot link discovery -- required for the root-verifiable CLI acceptance.
This commit is contained in:
+25
-2
@@ -3,5 +3,28 @@
|
||||
AM_CFLAGS = -std=c23 -Wall -Wextra -Wpedantic
|
||||
|
||||
bin_PROGRAMS = stupidtools
|
||||
stupidtools_SOURCES = main.c cli.c
|
||||
noinst_HEADERS = cli.h
|
||||
stupidtools_SOURCES = \
|
||||
main.c cli.c \
|
||||
error.c span.c \
|
||||
ext/discovery.c ext/lua.c ext/abi.c ext/lang_c.c ext/lang_cpp.c \
|
||||
../thirdparty/lua/lapi.c ../thirdparty/lua/lauxlib.c \
|
||||
../thirdparty/lua/lbaselib.c ../thirdparty/lua/lcode.c \
|
||||
../thirdparty/lua/lctype.c ../thirdparty/lua/ldebug.c \
|
||||
../thirdparty/lua/ldo.c ../thirdparty/lua/ldump.c \
|
||||
../thirdparty/lua/lfunc.c ../thirdparty/lua/lgc.c \
|
||||
../thirdparty/lua/llex.c ../thirdparty/lua/lmem.c \
|
||||
../thirdparty/lua/lobject.c ../thirdparty/lua/lopcodes.c \
|
||||
../thirdparty/lua/lparser.c ../thirdparty/lua/lstate.c \
|
||||
../thirdparty/lua/lstring.c ../thirdparty/lua/ltable.c \
|
||||
../thirdparty/lua/ltm.c ../thirdparty/lua/lundump.c \
|
||||
../thirdparty/lua/lvm.c ../thirdparty/lua/lzio.c \
|
||||
../thirdparty/lua/lstrlib.c ../thirdparty/lua/ltablib.c
|
||||
|
||||
# The vendored Lua subset above deliberately EXCLUDES linit.c,
|
||||
# lmathlib.c, loadlib.c, liolib.c, loslib.c, ldblib.c, lcorolib.c,
|
||||
# lutf8lib.c and the lua.c/luac.c mains -- see src/ext/lua.c's -lm note
|
||||
# (the runtime's curated math/os tables replace lmathlib/loslib, and
|
||||
# the six libm shims in lua.c keep the link free of -lm).
|
||||
noinst_HEADERS = cli.h error.h span.h \
|
||||
ext/discovery.h ext/lua.h ext/abi.h ext/lang_c.h ext/lang_cpp.h \
|
||||
ext/lang_common.h
|
||||
|
||||
@@ -42,6 +42,9 @@ cli_print_usage(FILE *stream, const char *program)
|
||||
"Options:\n"
|
||||
" -h, --help show this help and exit\n"
|
||||
" -V, --version print version and exit\n"
|
||||
" --ext-dir DIR load extensions from DIR (repeatable; the\n"
|
||||
" STUPIDTOOLS_EXT environment variable, a\n"
|
||||
" colon-separated directory list, is honored too)\n"
|
||||
"\n"
|
||||
"Note: buildfile processing is not yet implemented.\n",
|
||||
program);
|
||||
@@ -70,6 +73,7 @@ cli_parse(int argc, char **argv, struct cli_opts *opts)
|
||||
|
||||
opts->program = program_name(argc > 0 ? argv[0] : NULL);
|
||||
opts->buildfile = NULL;
|
||||
opts->ext_dir_count = 0;
|
||||
|
||||
for (i = 1; i < argc; i++) {
|
||||
const char *arg = argv[i];
|
||||
@@ -101,6 +105,37 @@ cli_parse(int argc, char **argv, struct cli_opts *opts)
|
||||
return CLI_ACTION_VERSION;
|
||||
}
|
||||
|
||||
/* --ext-dir consumes the NEXT argument unconditionally
|
||||
* (getopt-style; even if it starts with '-'), is repeatable,
|
||||
* and rejects a missing/empty value and more than
|
||||
* CLI_MAX_EXT_DIRS repetitions as usage errors. The value is
|
||||
* borrowed from argv; a dir that does not exist is not an
|
||||
* error -- discovery skips missing dirs silently. */
|
||||
if (strcmp(arg, "--ext-dir") == 0) {
|
||||
if (i + 1 >= argc) {
|
||||
fprintf(stderr, "%s: option '--ext-dir' "
|
||||
"requires an argument\n", opts->program);
|
||||
cli_print_usage(stderr, opts->program);
|
||||
return CLI_ACTION_ERROR;
|
||||
}
|
||||
i++;
|
||||
if (argv[i][0] == '\0') {
|
||||
fprintf(stderr, "%s: option '--ext-dir' "
|
||||
"requires a non-empty directory\n",
|
||||
opts->program);
|
||||
cli_print_usage(stderr, opts->program);
|
||||
return CLI_ACTION_ERROR;
|
||||
}
|
||||
if (opts->ext_dir_count >= CLI_MAX_EXT_DIRS) {
|
||||
fprintf(stderr, "%s: too many --ext-dir options "
|
||||
"(max %d)\n", opts->program, CLI_MAX_EXT_DIRS);
|
||||
cli_print_usage(stderr, opts->program);
|
||||
return CLI_ACTION_ERROR;
|
||||
}
|
||||
opts->ext_dirs[opts->ext_dir_count++] = argv[i];
|
||||
continue;
|
||||
}
|
||||
|
||||
fprintf(stderr, "%s: unrecognized option '%s'\n",
|
||||
opts->program, arg);
|
||||
cli_print_usage(stderr, opts->program);
|
||||
|
||||
@@ -17,6 +17,9 @@
|
||||
#define CLI_EXIT_RUNTIME 1
|
||||
#define CLI_EXIT_USAGE 2
|
||||
|
||||
/* Sane cap on repeated --ext-dir flags (todo 20). */
|
||||
#define CLI_MAX_EXT_DIRS 32
|
||||
|
||||
/* What the caller should do after parsing argv. */
|
||||
enum cli_action {
|
||||
CLI_ACTION_RUN, /* proceed with the parsed buildfile (todo 16+) */
|
||||
@@ -28,6 +31,11 @@ enum cli_action {
|
||||
struct cli_opts {
|
||||
const char *program; /* argv[0] as given (or a default) */
|
||||
const char *buildfile; /* single positional buildfile, NULL if none */
|
||||
/* --ext-dir values, in the order given; the pointers are borrowed
|
||||
* from argv. STUPIDTOOLS_EXT is NOT merged here -- discovery reads
|
||||
* the environment itself and appends those dirs after these. */
|
||||
const char *ext_dirs[CLI_MAX_EXT_DIRS];
|
||||
size_t ext_dir_count;
|
||||
};
|
||||
|
||||
enum cli_action cli_parse(int argc, char **argv, struct cli_opts *opts);
|
||||
|
||||
@@ -0,0 +1,458 @@
|
||||
/*
|
||||
* src/ext/discovery.c - extension discovery and loading (plan todo 20).
|
||||
*
|
||||
* See src/ext/discovery.h for the full contract: the discovery order
|
||||
* (builtin C modules -> --ext-dir -> STUPIDTOOLS_EXT -> user dir ->
|
||||
* builtin dir), the per-directory scan rules (regular *.lua files,
|
||||
* bytewise order, missing dirs are no-ops), and the FAIL-FAST error
|
||||
* contract ("extension load failed: <path>: ...").
|
||||
*
|
||||
* Everything loads through the sandboxed Lua runtime (st_lua_run); the
|
||||
* builtin C/C++ language modules go through st_ext_init_builtins() into
|
||||
* the ABI ctx. No language is hardcoded here.
|
||||
*
|
||||
* Copyright (c) 2026 huntedbytheirs
|
||||
* SPDX-License-Identifier: BSD-3-Clause
|
||||
*/
|
||||
|
||||
#ifndef _POSIX_C_SOURCE
|
||||
#define _POSIX_C_SOURCE 200809L /* strdup, strtok_r, stat, readdir */
|
||||
#endif
|
||||
|
||||
#include <dirent.h>
|
||||
#include <errno.h>
|
||||
#include <stdarg.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <sys/stat.h>
|
||||
|
||||
#include "abi.h"
|
||||
#include "discovery.h"
|
||||
#include "lua.h"
|
||||
|
||||
/* Compile-time override for the shipped builtin extension directory;
|
||||
* the default is resolved relative to the current working directory
|
||||
* (repo-layout friendly -- see builtin-ext/README.md). An installed
|
||||
* layout overrides it, e.g.
|
||||
* -DSTUPIDTOOLS_BUILTIN_EXT_DIR='"<prefix>/share/stupidtools/ext"'
|
||||
*/
|
||||
#ifndef STUPIDTOOLS_BUILTIN_EXT_DIR
|
||||
#define STUPIDTOOLS_BUILTIN_EXT_DIR "builtin-ext"
|
||||
#endif
|
||||
|
||||
/* ---------------- logging ---------------- */
|
||||
|
||||
/* NULL -> default stderr logger. The one piece of module state; the
|
||||
* task explicitly asks for a minimal hook, and threading a logger
|
||||
* parameter through the whole chain would churn the public signature. */
|
||||
static st_ext_log_fn g_log_fn;
|
||||
|
||||
void
|
||||
st_ext_discover_set_log(st_ext_log_fn fn)
|
||||
{
|
||||
g_log_fn = fn;
|
||||
}
|
||||
|
||||
void
|
||||
st_ext_discover_log(const char *fmt, ...)
|
||||
{
|
||||
va_list ap;
|
||||
|
||||
va_start(ap, fmt);
|
||||
if (g_log_fn != NULL) {
|
||||
g_log_fn(fmt, ap);
|
||||
} else {
|
||||
vfprintf(stderr, fmt, ap);
|
||||
}
|
||||
va_end(ap);
|
||||
}
|
||||
|
||||
/* ---------------- small helpers ---------------- */
|
||||
|
||||
/* Comma-joined names of registrations [from, count) via the accessor;
|
||||
* malloc'd, NULL when the range is empty or on allocation failure. */
|
||||
static char *
|
||||
names_join(const struct st_lua_rt *rt, size_t from, size_t count,
|
||||
const char *(*name_at)(const struct st_lua_rt *, size_t))
|
||||
{
|
||||
size_t i, total = 0;
|
||||
char *buf, *p;
|
||||
|
||||
if (count <= from) return NULL;
|
||||
for (i = from; i < count; i++) {
|
||||
const char *n = name_at(rt, i);
|
||||
if (n != NULL) total += strlen(n) + 2; /* ", " separator */
|
||||
}
|
||||
if (total == 0) return NULL;
|
||||
buf = malloc(total + 1);
|
||||
if (buf == NULL) return NULL;
|
||||
p = buf;
|
||||
for (i = from; i < count; i++) {
|
||||
const char *n = name_at(rt, i);
|
||||
size_t len;
|
||||
|
||||
if (n == NULL) continue;
|
||||
len = strlen(n);
|
||||
if (p != buf) {
|
||||
*p++ = ',';
|
||||
*p++ = ' ';
|
||||
}
|
||||
memcpy(p, n, len);
|
||||
p += len;
|
||||
}
|
||||
*p = '\0';
|
||||
return buf;
|
||||
}
|
||||
|
||||
/* "c, cxx" style list of the ABI ctx's language names (malloc'd). */
|
||||
static char *
|
||||
abi_language_names(const struct st_ext_ctx *ctx)
|
||||
{
|
||||
size_t i, n = st_ext_language_count(ctx), total = 0;
|
||||
char *buf, *p;
|
||||
|
||||
for (i = 0; i < n; i++) {
|
||||
const char *nm = st_ext_language_name(ctx, i);
|
||||
if (nm != NULL) total += strlen(nm) + 2;
|
||||
}
|
||||
if (total == 0) return NULL;
|
||||
buf = malloc(total + 1);
|
||||
if (buf == NULL) return NULL;
|
||||
p = buf;
|
||||
for (i = 0; i < n; i++) {
|
||||
const char *nm = st_ext_language_name(ctx, i);
|
||||
size_t len;
|
||||
|
||||
if (nm == NULL) continue;
|
||||
len = strlen(nm);
|
||||
if (p != buf) {
|
||||
*p++ = ',';
|
||||
*p++ = ' ';
|
||||
}
|
||||
memcpy(p, nm, len);
|
||||
p += len;
|
||||
}
|
||||
*p = '\0';
|
||||
return buf;
|
||||
}
|
||||
|
||||
/* Reads a whole file into a freshly malloc'd NUL-terminated buffer.
|
||||
* NULL on success (buffer in *out, length in *out_len); an st_error on
|
||||
* failure (I/O -> ST_ERR_IO naming the file, OOM -> ST_ERR_INTERNAL). */
|
||||
static struct st_error *
|
||||
read_module_source(const char *path, char **out, size_t *out_len)
|
||||
{
|
||||
FILE *f;
|
||||
long size;
|
||||
char *buf;
|
||||
size_t got;
|
||||
size_t need;
|
||||
struct st_error *err;
|
||||
|
||||
f = fopen(path, "rb");
|
||||
if (f == NULL) goto io_fail;
|
||||
if (fseek(f, 0, SEEK_END) != 0) {
|
||||
(void)fclose(f);
|
||||
goto io_fail;
|
||||
}
|
||||
size = ftell(f);
|
||||
if (size < 0 || fseek(f, 0, SEEK_SET) != 0) {
|
||||
(void)fclose(f);
|
||||
goto io_fail;
|
||||
}
|
||||
buf = malloc((size_t)size + 1);
|
||||
if (buf == NULL) {
|
||||
(void)fclose(f);
|
||||
return st_error_internal("extension load failed: out of memory");
|
||||
}
|
||||
got = fread(buf, 1, (size_t)size, f);
|
||||
if (got != (size_t)size || fclose(f) != 0) {
|
||||
free(buf);
|
||||
goto io_fail;
|
||||
}
|
||||
buf[size] = '\0';
|
||||
*out = buf;
|
||||
*out_len = (size_t)size;
|
||||
return NULL;
|
||||
|
||||
io_fail:
|
||||
need = strlen(path) + 64;
|
||||
buf = malloc(need);
|
||||
if (buf == NULL)
|
||||
return st_error_internal("extension load failed: out of memory");
|
||||
(void)snprintf(buf, need, "extension load failed: %s: %s", path,
|
||||
strerror(errno));
|
||||
err = st_error_io(buf);
|
||||
free(buf);
|
||||
return err;
|
||||
}
|
||||
|
||||
/* Wraps the runtime's error so the message names the module file. The
|
||||
* category is preserved (sandbox violations stay ST_ERR_KDL_SCHEMA so
|
||||
* st_error_print keeps its [kdl-schema] marker); the runtime's own
|
||||
* message already carries "<file>:<line>: " via chunkname propagation. */
|
||||
static struct st_error *
|
||||
module_load_error(struct st_error *inner, const char *path)
|
||||
{
|
||||
enum st_error_category cat = st_error_category_of(inner);
|
||||
const char *msg = st_error_message(inner);
|
||||
size_t need = strlen(path) + (msg != NULL ? strlen(msg) : 12) + 32;
|
||||
char *buf = malloc(need);
|
||||
struct st_error *err;
|
||||
|
||||
if (buf == NULL) {
|
||||
st_error_free(inner);
|
||||
return st_error_internal("extension load failed: out of memory");
|
||||
}
|
||||
(void)snprintf(buf, need, "extension load failed: %s: %s", path,
|
||||
msg != NULL ? msg : "(no message)");
|
||||
err = st_error_new(cat, buf);
|
||||
free(buf);
|
||||
st_error_free(inner);
|
||||
return err;
|
||||
}
|
||||
|
||||
/* Logs one loaded module with the registrations it added, computed from
|
||||
* the runtime's counts before/after the load (registrations only ever
|
||||
* append, so the additions are exactly [before, after)). */
|
||||
static void
|
||||
log_module(struct st_lua_rt *rt, const char *path, size_t checks_before,
|
||||
size_t langs_before)
|
||||
{
|
||||
size_t checks_after = st_lua_check_count(rt);
|
||||
size_t langs_after = st_lua_language_count(rt);
|
||||
char *ck = names_join(rt, checks_before, checks_after, st_lua_check_name);
|
||||
char *lg = names_join(rt, langs_before, langs_after,
|
||||
st_lua_language_name);
|
||||
|
||||
if (ck != NULL && lg != NULL) {
|
||||
st_ext_discover_log("loaded extension: %s — checks: %zu (%s) "
|
||||
"languages: %zu (%s)\n", path,
|
||||
checks_after - checks_before, ck,
|
||||
langs_after - langs_before, lg);
|
||||
} else if (ck != NULL) {
|
||||
st_ext_discover_log("loaded extension: %s — checks: %zu (%s) "
|
||||
"languages: %zu\n", path, checks_after - checks_before, ck,
|
||||
langs_after - langs_before);
|
||||
} else if (lg != NULL) {
|
||||
st_ext_discover_log("loaded extension: %s — checks: %zu "
|
||||
"languages: %zu (%s)\n", path, checks_after - checks_before,
|
||||
langs_after - langs_before, lg);
|
||||
} else {
|
||||
st_ext_discover_log("loaded extension: %s — checks: %zu "
|
||||
"languages: %zu\n", path, checks_after - checks_before,
|
||||
langs_after - langs_before);
|
||||
}
|
||||
free(ck);
|
||||
free(lg);
|
||||
}
|
||||
|
||||
/* Logs the builtin C modules from the ACTUAL ABI registrations. */
|
||||
static void
|
||||
log_builtins(const struct st_ext_ctx *ctx)
|
||||
{
|
||||
char *names = abi_language_names(ctx);
|
||||
|
||||
if (names != NULL) {
|
||||
st_ext_discover_log("loaded builtin modules: languages: %s\n",
|
||||
names);
|
||||
free(names);
|
||||
} else {
|
||||
st_ext_discover_log("loaded builtin modules\n");
|
||||
}
|
||||
}
|
||||
|
||||
/* Loads one module file into the sandbox. NULL on success; on failure a
|
||||
* wrapped error naming the file (the fail-fast contract). */
|
||||
static struct st_error *
|
||||
load_module(struct st_lua_rt *rt, const char *path)
|
||||
{
|
||||
char *src = NULL;
|
||||
size_t len = 0;
|
||||
size_t c_before = st_lua_check_count(rt);
|
||||
size_t l_before = st_lua_language_count(rt);
|
||||
struct st_error *err = read_module_source(path, &src, &len);
|
||||
|
||||
if (err != NULL) return err;
|
||||
err = st_lua_run(rt, src, path); /* chunkname = full path */
|
||||
free(src);
|
||||
if (err != NULL) return module_load_error(err, path);
|
||||
log_module(rt, path, c_before, l_before);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
static int
|
||||
cmp_str(const void *a, const void *b)
|
||||
{
|
||||
const char *const *sa = a;
|
||||
const char *const *sb = b;
|
||||
|
||||
return strcmp(*sa, *sb);
|
||||
}
|
||||
|
||||
/* Scans one directory for *.lua modules and loads them in bytewise
|
||||
* order. NULL on success; a missing directory is a documented no-op. */
|
||||
static struct st_error *
|
||||
discover_dir(struct st_lua_rt *rt, const char *dir)
|
||||
{
|
||||
DIR *d = opendir(dir);
|
||||
struct dirent *e;
|
||||
char **names = NULL;
|
||||
size_t n = 0, cap = 0, i;
|
||||
struct st_error *err = NULL;
|
||||
|
||||
if (d == NULL) return NULL; /* missing dir: silent no-op */
|
||||
while ((e = readdir(d)) != NULL) {
|
||||
size_t nlen;
|
||||
|
||||
if (e->d_name[0] == '.') continue; /* ., .. and dotfiles */
|
||||
nlen = strlen(e->d_name);
|
||||
if (nlen < 4 || strcmp(e->d_name + nlen - 4, ".lua") != 0)
|
||||
continue;
|
||||
if (n == cap) {
|
||||
size_t ncap = cap == 0 ? 16 : cap * 2;
|
||||
char **nn = realloc(names, ncap * sizeof *nn);
|
||||
|
||||
if (nn == NULL) {
|
||||
err = st_error_internal("extension load failed: "
|
||||
"out of memory");
|
||||
break;
|
||||
}
|
||||
names = nn;
|
||||
cap = ncap;
|
||||
}
|
||||
names[n] = strdup(e->d_name);
|
||||
if (names[n] == NULL) {
|
||||
err = st_error_internal("extension load failed: "
|
||||
"out of memory");
|
||||
break;
|
||||
}
|
||||
n++;
|
||||
}
|
||||
(void)closedir(d);
|
||||
if (err != NULL) {
|
||||
for (i = 0; i < n; i++) free(names[i]);
|
||||
free(names);
|
||||
return err;
|
||||
}
|
||||
qsort(names, n, sizeof *names, cmp_str);
|
||||
for (i = 0; i < n && err == NULL; i++) {
|
||||
char *path;
|
||||
struct stat st;
|
||||
size_t dlen = strlen(dir);
|
||||
size_t nlen = strlen(names[i]);
|
||||
|
||||
while (dlen > 0 && dir[dlen - 1] == '/') dlen--;
|
||||
path = malloc(dlen + 1 + nlen + 1);
|
||||
if (path == NULL) {
|
||||
err = st_error_internal("extension load failed: "
|
||||
"out of memory");
|
||||
break;
|
||||
}
|
||||
memcpy(path, dir, dlen);
|
||||
path[dlen] = '/';
|
||||
memcpy(path + dlen + 1, names[i], nlen + 1);
|
||||
if (stat(path, &st) != 0 || !S_ISREG(st.st_mode)) {
|
||||
/* vanished between readdir and stat, or non-regular
|
||||
* (a directory named *.lua etc.): skip */
|
||||
free(path);
|
||||
continue;
|
||||
}
|
||||
err = load_module(rt, path);
|
||||
free(path);
|
||||
}
|
||||
for (i = 0; i < n; i++) free(names[i]);
|
||||
free(names);
|
||||
return err;
|
||||
}
|
||||
|
||||
/* STUPIDTOOLS_EXT: colon-separated directory list (no escaping); empty
|
||||
* entries are skipped so a trailing colon is harmless. */
|
||||
static struct st_error *
|
||||
discover_env_dirs(struct st_lua_rt *rt)
|
||||
{
|
||||
const char *env = getenv("STUPIDTOOLS_EXT");
|
||||
char *copy;
|
||||
char *save = NULL;
|
||||
char *tok;
|
||||
struct st_error *err = NULL;
|
||||
|
||||
if (env == NULL || env[0] == '\0') return NULL;
|
||||
copy = strdup(env);
|
||||
if (copy == NULL)
|
||||
return st_error_internal("extension load failed: out of memory");
|
||||
for (tok = strtok_r(copy, ":", &save); tok != NULL && err == NULL;
|
||||
tok = strtok_r(NULL, ":", &save)) {
|
||||
if (tok[0] != '\0') err = discover_dir(rt, tok);
|
||||
}
|
||||
free(copy);
|
||||
return err;
|
||||
}
|
||||
|
||||
/* User extension dir: $XDG_DATA_HOME/stupidtools/ext (canonical), else
|
||||
* $HOME/.local/share/stupidtools/ext; skipped when neither is usable. */
|
||||
static struct st_error *
|
||||
discover_user_dir(struct st_lua_rt *rt)
|
||||
{
|
||||
const char *xdg = getenv("XDG_DATA_HOME");
|
||||
const char *home;
|
||||
char *dir;
|
||||
size_t need;
|
||||
struct st_error *err;
|
||||
|
||||
if (xdg == NULL || xdg[0] == '\0') {
|
||||
home = getenv("HOME");
|
||||
if (home == NULL || home[0] == '\0') return NULL;
|
||||
need = strlen(home) + sizeof "/.local/share/stupidtools/ext";
|
||||
dir = malloc(need);
|
||||
if (dir == NULL)
|
||||
return st_error_internal("extension load failed: "
|
||||
"out of memory");
|
||||
(void)snprintf(dir, need, "%s/.local/share/stupidtools/ext",
|
||||
home);
|
||||
} else {
|
||||
need = strlen(xdg) + sizeof "/stupidtools/ext";
|
||||
dir = malloc(need);
|
||||
if (dir == NULL)
|
||||
return st_error_internal("extension load failed: "
|
||||
"out of memory");
|
||||
(void)snprintf(dir, need, "%s/stupidtools/ext", xdg);
|
||||
}
|
||||
err = discover_dir(rt, dir);
|
||||
free(dir);
|
||||
return err;
|
||||
}
|
||||
|
||||
/* ---------------- public entry point ---------------- */
|
||||
|
||||
struct st_error *
|
||||
st_ext_discover(struct st_ext_ctx *ctx, struct st_lua_rt *rt,
|
||||
const char *const *extra_dirs, size_t n_dirs)
|
||||
{
|
||||
struct st_error *err;
|
||||
size_t i;
|
||||
|
||||
if (rt == NULL) return st_error_internal("st_ext_discover: NULL runtime");
|
||||
if (ctx == NULL) return st_error_usage("st_ext_discover: NULL extension context");
|
||||
|
||||
/* Builtin C/C++ language modules first -- the foundation; the
|
||||
* builtin-ext DIRECTORY is scanned below with the other dirs. */
|
||||
err = st_ext_init_builtins(ctx);
|
||||
if (err != NULL) return err;
|
||||
log_builtins(ctx);
|
||||
|
||||
for (i = 0; i < n_dirs && err == NULL; i++) {
|
||||
if (extra_dirs[i] != NULL && extra_dirs[i][0] != '\0')
|
||||
err = discover_dir(rt, extra_dirs[i]);
|
||||
}
|
||||
if (err != NULL) return err;
|
||||
|
||||
err = discover_env_dirs(rt);
|
||||
if (err != NULL) return err;
|
||||
|
||||
err = discover_user_dir(rt);
|
||||
if (err != NULL) return err;
|
||||
|
||||
return discover_dir(rt, STUPIDTOOLS_BUILTIN_EXT_DIR);
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
#ifndef ST_EXT_DISCOVERY_H
|
||||
#define ST_EXT_DISCOVERY_H
|
||||
|
||||
/*
|
||||
* Extension discovery and loading (plan todo 20).
|
||||
*
|
||||
* st_ext_discover() loads every extension known to a stupidtools run
|
||||
* into a freshly created ABI context + sandboxed Lua runtime. Search
|
||||
* order:
|
||||
*
|
||||
* 1. the BUILTIN C language modules -- st_ext_init_builtins() puts
|
||||
* the "c"/"cxx" languages (and their checks) into the ABI ctx;
|
||||
* 2. each --ext-dir directory (extra_dirs, in the given order);
|
||||
* 3. each STUPIDTOOLS_EXT directory (colon-separated, empty entries
|
||||
* skipped);
|
||||
* 4. the USER directory: $XDG_DATA_HOME/stupidtools/ext, falling
|
||||
* back to $HOME/.local/share/stupidtools/ext when XDG_DATA_HOME is
|
||||
* unset or empty; skipped entirely when neither variable yields a
|
||||
* path. XDG_DATA_HOME is the canonical convention.
|
||||
* 5. the BUILTIN directory: the STUPIDTOOLS_BUILTIN_EXT_DIR macro
|
||||
* (default "builtin-ext", resolved relative to the current working
|
||||
* directory of the running binary; an installed layout overrides
|
||||
* it at compile time, e.g.
|
||||
* -DSTUPIDTOOLS_BUILTIN_EXT_DIR='"<prefix>/share/stupidtools/ext"').
|
||||
*
|
||||
* Directory scanning contract:
|
||||
* - only REGULAR files whose name ends in ".lua" are loaded (dotfiles
|
||||
* excluded). Entries arrive from readdir() as bare basenames -- no
|
||||
* directory entry can contain a path separator, so a malicious
|
||||
* name like "../evil.lua" cannot occur, let alone escape the dir.
|
||||
* Symlinks are followed (stat); pointing one at a file outside the
|
||||
* directory is the caller's own responsibility in an explicitly
|
||||
* configured dir.
|
||||
* - within a directory, modules load in bytewise-lexicographic order
|
||||
* (deterministic, independent of readdir order);
|
||||
* - a directory that does not exist is silently skipped (a no-op --
|
||||
* env lists routinely contain stale paths); an empty directory is
|
||||
* likewise a no-op; an EMPTY .lua file loads as an empty chunk and
|
||||
* is a clean no-op;
|
||||
* - each module is read into memory and run via st_lua_run() with the
|
||||
* full path as chunkname, so Lua diagnostics name file AND line.
|
||||
* Note: st_lua_run() compiles strlen(chunk) bytes, so a file with
|
||||
* embedded NULs is truncated at the first NUL before compilation;
|
||||
* binary garbage then fails with a syntax error naming the file.
|
||||
*
|
||||
* Failure contract: FAIL-FAST. The first module that fails to load
|
||||
* (syntax error, runtime error, sandbox violation, unreadable file)
|
||||
* aborts discovery; the returned st_error's message starts with
|
||||
* "extension load failed: <path>: ..." and preserves the underlying
|
||||
* category (sandbox violations stay ST_ERR_KDL_SCHEMA, other Lua
|
||||
* failures ST_ERR_INTERNAL, read failures ST_ERR_IO). Later modules
|
||||
* are NOT attempted. Deliberate choice: a broken extension should be
|
||||
* loud, and a half-loaded extension set is a confusing state.
|
||||
*
|
||||
* Lifecycle notes:
|
||||
* - st_ext_discover() calls st_ext_init_builtins() itself; call it
|
||||
* ONCE per ctx (a second call re-registers the builtins and fails
|
||||
* on duplicate registrations);
|
||||
* - the ABI ctx receives the builtin modules only; Lua registrations
|
||||
* stay in the runtime's own registries (st_lua_check_* /
|
||||
* st_lua_language_*) -- todo 21 bridges them into the ctx.
|
||||
*
|
||||
* Logging: st_ext_discover_log() prints to stderr by default;
|
||||
* st_ext_discover_set_log() installs a vfprintf-style callback (NULL
|
||||
* restores the default). Discovery logs one line per loaded module:
|
||||
*
|
||||
* loaded extension: <path> — checks: <n> (<names>) languages: <m> (<names>)
|
||||
*
|
||||
* where the counts and names come from the ACTUAL registrations the
|
||||
* module performed (enumerated from the runtime), never from the file
|
||||
* name alone. The builtin modules log one line of the same shape.
|
||||
*/
|
||||
|
||||
#include <stdarg.h>
|
||||
#include <stddef.h>
|
||||
|
||||
#include "error.h"
|
||||
|
||||
struct st_ext_ctx;
|
||||
struct st_lua_rt;
|
||||
|
||||
/* vfprintf-style log sink (NULL restores the default stderr logger). */
|
||||
typedef void (*st_ext_log_fn)(const char *fmt, va_list ap);
|
||||
|
||||
void st_ext_discover_set_log(st_ext_log_fn fn);
|
||||
void st_ext_discover_log(const char *fmt, ...);
|
||||
|
||||
/* Returns NULL on success. `extra_dirs` are the --ext-dir values (may
|
||||
* be NULL/0); see the header contract for the full discovery order and
|
||||
* the fail-fast failure semantics. */
|
||||
struct st_error *st_ext_discover(struct st_ext_ctx *ctx, struct st_lua_rt *rt,
|
||||
const char *const *extra_dirs,
|
||||
size_t n_dirs);
|
||||
|
||||
#endif /* ST_EXT_DISCOVERY_H */
|
||||
+59
@@ -7,17 +7,68 @@
|
||||
* Entrypoint: parse argv via cli.c, dispatch --help/--version/usage
|
||||
* errors, and reserve the `stupidtools <buildfile>` shape. Buildfile
|
||||
* processing arrives in later todos (16+).
|
||||
*
|
||||
* Interim extension pipeline (todo 20): with --ext-dir flags or a
|
||||
* STUPIDTOOLS_EXT environment, run extension discovery, print what
|
||||
* loaded, and exit 0 -- until todo 23 folds discovery into the full
|
||||
* buildfile pipeline.
|
||||
*/
|
||||
|
||||
#include "cli.h"
|
||||
|
||||
#include "ext/abi.h"
|
||||
#include "ext/discovery.h"
|
||||
#include "ext/lua.h"
|
||||
|
||||
#include <stdarg.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
|
||||
/* Discovery's log sink: route the "loaded extension: ..." lines to
|
||||
* stdout so scripts can grep them cleanly. */
|
||||
static void
|
||||
stdout_log(const char *fmt, va_list ap)
|
||||
{
|
||||
vfprintf(stdout, fmt, ap);
|
||||
}
|
||||
|
||||
/* Creates a fresh ctx + runtime, discovers everything (builtin modules
|
||||
* first, then the --ext-dir dirs, STUPIDTOOLS_EXT, the user dir and the
|
||||
* builtin dir -- see ext/discovery.h), and prints each loaded module.
|
||||
* Returns CLI_EXIT_OK on success; a load error prints its diagnostic to
|
||||
* stderr and returns CLI_EXIT_RUNTIME (fail-fast contract). */
|
||||
static int
|
||||
run_extension_discovery(const struct cli_opts *opts)
|
||||
{
|
||||
struct st_ext_ctx *ctx = st_ext_ctx_new();
|
||||
struct st_lua_rt *rt = st_lua_rt_new();
|
||||
struct st_error *err = NULL;
|
||||
int rc = CLI_EXIT_RUNTIME;
|
||||
|
||||
if (ctx == NULL || rt == NULL) {
|
||||
fprintf(stderr, "%s: error: out of memory\n", opts->program);
|
||||
} else {
|
||||
st_ext_discover_set_log(stdout_log);
|
||||
err = st_ext_discover(ctx, rt, opts->ext_dirs,
|
||||
opts->ext_dir_count);
|
||||
if (err != NULL) {
|
||||
st_error_print(stderr, err);
|
||||
st_error_free(err);
|
||||
} else {
|
||||
rc = CLI_EXIT_OK;
|
||||
}
|
||||
}
|
||||
st_lua_rt_free(rt);
|
||||
st_ext_ctx_free(ctx);
|
||||
return rc;
|
||||
}
|
||||
|
||||
int
|
||||
main(int argc, char **argv)
|
||||
{
|
||||
struct cli_opts opts;
|
||||
enum cli_action action;
|
||||
const char *env;
|
||||
|
||||
action = cli_parse(argc, argv, &opts);
|
||||
|
||||
@@ -33,6 +84,14 @@ main(int argc, char **argv)
|
||||
break;
|
||||
}
|
||||
|
||||
if (opts.ext_dir_count > 0) {
|
||||
return run_extension_discovery(&opts);
|
||||
}
|
||||
env = getenv("STUPIDTOOLS_EXT");
|
||||
if (env != NULL && env[0] != '\0') {
|
||||
return run_extension_discovery(&opts);
|
||||
}
|
||||
|
||||
if (opts.buildfile == NULL) {
|
||||
fprintf(stderr, "%s: error: no build file given\n",
|
||||
opts.program);
|
||||
|
||||
Reference in New Issue
Block a user