From 2e8553116bc52a3d1492b342e81e182f99090360 Mon Sep 17 00:00:00 2001 From: huntedbytheirs Date: Fri, 28 Aug 2026 21:51:59 -0400 Subject: [PATCH] feat(ext): expose Lua API for checks and languages --- src/ext/api.c | 266 ++++++++++++++++++++++++++++ src/ext/api.h | 140 +++++++++++++++ src/ext/lua.c | 159 ++++++++++++++++- tests/unit/test_api.c | 402 ++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 961 insertions(+), 6 deletions(-) create mode 100644 src/ext/api.c create mode 100644 src/ext/api.h create mode 100644 tests/unit/test_api.c diff --git a/src/ext/api.c b/src/ext/api.c new file mode 100644 index 0000000..c962f71 --- /dev/null +++ b/src/ext/api.c @@ -0,0 +1,266 @@ +/* + * src/ext/api.c - the Lua API over the extension ABI (plan todo 21). + * + * See src/ext/api.h for the full contract: st_ext_bridge_lua() flushes a + * Lua runtime's registrations into a struct st_ext_ctx (thin shim over + * the todo-13 ABI -- no registry reimplementation, no DSL mutation), and + * st_lua_probe_run() resolves a probe spec by driving the detected C + * toolchain through st_ext_run_capture (argv only, never a shell). + * + * The probe run here is a DIRECT compile/link/run for the acceptance + * test only. Feature resolution (todo 14) and configure-script probe + * generation (todo 12) consume the registered checks later; this file + * stops at "run a spec against a toolchain and report pass/fail". + */ + +#ifndef _POSIX_C_SOURCE +#define _POSIX_C_SOURCE 200809L /* mkstemp, write, unlink, strdup */ +#endif + +#include "api.h" + +#include +#include +#include + +/* The language v1 custom checks compile against: custom checks are C + * snippets, so they belong to the builtin "c" module's toolchain. This + * is the one language string this bridge knows; it is NOT a core string + * (core never sees it -- api.c is extension territory, like lang_c.c). */ +#define ST_LUA_CHECK_LANGUAGE "c" + +/* ------------------------------------------------------------------ */ +/* language detect stub */ +/* ------------------------------------------------------------------ */ + +/* A Lua-registered language has no generic detection in v1: there is no + * way for an arbitrary extension to express "here is my compiler" that + * survives the sandbox (which has no process/io surface). Real detection + * for third-party languages lands in todo 22 (example fortran module). + * Until then detecting a Lua language is a clean usage error -- a Lua + * language still REGISTERS (lands in the ctx, enumerable, name visible) + * but its toolchain cannot be detected. */ +static struct st_error * +lua_lang_detect(struct st_ext_ctx *ctx, void *module_ctx, + struct st_toolchain *out) +{ + (void)ctx; + (void)module_ctx; + (void)out; + return st_error_usage("Lua-registered language has no detection yet " + "(todo 22)"); +} + +/* ------------------------------------------------------------------ */ +/* bridge */ +/* ------------------------------------------------------------------ */ + +struct st_error * +st_ext_bridge_lua(struct st_ext_ctx *ctx, struct st_lua_rt *rt) +{ + struct st_error *err; + size_t i, n; + + if (ctx == NULL || rt == NULL) { + return st_error_usage("st_ext_bridge_lua: NULL argument"); + } + + /* Languages first, then checks (checks reference a language; the + * builtin "c" module is already registered by discovery/init). */ + n = st_lua_language_count(rt); + for (i = 0; i < n; i++) { + const char *name = st_lua_language_name(rt, i); + + err = st_ext_register_language(ctx, name, lua_lang_detect, NULL, + NULL); + if (err != NULL) { + return err; + } + } + + n = st_lua_check_count(rt); + for (i = 0; i < n; i++) { + const char *name = st_lua_check_name(rt, i); + void *spec = st_lua_check_probe_spec(rt, i); + + /* The Lua check NAME becomes the ABI check KIND; the spec's own + * `kind` field (compile/link/run) is the PROBE MODE and stays + * inside the spec. probe_spec is BORROWED (owned by the rt -- + * see api.h ownership note). */ + err = st_ext_register_check(ctx, ST_LUA_CHECK_LANGUAGE, name, spec); + if (err != NULL) { + return err; + } + } + return NULL; +} + +/* ------------------------------------------------------------------ */ +/* probe run */ +/* ------------------------------------------------------------------ */ + +/* Grow a heap argv by one element (kept NULL-terminated). Returns 0 or + * -1 on OOM. */ +static int +argv_push(char ***argv, size_t *argc, const char *s) +{ + char **na = realloc(*argv, (*argc + 2) * sizeof *na); + + if (na == NULL) { + return -1; + } + *argv = na; + (*argv)[(*argc)++] = (char *)s; + (*argv)[*argc] = NULL; + return 0; +} + +/* Copy the captured compiler output into *errbuf (or free it). Sets + * *errbuf = NULL when the probe passed or there was nothing captured. */ +static void +set_errbuf(char **errbuf, char *captured, int passed) +{ + if (errbuf == NULL) { + free(captured); + return; + } + if (passed) { + free(captured); + *errbuf = NULL; + } else if (captured != NULL) { + *errbuf = captured; /* hand ownership to the caller */ + } else { + *errbuf = strdup("probe failed (no compiler output)"); + } +} + +int +st_lua_probe_run(const struct st_toolchain *tc, + const struct st_lua_probe_spec *spec, char **errbuf) +{ + char src_tmpl[] = "/tmp/stprobe_src_XXXXXX"; + char out_tmpl[] = "/tmp/stprobe_out_XXXXXX"; + int sfd; + int ofd; + size_t off; + size_t slen; + size_t nlink = 0; + size_t i; + size_t argc = 0; + char **argv = NULL; + char *captured = NULL; + size_t captured_len = 0; + int compile_only; + int run_it; + int rc; + + if (errbuf != NULL) { + *errbuf = NULL; + } + if (tc == NULL || tc->path == NULL || tc->path[0] == '\0' || + spec == NULL || spec->kind == NULL || spec->source == NULL) { + if (errbuf != NULL) { + *errbuf = strdup("probe: missing toolchain or spec"); + } + return -1; + } + + compile_only = strcmp(spec->kind, "compile") == 0; + run_it = strcmp(spec->kind, "run") == 0; + + slen = strlen(spec->source); + if (spec->link != NULL) { + while (spec->link[nlink] != NULL) { + nlink++; + } + } + + /* Write the source to a unique temp file. The "-x c" flag below (not + * a ".c" suffix -- mkstemp needs the template to END in XXXXXX, so no + * suffix is possible) forces the compiler to classify it as C: an + * extensionless file is otherwise treated as a linker input and would + * "pass" every probe. mkstemp -> no shell, no path collision; the + * source's shell metacharacters are just bytes in a file. */ + sfd = mkstemp(src_tmpl); + if (sfd < 0) { + if (errbuf != NULL) { + *errbuf = strdup("probe: cannot create temp source file"); + } + return -1; + } + /* A unique output path (the file is created empty; cc overwrites it). + * Non-fatal if it fails: the literal template still names a usable + * path and every probe unlinks it afterwards. */ + ofd = mkstemp(out_tmpl); + if (ofd >= 0) { + (void)close(ofd); + } + off = 0; + while (off < slen) { + ssize_t w = write(sfd, spec->source + off, slen - off); + + if (w < 0) { + if (errno == EINTR) { + continue; + } + break; + } + off += (size_t)w; + } + (void)close(sfd); + if (off != slen) { + (void)unlink(src_tmpl); + if (errbuf != NULL) { + *errbuf = strdup("probe: cannot write temp source file"); + } + return -1; + } + + /* -x c [-c] -o [link...] */ + if (argv_push(&argv, &argc, tc->path) != 0 || + argv_push(&argv, &argc, "-x") != 0 || + argv_push(&argv, &argc, "c") != 0 || + (compile_only && argv_push(&argv, &argc, "-c") != 0) || + argv_push(&argv, &argc, src_tmpl) != 0 || + argv_push(&argv, &argc, "-o") != 0 || + argv_push(&argv, &argc, out_tmpl) != 0) { + goto oom; + } + for (i = 0; i < nlink; i++) { + if (argv_push(&argv, &argc, spec->link[i]) != 0) { + goto oom; + } + } + + /* compile (and link) -- `out_tmpl` already exists from the mkstemp + * placeholder above; cc overwrites it. */ + rc = st_ext_run_capture(argv, &captured, &captured_len); + + if (rc == 0 && run_it) { + char *runargv[2] = { out_tmpl, NULL }; + char *rcap = NULL; + size_t rclen = 0; + + free(captured); + captured = NULL; + captured_len = 0; + rc = st_ext_run_capture(runargv, &rcap, &rclen); + free(rcap); + } + + free(argv); + (void)unlink(out_tmpl); + (void)unlink(src_tmpl); + set_errbuf(errbuf, captured, rc == 0); + return rc == 0 ? 1 : 0; + +oom: + free(argv); + (void)unlink(out_tmpl); + (void)unlink(src_tmpl); + free(captured); + if (errbuf != NULL) { + *errbuf = strdup("probe: out of memory"); + } + return -1; +} diff --git a/src/ext/api.h b/src/ext/api.h new file mode 100644 index 0000000..32485e9 --- /dev/null +++ b/src/ext/api.h @@ -0,0 +1,140 @@ +#ifndef ST_EXT_API_H +#define ST_EXT_API_H + +/* + * The Lua API over the extension ABI (plan todo 21). + * + * Todo 19 shipped the sandboxed Lua runtime and let extensions RECORD + * registrations into the runtime's own lists (st_lua_check_* / + * st_lua_language_*). Todo 13 shipped the extension ABI (abi.h) with the + * real check/language registries a builtin module (lang_c.c / lang_cpp.c) + * registers into. THIS header bridges the two: it flushes a Lua runtime's + * registrations into a struct st_ext_ctx so a Lua-registered check or + * language behaves exactly like a builtin. + * + * The bridge (st_ext_bridge_lua) is a thin shim -- it does NOT reimplement + * the registry in Lua and does NOT let extensions mutate the DSL grammar: + * + * - each Lua check -> st_ext_register_check(ctx, "c", name, spec) + * - each Lua language -> st_ext_register_language(ctx, name, detect, ...) + * + * PROBE SPEC (the one piece of module data this todo adds): + * + * A Lua extension describes a custom check with a Lua table: + * + * st.register_check("magic", { + * kind = "compile", -- "compile" | "link" | "run" + * source = "int main(void){return 0;}", + * link = { "-lfoo", "-lbar" }, -- optional + * }) + * + * which the runtime materializes into the C struct below and stores in + * the runtime registry's opaque `probe_spec` slot. `kind` is the PROBE + * MODE (compile / link / run), not the ABI's check "kind" (that slot + * gets the check's NAME -- see the bridge note). `source` is a C + * snippet; `link` are extra argv elements for the link step. v1 keeps + * everything STRINGS: no Lua functions as probe callbacks (documented + * limitation; a later todo may allow functions). + * + * RESOLVING a spec = compiling `source` with the detected C toolchain + * and asserting the compile/link/run succeeded. st_lua_probe_run() does + * exactly that (driving the compiler through st_ext_run_capture, argv + * only -- never a shell string) and is what the acceptance test uses + * directly. The registry-driven resolution that todo 14/16 perform is + * OUT of scope here; those todos consume the registered checks and call + * the probe runner (or its generated-configure equivalent, todo 12). + * + * OWNERSHIP (the one subtle contract): + * + * A probe spec is heap-allocated by the Lua runtime when it parses the + * table, OWNED BY THE RUNTIME's registry entry, and freed when the + * runtime is freed. The ABI ctx stores only a BORROWED pointer to it + * (abi.h's probe_spec slot is opaque module data the ctx never frees). + * Consequence: the ctx must not be used to read probe specs after the + * runtime that produced them is freed. A bridge call is therefore + * expected to be followed by (ctx, rt) being used together and freed in + * that order (rt last). This mirrors how lang_c.c's probe_spec would be + * owned by the module that registered it. + * + * Why st_lua_probe_spec_free is a STATIC INLINE here rather than a + * function in api.c: the Lua runtime (lua.c, todo 19) must free specs it + * owns, but tests/unit/test_lua.c and test_discovery.c link lua.c WITHOUT + * api.c. A static inline in this header keeps lua.c free of any api.c + * symbol reference while still sharing one spec layout. Do NOT move the + * free into api.c without also fixing those two frozen test link lines. + */ + +#include + +#include "abi.h" +#include "lua.h" + +/* A custom check's probe spec, materialized from the Lua table + * { kind = "compile"|"link"|"run", source = "", + * link = { "-lfoo", ... } } (link optional). All fields are owned + * (strdup'd) and freed together by st_lua_probe_spec_free(). */ +struct st_lua_probe_spec { + char *kind; /* "compile" | "link" | "run" */ + char *source; /* the C snippet (NUL-terminated, owned) */ + char **link; /* NULL-terminated extra link args, or NULL when absent */ +}; + +/* Frees a spec (all fields). NULL is a safe no-op. Static inline so the + * runtime (lua.c) can call it without linking api.c. */ +static inline void +st_lua_probe_spec_free(struct st_lua_probe_spec *spec) +{ + size_t i; + + if (spec == NULL) { + return; + } + free(spec->kind); + free(spec->source); + if (spec->link != NULL) { + for (i = 0; spec->link[i] != NULL; i++) { + free(spec->link[i]); + } + free(spec->link); + } + free(spec); +} + +/* Flushes the Lua runtime's registrations into the ABI ctx: + * + * languages: st_ext_register_language(ctx, name, lua_lang_detect, ...) + * checks: st_ext_register_check(ctx, "c", name, probe_spec) + * + * where "c" is the language a v1 custom check compiles against (v1 custom + * checks are C snippets; the C toolchain -- builtin "c" module -- is what + * st_lua_probe_run drives). The language detect fn is a documented v1 + * stub (Lua languages have no generic detection yet; todo 22 wires it). + * + * Returns NULL on success. Any registration error (duplicate name that + * collides with a builtin, e.g. a Lua check named "header", or a Lua + * language named "c") is returned unchanged. Idempotency is NOT promised: + * a second bridge re-registers the same names and fails on duplicates. */ +struct st_error *st_ext_bridge_lua(struct st_ext_ctx *ctx, + struct st_lua_rt *rt); + +/* Runs a probe spec against a detected C toolchain (`tc->path` is the + * compiler command, e.g. the CC the builtin "c" module detected): + * + * compile -> -c -o + * link -> -o [link...] + * run -> link, then execute + * + * The source is written to a mkstemp'd file and every argument is passed + * as an argv element through st_ext_run_capture (execvp) -- NEVER through + * a shell, so shell metacharacters in `source` or `link` are inert. + * + * Returns 1 when the required step(s) all exit 0 (probe passed), 0 when + * any step fails (compiler non-zero exit or spawn failure), or -1 on a + * setup error (NULL toolchain/spec, temp-file creation/write failure, or + * OOM). When `errbuf` is non-NULL and the probe does NOT pass, *errbuf is + * set to a malloc'd copy of the captured compiler stderr/stdout (caller + * frees); on success *errbuf is set to NULL. */ +int st_lua_probe_run(const struct st_toolchain *tc, + const struct st_lua_probe_spec *spec, char **errbuf); + +#endif /* ST_EXT_API_H */ diff --git a/src/ext/lua.c b/src/ext/lua.c index bcffb81..9aaaecf 100644 --- a/src/ext/lua.c +++ b/src/ext/lua.c @@ -54,6 +54,14 @@ #include "lua.h" +/* The probe-spec struct (struct st_lua_probe_spec) and its static-inline + * free live in api.h (todo 21). Including it here is deliberate and safe + * under the frozen unit-test link lines: api.h contributes only a struct + * definition + a static inline, so lua.c gains NO reference to any + * api.c/abi.c symbol -- tests/unit/test_lua.c and test_discovery.c, which + * link lua.c WITHOUT api.c/abi.c, keep linking. */ +#include "api.h" + /* ------------------------------------------------------------------ */ /* libm-free math shims */ /* ------------------------------------------------------------------ */ @@ -296,7 +304,8 @@ pow(double x, double y) struct st_lua_reg { struct st_lua_reg *next; char *name; /* owned copy */ - void *probe_spec; /* checks only; always NULL for languages */ + void *probe_spec; /* checks: a struct st_lua_probe_spec * (owned); + languages: always NULL (name only in v1) */ }; struct st_lua_rt { @@ -346,6 +355,7 @@ regs_free(struct st_lua_reg *head) while (p != NULL) { struct st_lua_reg *next = p->next; free(p->name); + st_lua_probe_spec_free((struct st_lua_probe_spec *)p->probe_spec); free(p); p = next; } @@ -515,15 +525,143 @@ l_math_open(lua_State *L) /* registration API (Lua side) */ /* ------------------------------------------------------------------ */ -/* The `st` table: st.register_check(name) / st.register_language(name). - * Both write into the runtime's own registries through the same C API - * todo 21 will enumerate when bridging into a struct st_ext_ctx. The - * rt travels as a light-userdata upvalue -- there is no global state. */ +/* The `st` table: st.register_check(name[, spec]) and + * st.register_language(name[, spec]). Both write into the runtime's own + * registries through the same C API todo 21 enumerates when bridging into + * a struct st_ext_ctx. The rt travels as a light-userdata upvalue -- there + * is no global state. A present second argument (a probe-spec TABLE) is + * parsed into a heap struct st_lua_probe_spec (api.h) and passed through + * as the registry entry's opaque spec pointer. */ + +static void +l_links_free(char **link) +{ + size_t i; + + if (link == NULL) { + return; + } + for (i = 0; link[i] != NULL; i++) { + free(link[i]); + } + free(link); +} + +/* Parses the optional probe-spec table (argument 2) into a heap + * struct st_lua_probe_spec. Returns NULL when argument 2 is absent or + * nil (bare registration); raises a Lua error on a malformed table. + * All fields are strdup'd (so the borrowed lua_tostring pointers never + * outlive their stack slot); the caller owns the result. */ +static struct st_lua_probe_spec * +l_parse_probe_spec(lua_State *L) +{ + struct st_lua_probe_spec *spec; + char *kind_dup = NULL; + char *source_dup = NULL; + char **link = NULL; + size_t nlink = 0; + size_t i; + const char *s; + + if (lua_gettop(L) < 2 || lua_isnoneornil(L, 2)) { + return NULL; + } + luaL_checktype(L, 2, LUA_TTABLE); + + lua_getfield(L, 2, "kind"); + if (lua_type(L, -1) != LUA_TSTRING) { + luaL_error(L, "probe spec 'kind' must be a string"); + return NULL; + } + s = lua_tostring(L, -1); + if (strcmp(s, "compile") != 0 && strcmp(s, "link") != 0 && + strcmp(s, "run") != 0) { + luaL_error(L, "probe spec 'kind' must be \"compile\", \"link\" or " + "\"run\""); + return NULL; + } + kind_dup = strdup(s); + lua_pop(L, 1); + if (kind_dup == NULL) { + luaL_error(L, "out of memory"); + return NULL; + } + + lua_getfield(L, 2, "source"); + if (lua_type(L, -1) != LUA_TSTRING) { + free(kind_dup); + luaL_error(L, "probe spec 'source' must be a string"); + return NULL; + } + s = lua_tostring(L, -1); + source_dup = strdup(s); + lua_pop(L, 1); + if (source_dup == NULL) { + free(kind_dup); + luaL_error(L, "out of memory"); + return NULL; + } + + lua_getfield(L, 2, "link"); + if (!lua_isnoneornil(L, -1)) { + luaL_checktype(L, -1, LUA_TTABLE); + nlink = lua_rawlen(L, -1); + link = calloc(nlink + 1, sizeof *link); + if (link == NULL) { + free(kind_dup); + free(source_dup); + luaL_error(L, "out of memory"); + return NULL; + } + for (i = 0; i < nlink; i++) { + size_t slen; + + lua_geti(L, -1, (lua_Integer)i + 1); + s = lua_tostring(L, -1); + slen = lua_rawlen(L, -1); + if (lua_type(L, -1) != LUA_TSTRING || s == NULL || + strlen(s) != slen) { + lua_pop(L, 1); + l_links_free(link); + free(kind_dup); + free(source_dup); + luaL_error(L, "probe spec 'link' entries must be strings " + "without NUL bytes"); + return NULL; + } + link[i] = strdup(s); + lua_pop(L, 1); + if (link[i] == NULL) { + l_links_free(link); + free(kind_dup); + free(source_dup); + luaL_error(L, "out of memory"); + return NULL; + } + } + link[nlink] = NULL; + } + lua_pop(L, 1); + + spec = malloc(sizeof *spec); + if (spec == NULL) { + l_links_free(link); + free(kind_dup); + free(source_dup); + luaL_error(L, "out of memory"); + return NULL; + } + spec->kind = kind_dup; + spec->source = source_dup; + spec->link = link; + return spec; +} static int l_st_register_check(lua_State *L) { struct st_lua_rt *rt = lua_touserdata(L, lua_upvalueindex(1)); + struct st_lua_probe_spec *spec; size_t len; const char *name; @@ -535,7 +673,9 @@ l_st_register_check(lua_State *L) if (name == NULL || strlen(name) != len) { return luaL_error(L, "check name must not contain a NUL byte"); } - if (st_lua_register_check(rt, name, NULL) != 0) { + spec = l_parse_probe_spec(L); + if (st_lua_register_check(rt, name, spec) != 0) { + st_lua_probe_spec_free(spec); /* not stored: free it */ return luaL_error(L, "check '%s' is already registered", name); } return 0; @@ -556,6 +696,13 @@ l_st_register_language(lua_State *L) if (name == NULL || strlen(name) != len) { return luaL_error(L, "language name must not contain a NUL byte"); } + /* Optional second arg: accept a probe-spec TABLE for forward + * compatibility, but for v1 register the name only -- the table's + * contents (detection info) are consumed by todo 22. A non-table + * second arg is a clean error. */ + if (lua_gettop(L) >= 2 && !lua_isnoneornil(L, 2)) { + luaL_checktype(L, 2, LUA_TTABLE); + } if (st_lua_register_language(rt, name) != 0) { return luaL_error(L, "language '%s' is already registered", name); } diff --git a/tests/unit/test_api.c b/tests/unit/test_api.c new file mode 100644 index 0000000..3e7b787 --- /dev/null +++ b/tests/unit/test_api.c @@ -0,0 +1,402 @@ +/* LINK: ../../src/ext/api.c ../../src/ext/lua.c ../../src/ext/abi.c ../../src/ext/lang_c.c ../../src/ext/lang_cpp.c ../../src/error.c ../../src/span.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 */ + +#ifndef _POSIX_C_SOURCE +#define _POSIX_C_SOURCE 200809L /* setenv/unsetenv, access */ +#endif + +/* + * tests/unit/test_api.c + * + * Unit tests for the Lua API over the extension ABI (src/ext/api.h, + * plan todo 21): st_ext_bridge_lua flushes a Lua runtime's registrations + * into a struct st_ext_ctx, and st_lua_probe_run resolves a probe spec + * against a detected C toolchain. + * + * The magic LINK comment on line 1 is REQUIRED by tests/run.sh: it lists + * the extra .c sources compiled into this test binary (paths relative to + * tests/unit/). It links api.c, the sandboxed Lua runtime (lua.c + the + * same 27 vendored Lua sources as test_lua.c, minus linit/lmathlib/ + * loadlib/liolib/loslib/lua.c/luac.c -- see the -lm note there), the + * extension ABI + builtin C/C++ modules, and error/span. + */ +#include "munit.h" + +#include "ext/abi.h" +#include "ext/api.h" +#include "ext/lua.h" + +#include +#include +#include + +/* ---------- helpers ---------- */ + +static void +reset_cc_env(void) +{ + unsetenv("CC"); + unsetenv("CFLAGS"); + unsetenv("CXX"); + unsetenv("CXXFLAGS"); +} + +/* Fresh ctx + rt pair with the builtin C/C++ modules already registered + * (a bridge flushes Lua registrations on top of them). */ +static int +fresh_setup(struct st_ext_ctx **ctx, struct st_lua_rt **rt) +{ + *ctx = st_ext_ctx_new(); + *rt = st_lua_rt_new(); + if (*ctx == NULL || *rt == NULL) { + return -1; + } + if (st_ext_init_builtins(*ctx) != NULL) { + return -1; + } + return 0; +} + +/* ---------- (a) a Lua check lands in the ctx after bridging ---------- */ + +static MunitResult +test_bridge_check_visible(const MunitParameter params[], void *data) +{ + struct st_ext_ctx *ctx = NULL; + struct st_lua_rt *rt = NULL; + struct st_error *err; + size_t i, n; + int found = 0; + + (void)params; + (void)data; + + munit_assert_int(fresh_setup(&ctx, &rt), ==, 0); + + err = st_lua_run(rt, + "st.register_check(\"magic\", {kind=\"compile\", " + "source=\"int main(void){return 0;}\"})", + "=api-check"); + munit_assert_null(err); + + err = st_ext_bridge_lua(ctx, rt); + munit_assert_null(err); + + /* builtin "c"/header + "cxx"/header + the bridged "c"/magic = 3 */ + munit_assert_size(st_ext_check_count(ctx), ==, 3); + n = st_ext_check_count(ctx); + for (i = 0; i < n; i++) { + if (strcmp(st_ext_check_kind(ctx, i), "magic") != 0) { + continue; + } + const struct st_lua_probe_spec *spec = + st_ext_check_probe_spec(ctx, i); + + found = 1; + munit_assert_string_equal(st_ext_check_language(ctx, i), "c"); + munit_assert_not_null(spec); + munit_assert_string_equal(spec->kind, "compile"); + munit_assert_not_null(strstr(spec->source, "int main")); + } + munit_assert_int(found, ==, 1); + + /* the ctx borrows the spec from the runtime: free ctx first, rt last */ + st_ext_ctx_free(ctx); + st_lua_rt_free(rt); + return MUNIT_OK; +} + +/* ---------- (b) probe resolves true/false against the detected CC ---------- */ + +static MunitResult +test_probe_true_false(const MunitParameter params[], void *data) +{ + struct st_ext_ctx *ctx = NULL; + struct st_lua_rt *rt = NULL; + struct st_error *err; + const struct st_toolchain *tc; + struct st_lua_probe_spec valid; + struct st_lua_probe_spec invalid; + char *valid_eb = NULL; + char *invalid_eb = NULL; + int rv, ri; + + (void)params; + (void)data; + + munit_assert_int(fresh_setup(&ctx, &rt), ==, 0); + reset_cc_env(); + err = st_ext_detect_language(ctx, "c"); + munit_assert_null(err); + tc = st_ext_language_toolchain_named(ctx, "c"); + munit_assert_not_null(tc); + munit_assert_not_null(tc->path); + + valid.kind = "compile"; + valid.source = "int main(void){return 0;}"; + valid.link = NULL; + invalid.kind = "compile"; + invalid.source = "int main(void){this is not C;}"; + invalid.link = NULL; + + rv = st_lua_probe_run(tc, &valid, &valid_eb); + ri = st_lua_probe_run(tc, &invalid, &invalid_eb); + + munit_assert_int(rv, ==, 1); + munit_assert_null(valid_eb); /* a pass carries no error text */ + munit_assert_int(ri, ==, 0); + /* the false probe must have ACTUALLY failed the compile: the captured + * compiler stderr is non-empty (misleading-success guard) */ + munit_assert_not_null(invalid_eb); + munit_assert_int(strlen(invalid_eb), >, 0); + + free(valid_eb); + free(invalid_eb); + st_ext_ctx_free(ctx); + st_lua_rt_free(rt); + return MUNIT_OK; +} + +/* ---------- (c) a Lua language lands in the ctx after bridging ---------- */ + +static MunitResult +test_language_lands(const MunitParameter params[], void *data) +{ + struct st_ext_ctx *ctx = NULL; + struct st_lua_rt *rt = NULL; + struct st_error *err; + size_t i, n; + int found = 0; + + (void)params; + (void)data; + + munit_assert_int(fresh_setup(&ctx, &rt), ==, 0); + err = st_lua_run(rt, "st.register_language(\"fortran\", {})", + "=api-lang"); + munit_assert_null(err); + err = st_ext_bridge_lua(ctx, rt); + munit_assert_null(err); + + n = st_ext_language_count(ctx); + munit_assert_size(n, ==, 3); /* c, cxx, fortran */ + for (i = 0; i < n; i++) { + if (strcmp(st_ext_language_name(ctx, i), "fortran") == 0) { + found = 1; + } + } + munit_assert_int(found, ==, 1); + + st_ext_ctx_free(ctx); + st_lua_rt_free(rt); + return MUNIT_OK; +} + +/* ---------- (d) a Lua registration colliding with a builtin errors ---------- */ + +static MunitResult +test_duplicate_error(const MunitParameter params[], void *data) +{ + struct st_ext_ctx *ctx = NULL; + struct st_lua_rt *rt = NULL; + struct st_error *err; + const char *msg; + + (void)params; + (void)data; + + /* a Lua check named "header" collides with the builtin "c"/"header" */ + munit_assert_int(fresh_setup(&ctx, &rt), ==, 0); + err = st_lua_run(rt, "st.register_check(\"header\")", "=api-dup"); + munit_assert_null(err); + err = st_ext_bridge_lua(ctx, rt); + munit_assert_not_null(err); + msg = st_error_message(err); + munit_assert_not_null(strstr(msg, "already registered")); + st_error_free(err); + st_ext_ctx_free(ctx); + st_lua_rt_free(rt); + + /* a Lua language named "c" collides with the builtin language */ + munit_assert_int(fresh_setup(&ctx, &rt), ==, 0); + err = st_lua_run(rt, "st.register_language(\"c\")", "=api-dup2"); + munit_assert_null(err); + err = st_ext_bridge_lua(ctx, rt); + munit_assert_not_null(err); + msg = st_error_message(err); + munit_assert_not_null(strstr(msg, "already registered")); + st_error_free(err); + st_ext_ctx_free(ctx); + st_lua_rt_free(rt); + + return MUNIT_OK; +} + +/* ---------- (e) malformed probe-spec tables are clean errors ---------- */ + +static MunitResult +test_malformed_spec_errors(const MunitParameter params[], void *data) +{ + struct st_lua_rt *rt; + struct st_error *err; + const char *msg; + + (void)params; + (void)data; + + rt = st_lua_rt_new(); + munit_assert_not_null(rt); + + /* non-table second argument */ + err = st_lua_run(rt, "st.register_check(\"x\", \"not-a-table\")", + "=api-bad1"); + munit_assert_not_null(err); + msg = st_error_message(err); + munit_assert_not_null(strstr(msg, "table")); + st_error_free(err); + + /* unknown kind */ + err = st_lua_run(rt, + "st.register_check(\"x\", {kind=\"bogus\", " + "source=\"int x;\"})", + "=api-bad2"); + munit_assert_not_null(err); + msg = st_error_message(err); + munit_assert_not_null(strstr(msg, "kind")); + st_error_free(err); + + /* missing source */ + err = st_lua_run(rt, "st.register_check(\"x\", {kind=\"compile\"})", + "=api-bad3"); + munit_assert_not_null(err); + msg = st_error_message(err); + munit_assert_not_null(strstr(msg, "source")); + st_error_free(err); + + /* the runtime stays usable after errors */ + err = st_lua_run(rt, "st.register_check(\"ok\")", "=api-ok"); + munit_assert_null(err); + + st_lua_rt_free(rt); + return MUNIT_OK; +} + +/* ---------- (f) shell metacharacters in a probe source stay inert ---------- */ + +static MunitResult +test_injection_inert(const MunitParameter params[], void *data) +{ + struct st_ext_ctx *ctx = NULL; + struct st_lua_rt *rt = NULL; + struct st_error *err; + const struct st_toolchain *tc; + struct st_lua_probe_spec spec; + static const char *marker = "/tmp/stprobe_injected_marker"; + char *eb = NULL; + int rc; + + (void)params; + (void)data; + + munit_assert_int(fresh_setup(&ctx, &rt), ==, 0); + reset_cc_env(); + err = st_ext_detect_language(ctx, "c"); + munit_assert_null(err); + tc = st_ext_language_toolchain_named(ctx, "c"); + munit_assert_not_null(tc); + + (void)unlink(marker); + + spec.kind = "compile"; + spec.source = "int main(void){return 0;}\" ; touch " + "/tmp/stprobe_injected_marker ; \""; + spec.link = NULL; + + /* not valid C -> the probe fails; the injection must NOT have run */ + rc = st_lua_probe_run(tc, &spec, &eb); + munit_assert_int(rc, ==, 0); + munit_assert_int(access(marker, F_OK), ==, -1); + + free(eb); + (void)unlink(marker); + st_ext_ctx_free(ctx); + st_lua_rt_free(rt); + return MUNIT_OK; +} + +/* ---------- (g) link/run probe modes and the link[] flag passthrough ---------- */ + +static MunitResult +test_probe_modes(const MunitParameter params[], void *data) +{ + struct st_ext_ctx *ctx = NULL; + struct st_lua_rt *rt = NULL; + struct st_error *err; + const struct st_toolchain *tc; + struct st_lua_probe_spec link; + struct st_lua_probe_spec run; + struct st_lua_probe_spec runfail; + struct st_lua_probe_spec badlink; + static char *bad_link[] = { "-l__stprobe_nonexistent_lib__", NULL }; + + (void)params; + (void)data; + + munit_assert_int(fresh_setup(&ctx, &rt), ==, 0); + reset_cc_env(); + err = st_ext_detect_language(ctx, "c"); + munit_assert_null(err); + tc = st_ext_language_toolchain_named(ctx, "c"); + munit_assert_not_null(tc); + + link.kind = "link"; + link.source = "int main(void){return 0;}"; + link.link = NULL; + run.kind = "run"; + run.source = "int main(void){return 0;}"; + run.link = NULL; + runfail.kind = "run"; + runfail.source = "int main(void){return 7;}"; + runfail.link = NULL; + badlink.kind = "link"; + badlink.source = "int main(void){return 0;}"; + badlink.link = bad_link; + + munit_assert_int(st_lua_probe_run(tc, &link, NULL), ==, 1); + munit_assert_int(st_lua_probe_run(tc, &run, NULL), ==, 1); + munit_assert_int(st_lua_probe_run(tc, &runfail, NULL), ==, 0); + /* a bogus -l flag reaches the linker and fails -> the link[] argv + * elements are genuinely passed through */ + munit_assert_int(st_lua_probe_run(tc, &badlink, NULL), ==, 0); + + st_ext_ctx_free(ctx); + st_lua_rt_free(rt); + return MUNIT_OK; +} + +static MunitTest tests[] = { + { "/api/bridge-check-visible", test_bridge_check_visible, NULL, NULL, + MUNIT_TEST_OPTION_NONE, NULL }, + { "/api/probe-true-false", test_probe_true_false, NULL, NULL, + MUNIT_TEST_OPTION_NONE, NULL }, + { "/api/language-lands", test_language_lands, NULL, NULL, + MUNIT_TEST_OPTION_NONE, NULL }, + { "/api/duplicate-error", test_duplicate_error, NULL, NULL, + MUNIT_TEST_OPTION_NONE, NULL }, + { "/api/malformed-spec-errors", test_malformed_spec_errors, NULL, NULL, + MUNIT_TEST_OPTION_NONE, NULL }, + { "/api/injection-inert", test_injection_inert, NULL, NULL, + MUNIT_TEST_OPTION_NONE, NULL }, + { "/api/probe-modes", test_probe_modes, NULL, NULL, + MUNIT_TEST_OPTION_NONE, NULL }, + { NULL, NULL, NULL, NULL, MUNIT_TEST_OPTION_NONE, NULL }, +}; + +static const MunitSuite suite = { + "/api", tests, NULL, 1, MUNIT_SUITE_OPTION_NONE, +}; + +int +main(int argc, char *argv[MUNIT_ARRAY_PARAM(argc + 1)]) +{ + return munit_suite_main(&suite, NULL, argc, argv); +}