From 468aa1c4ef9fa807d3fa160f516aff7444b27160 Mon Sep 17 00:00:00 2001 From: huntedbytheirs Date: Fri, 28 Aug 2026 20:17:58 -0400 Subject: [PATCH] feat(ext): add builtin C/C++ language modules --- src/ext/abi.c | 694 ++++++++++++++++++++++++++++++++++++++++++ src/ext/abi.h | 161 ++++++++++ src/ext/lang_c.c | 44 +++ src/ext/lang_c.h | 20 ++ src/ext/lang_common.h | 225 ++++++++++++++ src/ext/lang_cpp.c | 45 +++ src/ext/lang_cpp.h | 19 ++ tests/unit/test_abi.c | 462 ++++++++++++++++++++++++++++ 8 files changed, 1670 insertions(+) create mode 100644 src/ext/abi.c create mode 100644 src/ext/abi.h create mode 100644 src/ext/lang_c.c create mode 100644 src/ext/lang_c.h create mode 100644 src/ext/lang_common.h create mode 100644 src/ext/lang_cpp.c create mode 100644 src/ext/lang_cpp.h create mode 100644 tests/unit/test_abi.c diff --git a/src/ext/abi.c b/src/ext/abi.c new file mode 100644 index 0000000..7bc808d --- /dev/null +++ b/src/ext/abi.c @@ -0,0 +1,694 @@ +#ifndef _POSIX_C_SOURCE +#define _POSIX_C_SOURCE 200809L /* strdup, fork/exec/wait (POSIX.1-2008) */ +#endif + +#include "abi.h" + +#include "lang_c.h" +#include "lang_cpp.h" + +#include +#include +#include +#include +#include +#include +#include + +/* + * Generic extension ABI (plan todo 13). Owns three registries: + * variables (name/value, insertion order), languages, checks. + * + * No language-specific knowledge lives here: the builtin module table + * below names module ENTRY POINTS only; every compiler string lives in + * lang_c.c / lang_cpp.c. All state is reached through an explicitly + * passed `struct st_ext_ctx *` so a future Lua binding (todo 19) only + * wraps these functions. + */ + +/* ---------------- formatting helper ---------------- */ + +/* snprintf-based formatter for our diagnostic messages. Returns a heap + * st_error of the given category. */ +static struct st_error * +errf(enum st_error_category cat, const char *fmt, ...) +{ + va_list ap; + int n; + char *msg; + struct st_error *e; + + va_start(ap, fmt); + n = vsnprintf(NULL, 0, fmt, ap); + va_end(ap); + if (n < 0) { + return st_error_internal("failed to format error message"); + } + msg = malloc((size_t)n + 1); + if (msg == NULL) { + return st_error_internal("out of memory"); + } + va_start(ap, fmt); + vsnprintf(msg, (size_t)n + 1, fmt, ap); + va_end(ap); + e = st_error_new(cat, msg); + free(msg); + return e; +} + +/* ---------------- variable registry ---------------- */ + +struct st_reg_var { + char *name; /* owned */ + char *value; /* owned; never NULL after insertion */ + struct st_reg_var *next; +}; + +struct st_registry { + struct st_reg_var *head; + struct st_reg_var *tail; + size_t count; +}; + +struct st_registry * +st_registry_new(void) +{ + return calloc(1, sizeof(struct st_registry)); +} + +void +st_registry_free(struct st_registry *r) +{ + struct st_reg_var *node, *next; + + if (r == NULL) { + return; + } + for (node = r->head; node != NULL; node = next) { + next = node->next; + free(node->name); + free(node->value); + free(node); + } + free(r); +} + +struct st_error * +st_registry_set_var(struct st_registry *r, const char *name, + const char *value) +{ + struct st_reg_var *node, *prev; + char *dup_name, *dup_value; + + if (r == NULL || name == NULL || name[0] == '\0') { + return st_error_usage("st_registry_set_var: NULL/empty name"); + } + if (value != NULL) { + dup_value = strdup(value); + if (dup_value == NULL) { + return st_error_internal("out of memory"); + } + } else { + dup_value = NULL; + } + + prev = NULL; + for (node = r->head; node != NULL; prev = node, node = node->next) { + if (strcmp(node->name, name) != 0) { + continue; + } + if (dup_value == NULL) { + /* unset: unlink + free */ + if (prev != NULL) { + prev->next = node->next; + } else { + r->head = node->next; + } + if (r->tail == node) { + r->tail = prev; + } + free(node->name); + free(node->value); + free(node); + r->count--; + } else { + free(node->value); + node->value = dup_value; + } + return NULL; + } + + if (dup_value == NULL) { + return NULL; /* unsetting a var that does not exist: no-op */ + } + dup_name = strdup(name); + if (dup_name == NULL) { + free(dup_value); + return st_error_internal("out of memory"); + } + node = calloc(1, sizeof *node); + if (node == NULL) { + free(dup_name); + free(dup_value); + return st_error_internal("out of memory"); + } + node->name = dup_name; + node->value = dup_value; + if (r->tail != NULL) { + r->tail->next = node; + } else { + r->head = node; + } + r->tail = node; + r->count++; + return NULL; +} + +const char * +st_registry_get_var(const struct st_registry *r, const char *name) +{ + const struct st_reg_var *node; + + if (r == NULL || name == NULL) { + return NULL; + } + for (node = r->head; node != NULL; node = node->next) { + if (strcmp(node->name, name) == 0) { + return node->value; + } + } + return NULL; +} + +size_t +st_registry_var_count(const struct st_registry *r) +{ + return r == NULL ? 0 : r->count; +} + +const char * +st_registry_var_name(const struct st_registry *r, size_t i) +{ + const struct st_reg_var *node; + + if (r == NULL) { + return NULL; + } + for (node = r->head; node != NULL && i > 0; node = node->next, i--) { + } + return node != NULL ? node->name : NULL; +} + +const char * +st_registry_var_value(const struct st_registry *r, size_t i) +{ + const struct st_reg_var *node; + + if (r == NULL) { + return NULL; + } + for (node = r->head; node != NULL && i > 0; node = node->next, i--) { + } + return node != NULL ? node->value : NULL; +} + +/* ---------------- toolchain ---------------- */ + +static void +toolchain_reset(struct st_toolchain *tc) +{ + if (tc == NULL) { + return; + } + free(tc->path); + free(tc->id); + free(tc->version); + tc->path = NULL; + tc->id = NULL; + tc->version = NULL; +} + +/* ---------------- language registry ---------------- */ + +struct st_language_node { + struct st_language pub; + char *name_owned; /* pub.name points here */ + char **var_names_owned; /* pub.var_names points here (NULL-term) */ + struct st_toolchain tc; /* pub.toolchain points here when detected */ + struct st_language_node *next; +}; + +struct st_check_node { + struct st_check pub; + char *language_owned; /* pub.language points here */ + char *kind_owned; /* pub.kind points here */ + struct st_check_node *next; +}; + +struct st_ext_ctx { + struct st_registry *vars; + struct st_language_node *langs_head, *langs_tail; + size_t lang_count; + struct st_check_node *checks_head, *checks_tail; + size_t check_count; +}; + +struct st_ext_ctx * +st_ext_ctx_new(void) +{ + struct st_ext_ctx *ctx = calloc(1, sizeof *ctx); + + if (ctx == NULL) { + return NULL; + } + ctx->vars = st_registry_new(); + if (ctx->vars == NULL) { + free(ctx); + return NULL; + } + return ctx; +} + +static void +language_node_free(struct st_language_node *node) +{ + size_t i; + + if (node == NULL) { + return; + } + free(node->name_owned); + if (node->var_names_owned != NULL) { + for (i = 0; node->var_names_owned[i] != NULL; i++) { + free(node->var_names_owned[i]); + } + } + free(node->var_names_owned); + toolchain_reset(&node->tc); + free(node); +} + +void +st_ext_ctx_free(struct st_ext_ctx *ctx) +{ + struct st_language_node *lnode, *lnext; + struct st_check_node *cnode, *cnext; + + if (ctx == NULL) { + return; + } + for (lnode = ctx->langs_head; lnode != NULL; lnode = lnext) { + lnext = lnode->next; + language_node_free(lnode); + } + for (cnode = ctx->checks_head; cnode != NULL; cnode = cnext) { + cnext = cnode->next; + free(cnode->language_owned); + free(cnode->kind_owned); + free(cnode); + } + st_registry_free(ctx->vars); + free(ctx); +} + +struct st_registry * +st_ext_var_registry(struct st_ext_ctx *ctx) +{ + return ctx != NULL ? ctx->vars : NULL; +} + +static struct st_language_node * +find_language(const struct st_ext_ctx *ctx, const char *name) +{ + struct st_language_node *node; + + if (ctx == NULL || name == NULL) { + return NULL; + } + for (node = ctx->langs_head; node != NULL; node = node->next) { + if (strcmp(node->pub.name, name) == 0) { + return node; + } + } + return NULL; +} + +static const struct st_language_node * +find_language_const(const struct st_ext_ctx *ctx, const char *name) +{ + return find_language(ctx, name); +} + +struct st_error * +st_ext_register_language(struct st_ext_ctx *ctx, const char *name, + st_lang_detect_fn detect, void *module_ctx, + const char *const *var_names) +{ + struct st_language_node *node; + size_t n_vars = 0, i; + + if (ctx == NULL || name == NULL || name[0] == '\0' || detect == NULL) { + return st_error_usage("st_ext_register_language: bad argument"); + } + if (find_language(ctx, name) != NULL) { + return errf(ST_ERR_USAGE, "language already registered: %s", name); + } + + node = calloc(1, sizeof *node); + if (node == NULL) { + return st_error_internal("out of memory"); + } + node->name_owned = strdup(name); + if (node->name_owned == NULL) { + language_node_free(node); + return st_error_internal("out of memory"); + } + while (var_names != NULL && var_names[n_vars] != NULL) { + n_vars++; + } + node->var_names_owned = calloc(n_vars + 1, sizeof *node->var_names_owned); + if (node->var_names_owned == NULL) { + language_node_free(node); + return st_error_internal("out of memory"); + } + for (i = 0; i < n_vars; i++) { + node->var_names_owned[i] = strdup(var_names[i]); + if (node->var_names_owned[i] == NULL) { + language_node_free(node); + return st_error_internal("out of memory"); + } + } + + node->pub.name = node->name_owned; + node->pub.detect = detect; + node->pub.module_ctx = module_ctx; + node->pub.var_names = (const char *const *)node->var_names_owned; + node->pub.toolchain = NULL; + + if (ctx->langs_tail != NULL) { + ctx->langs_tail->next = node; + } else { + ctx->langs_head = node; + } + ctx->langs_tail = node; + ctx->lang_count++; + return NULL; +} + +size_t +st_ext_language_count(const struct st_ext_ctx *ctx) +{ + return ctx != NULL ? ctx->lang_count : 0; +} + +const struct st_language * +st_ext_language(const struct st_ext_ctx *ctx, size_t i) +{ + const struct st_language_node *node; + + if (ctx == NULL) { + return NULL; + } + for (node = ctx->langs_head; node != NULL && i > 0; + node = node->next, i--) { + } + return node != NULL ? &node->pub : NULL; +} + +const char * +st_ext_language_name(const struct st_ext_ctx *ctx, size_t i) +{ + const struct st_language *lang = st_ext_language(ctx, i); + + return lang != NULL ? lang->name : NULL; +} + +const struct st_toolchain * +st_ext_language_toolchain(const struct st_ext_ctx *ctx, size_t i) +{ + const struct st_language *lang = st_ext_language(ctx, i); + + return lang != NULL ? lang->toolchain : NULL; +} + +const struct st_toolchain * +st_ext_language_toolchain_named(const struct st_ext_ctx *ctx, + const char *name) +{ + const struct st_language_node *node = find_language_const(ctx, name); + + return node != NULL ? node->pub.toolchain : NULL; +} + +struct st_error * +st_ext_detect_language(struct st_ext_ctx *ctx, const char *name) +{ + struct st_language_node *node = find_language(ctx, name); + struct st_error *err; + + if (node == NULL) { + return errf(ST_ERR_USAGE, "unknown language: %s", name); + } + toolchain_reset(&node->tc); + node->pub.toolchain = NULL; + + err = node->pub.detect(ctx, node->pub.module_ctx, &node->tc); + if (err != NULL) { + toolchain_reset(&node->tc); /* free partial fills */ + return err; + } + node->pub.toolchain = &node->tc; + return NULL; +} + +/* ---------------- check registry ---------------- */ + +static const struct st_check_node * +find_check(const struct st_ext_ctx *ctx, const char *language, + const char *kind) +{ + const struct st_check_node *node; + + if (ctx == NULL || language == NULL || kind == NULL) { + return NULL; + } + for (node = ctx->checks_head; node != NULL; node = node->next) { + if (strcmp(node->pub.language, language) == 0 && + strcmp(node->pub.kind, kind) == 0) { + return node; + } + } + return NULL; +} + +struct st_error * +st_ext_register_check(struct st_ext_ctx *ctx, const char *language, + const char *kind, void *probe_spec) +{ + struct st_check_node *node; + + if (ctx == NULL || language == NULL || language[0] == '\0' || + kind == NULL || kind[0] == '\0') { + return st_error_usage("st_ext_register_check: bad argument"); + } + if (find_check(ctx, language, kind) != NULL) { + return errf(ST_ERR_USAGE, "check already registered: %s/%s", + language, kind); + } + node = calloc(1, sizeof *node); + if (node == NULL) { + return st_error_internal("out of memory"); + } + node->language_owned = strdup(language); + node->kind_owned = strdup(kind); + if (node->language_owned == NULL || node->kind_owned == NULL) { + free(node->language_owned); + free(node->kind_owned); + free(node); + return st_error_internal("out of memory"); + } + node->pub.language = node->language_owned; + node->pub.kind = node->kind_owned; + node->pub.probe_spec = probe_spec; + + if (ctx->checks_tail != NULL) { + ctx->checks_tail->next = node; + } else { + ctx->checks_head = node; + } + ctx->checks_tail = node; + ctx->check_count++; + return NULL; +} + +size_t +st_ext_check_count(const struct st_ext_ctx *ctx) +{ + return ctx != NULL ? ctx->check_count : 0; +} + +const struct st_check * +st_ext_check(const struct st_ext_ctx *ctx, size_t i) +{ + const struct st_check_node *node; + + if (ctx == NULL) { + return NULL; + } + for (node = ctx->checks_head; node != NULL && i > 0; + node = node->next, i--) { + } + return node != NULL ? &node->pub : NULL; +} + +const char * +st_ext_check_language(const struct st_ext_ctx *ctx, size_t i) +{ + const struct st_check *check = st_ext_check(ctx, i); + + return check != NULL ? check->language : NULL; +} + +const char * +st_ext_check_kind(const struct st_ext_ctx *ctx, size_t i) +{ + const struct st_check *check = st_ext_check(ctx, i); + + return check != NULL ? check->kind : NULL; +} + +void * +st_ext_check_probe_spec(const struct st_ext_ctx *ctx, size_t i) +{ + const struct st_check *check = st_ext_check(ctx, i); + + return check != NULL ? check->probe_spec : NULL; +} + +/* ---------------- builtin module registry ---------------- */ + +typedef struct st_error *(*st_builtin_init_fn)(struct st_ext_ctx *); + +/* Entry points only -- no compiler strings here. Each module registers + * itself over the generic ABI. */ +static const st_builtin_init_fn builtin_modules[] = { + st_ext_lang_c_init, + st_ext_lang_cpp_init, +}; + +struct st_error * +st_ext_init_builtins(struct st_ext_ctx *ctx) +{ + size_t i; + + if (ctx == NULL) { + return st_error_usage("st_ext_init_builtins: NULL ctx"); + } + for (i = 0; i < sizeof builtin_modules / sizeof builtin_modules[0]; i++) { + struct st_error *err = builtin_modules[i](ctx); + + if (err != NULL) { + return err; + } + } + return NULL; +} + +/* ---------------- process helper ---------------- */ + +int +st_ext_run_capture(char *const argv[], char **out, size_t *out_len) +{ + int pfd[2]; + pid_t pid; + int status; + char *buf = NULL; + size_t len = 0, cap = 0; + int rc = -1; + + if (argv == NULL || argv[0] == NULL) { + return -1; + } + if (pipe(pfd) != 0) { + return -1; + } + pid = fork(); + if (pid < 0) { + close(pfd[0]); + close(pfd[1]); + return -1; + } + if (pid == 0) { + /* child: stdout + stderr into the pipe */ + close(pfd[0]); + if (dup2(pfd[1], STDOUT_FILENO) < 0 || + dup2(pfd[1], STDERR_FILENO) < 0) { + _exit(126); + } + close(pfd[1]); + execvp(argv[0], argv); + _exit(127); /* shell convention: command not found */ + } + + close(pfd[1]); + /* parent: drain until EOF; keep at most 64 KiB (--version is tiny) */ + for (;;) { + char scratch[4096]; + ssize_t n = read(pfd[0], scratch, sizeof scratch); + + if (n < 0) { + if (errno == EINTR) { + continue; + } + break; + } + if (n == 0) { + break; + } + if (out != NULL && len < 65536) { + size_t take = (size_t)n; + char *nbuf; + + if (take > 65536 - len) { + take = 65536 - len; + } + if (cap < len + take) { + size_t ncap = cap == 0 ? 256 : cap; + + while (ncap < len + take) { + ncap *= 2; + } + nbuf = realloc(buf, ncap); + if (nbuf == NULL) { + break; /* OOM: stop storing, keep draining below */ + } + buf = nbuf; + cap = ncap; + } + if (cap >= len + take) { + memcpy(buf + len, scratch, take); + len += take; + } + } + } + close(pfd[0]); + + while (waitpid(pid, &status, 0) < 0) { + if (errno != EINTR) { + rc = -1; + goto out; + } + } + rc = WIFEXITED(status) ? WEXITSTATUS(status) : -1; + +out: + if (out != NULL) { + *out = buf; + } else { + free(buf); + } + if (out_len != NULL) { + *out_len = len; + } + return rc; +} diff --git a/src/ext/abi.h b/src/ext/abi.h new file mode 100644 index 0000000..fa8e305 --- /dev/null +++ b/src/ext/abi.h @@ -0,0 +1,161 @@ +#ifndef ST_EXT_ABI_H +#define ST_EXT_ABI_H + +/* + * The extension ABI (plan todo 13). + * + * Everything a language module can do goes through this header. Core + * knows NOTHING language-specific: no compiler names, no CC/CFLAGS + * variables, nothing C/C++-specific anywhere outside the module files + * (lang_c.c / lang_cpp.c and their private shared header). + * + * Three generic facilities: + * 1. a variable registry (st_registry_set_var / st_registry_get_var) + * -- the configure generator (todo 16) substitutes from this BY + * NAME; CC/CFLAGS/CXX/CXXFLAGS are registered here by the C/C++ + * modules, never hardcoded in core; + * 2. a language registry (st_ext_register_language + detect); + * 3. a check registry (st_ext_register_check). + * + * Lua-friendliness (todo 19/21): everything is a plain C function + * pointer taking an explicitly passed `struct st_ext_ctx *`. There is + * NO static state hidden inside the .c files -- a Lua binding only + * needs to wrap these functions; the ctx is a userdata it passes back. + * Borrowed results stay valid until the next mutation of the ctx (the + * registries are linked lists, so node addresses are stable across + * registrations). + */ + +#include + +#include "error.h" + +/* ---------------- generic variable registry ---------------- */ + +struct st_registry; /* opaque; owns name/value pairs in insertion order */ + +struct st_registry *st_registry_new(void); +void st_registry_free(struct st_registry *r); /* NULL is a no-op */ + +/* Set or update a variable. `value == NULL` UNSETS (removes) it. + * Returns NULL on success, or a heap st_error on allocation failure. + * Values are duplicated. */ +struct st_error *st_registry_set_var(struct st_registry *r, const char *name, + const char *value); + +/* Borrowed value, or NULL when unset. */ +const char *st_registry_get_var(const struct st_registry *r, + const char *name); + +/* Ordered iteration (insertion order) for the substitution engine. */ +size_t st_registry_var_count(const struct st_registry *r); +const char *st_registry_var_name(const struct st_registry *r, size_t i); +const char *st_registry_var_value(const struct st_registry *r, size_t i); + +/* ---------------- toolchain result ---------------- */ + +/* What a successful detection produced. `path` is the command as + * invoked (env override value or the candidate that worked); `id` is + * derived from the compiler's own `--version` output (vendor name, or + * "unknown" when unrecognizable); `version` is likewise ("16.2.1") or + * "" when unparsable. All three fields are heap-allocated and owned by + * the ctx. */ +struct st_toolchain { + char *path; + char *id; + char *version; +}; + +/* ---------------- extension context ---------------- */ + +struct st_ext_ctx; /* opaque; owns languages, checks, variables */ + +/* Detects a language's toolchain. MUST fill `out` completely on + * success (all three fields freshly strdup'd); on failure it returns a + * heap st_error and may leave `out` partially filled -- the ctx frees + * partial fields automatically. */ +typedef struct st_error *(*st_lang_detect_fn)(struct st_ext_ctx *ctx, + void *module_ctx, + struct st_toolchain *out); + +/* A registered language. Borrowed view into the ctx; stable until the + * ctx is freed. */ +struct st_language { + const char *name; /* "c", "cxx", ... (owned by the ctx) */ + st_lang_detect_fn detect; /* how to find the toolchain */ + void *module_ctx; /* opaque module state, passed back */ + const char *const *var_names; /* NULL-terminated vars this language + registers on detection (CC, CFLAGS…) */ + const struct st_toolchain *toolchain; /* filled on detect; NULL before */ +}; + +/* A registered check kind ("header" is universal; per-language kinds + * arrive with todo 11/12). Borrowed view; stable until ctx free. */ +struct st_check { + const char *language; /* which language this check applies to */ + const char *kind; /* "header", "function", ... */ + void *probe_spec; /* opaque module data; may be NULL for now */ +}; + +struct st_ext_ctx *st_ext_ctx_new(void); /* NULL on allocation failure */ +void st_ext_ctx_free(struct st_ext_ctx *ctx); /* NULL is a no-op */ + +/* The ctx's variable registry (same instance the language modules + * write into; what todo 16 substitutes from). */ +struct st_registry *st_ext_var_registry(struct st_ext_ctx *ctx); + +/* Registers a language. `var_names` is copied (NULL-terminated array). + * Returns NULL on success, or an error for bad arguments / duplicates. */ +struct st_error *st_ext_register_language(struct st_ext_ctx *ctx, + const char *name, + st_lang_detect_fn detect, + void *module_ctx, + const char *const *var_names); + +size_t st_ext_language_count(const struct st_ext_ctx *ctx); +const struct st_language *st_ext_language(const struct st_ext_ctx *ctx, + size_t i); /* NULL past end */ +const char *st_ext_language_name(const struct st_ext_ctx *ctx, size_t i); +const struct st_toolchain *st_ext_language_toolchain( + const struct st_ext_ctx *ctx, size_t i); +const struct st_toolchain *st_ext_language_toolchain_named( + const struct st_ext_ctx *ctx, const char *name); + +/* Runs the registered detect fn for the named language and stores the + * result. Returns NULL on success, or an error (unknown language / + * detection failure -- e.g. no compiler found). */ +struct st_error *st_ext_detect_language(struct st_ext_ctx *ctx, + const char *name); + +/* Registers a check kind for a language. Duplicate (language, kind) + * pairs are errors. Returns NULL on success. */ +struct st_error *st_ext_register_check(struct st_ext_ctx *ctx, + const char *language, + const char *kind, void *probe_spec); + +size_t st_ext_check_count(const struct st_ext_ctx *ctx); +const struct st_check *st_ext_check(const struct st_ext_ctx *ctx, + size_t i); /* NULL past end */ +const char *st_ext_check_language(const struct st_ext_ctx *ctx, size_t i); +const char *st_ext_check_kind(const struct st_ext_ctx *ctx, size_t i); +void *st_ext_check_probe_spec(const struct st_ext_ctx *ctx, size_t i); + +/* ---------------- builtin module registry ---------------- */ + +/* Runs the builtin module entry points (lang_c, lang_cpp). The modules + * register themselves through the generic ABI above -- the table here + * names entry points only; every compiler string lives in the modules. + * Returns NULL on success. */ +struct st_error *st_ext_init_builtins(struct st_ext_ctx *ctx); + +/* ---------------- process helper for module authors ---------------- */ + +/* Runs argv (NULL-terminated; argv[0] looked up via execvp on PATH) + * and captures stdout+stderr into *out (malloc'd; caller frees; may be + * NULL to discard) with *out_len. Returns the child's exit status + * (0-255), or -1 when it could not be spawned (e.g. ENOENT) or was + * killed by a signal. No timeout: intended for short commands like + * `cc --version`. */ +int st_ext_run_capture(char *const argv[], char **out, size_t *out_len); + +#endif /* ST_EXT_ABI_H */ diff --git a/src/ext/lang_c.c b/src/ext/lang_c.c new file mode 100644 index 0000000..15e74c7 --- /dev/null +++ b/src/ext/lang_c.c @@ -0,0 +1,44 @@ +#ifndef _POSIX_C_SOURCE +#define _POSIX_C_SOURCE 200809L /* getenv/setenv surface (POSIX.1-2008) */ +#endif + +#include "lang_c.h" + +#include "lang_common.h" + +#include + +/* + * The C language, entirely as a module over the extension ABI. + * ALL C-specific strings live here -- core never sees them. + * + * Detection order (see lang_common.h): env CC -> cc -> gcc -> clang. + */ + +/* fallback candidates, in probe order (env override is checked first) */ +static const char *const candidates[] = { "cc", "gcc", "clang" }; + +/* variables this language owns and registers on successful detection */ +static const char *const var_names[] = { "CC", "CFLAGS", NULL }; + +static struct st_error * +detect_c(struct st_ext_ctx *ctx, void *module_ctx, struct st_toolchain *out) +{ + (void)module_ctx; + return st_lang_detect_common(ctx, "CC", "C", candidates, + sizeof candidates / sizeof candidates[0], + var_names, out); +} + +struct st_error * +st_ext_lang_c_init(struct st_ext_ctx *ctx) +{ + struct st_error *err; + + err = st_ext_register_language(ctx, "c", detect_c, NULL, var_names); + if (err != NULL) { + return err; + } + /* the `header` check is universal: register it for this language */ + return st_ext_register_check(ctx, "c", "header", NULL); +} diff --git a/src/ext/lang_c.h b/src/ext/lang_c.h new file mode 100644 index 0000000..0ee9e86 --- /dev/null +++ b/src/ext/lang_c.h @@ -0,0 +1,20 @@ +#ifndef ST_EXT_LANG_C_H +#define ST_EXT_LANG_C_H + +#include "abi.h" + +/* + * Builtin C language module entry point (called by the builtin module + * registry in abi.c; a future extension-discovery todo keeps this + * shape for user modules too). + * + * Registers over the generic ABI: + * - language "c" (detect fn + the CC/CFLAGS variables it owns) + * - check kind "header" for language "c" + * + * Returns NULL on success. Every compiler-specific string lives in + * lang_c.c -- nothing here is known to core. + */ +struct st_error *st_ext_lang_c_init(struct st_ext_ctx *ctx); + +#endif /* ST_EXT_LANG_C_H */ diff --git a/src/ext/lang_common.h b/src/ext/lang_common.h new file mode 100644 index 0000000..d150051 --- /dev/null +++ b/src/ext/lang_common.h @@ -0,0 +1,225 @@ +#ifndef ST_EXT_LANG_COMMON_H +#define ST_EXT_LANG_COMMON_H + +/* + * Shared internals for the builtin C and C++ language modules + * (lang_c.c / lang_cpp.c). NOT part of the public ABI -- abi.h is the + * only interface core (and a future Lua binding) sees. + * + * Everything here is `static inline` so the two modules stay + * independently compilable while sharing ONE tested implementation of + * the detection order and the `--version` parsing. + */ + +#include "abi.h" + +#include +#include +#include + +/* Copy the version token after `needle`: a run of [0-9.] up to + * whitespace. Returns malloc'd string or NULL. */ +static inline char * +st_lang_version_token(const char *text, const char *needle) +{ + const char *p = strstr(text, needle); + size_t n; + char *tok; + + if (p == NULL) { + return NULL; + } + p += strlen(needle); + while (*p == ' ' || *p == '\t') { + p++; + } + n = 0; + while ((p[n] >= '0' && p[n] <= '9') || p[n] == '.') { + n++; + } + if (n == 0) { + return NULL; + } + tok = malloc(n + 1); + if (tok == NULL) { + return NULL; + } + memcpy(tok, p, n); + tok[n] = '\0'; + return tok; +} + +/* Fallback: the last first-line token beginning with a digit, trimmed + * to its [0-9.] run. Handles "cc (Ubuntu 13.2.0-1) 13.2.0" style + * output where the "(GCC) " needle is absent. Returns malloc'd string + * or NULL. */ +static inline char * +st_lang_last_version(const char *text) +{ + const char *p = text; + char *tok = NULL; + + while (*p != '\0' && *p != '\n') { + const char *start; + size_t n; + + while (*p == ' ' || *p == '\t') { + p++; + } + if (*p == '\0' || *p == '\n') { + break; + } + start = p; + while (*p != '\0' && *p != ' ' && *p != '\t' && *p != '\n') { + p++; + } + if (*start < '0' || *start > '9') { + continue; /* token must begin with a digit */ + } + n = 0; + while (start + n < p && + ((start[n] >= '0' && start[n] <= '9') || start[n] == '.')) { + n++; + } + free(tok); + tok = malloc(n + 1); + if (tok == NULL) { + return NULL; + } + memcpy(tok, start, n); + tok[n] = '\0'; + } + return tok; +} + +/* Identify a compiler from its `--version` output: id ("clang", + * "gcc", "unknown") + version, both freshly allocated (version is "" + * when unparsable). Never hard-fails: worst case is "unknown"/"". */ +static inline void +st_lang_identify(const char *text, char **id, char **version) +{ + *id = NULL; + *version = NULL; + + if (strstr(text, "clang") != NULL) { + *id = strdup("clang"); + *version = st_lang_version_token(text, "clang version "); + } else if (strstr(text, "GCC") != NULL || strstr(text, "gcc") != NULL) { + *id = strdup("gcc"); + *version = st_lang_version_token(text, "(GCC) "); + } else { + *id = strdup("unknown"); + } + if (*version == NULL) { + *version = st_lang_last_version(text); + } + if (*version == NULL) { + *version = strdup(""); + } + if (*id == NULL) { + *id = strdup("unknown"); + } +} + +/* snprintf-based one-argument IO error (our detection messages carry at + * most one dynamic argument). */ +static inline struct st_error * +st_lang_errio(const char *fmt, const char *arg) +{ + int n = snprintf(NULL, 0, fmt, arg); + char *msg; + struct st_error *e; + + if (n < 0) { + return st_error_internal("failed to format error message"); + } + msg = malloc((size_t)n + 1); + if (msg == NULL) { + return st_error_internal("out of memory"); + } + snprintf(msg, (size_t)n + 1, fmt, arg); + e = st_error_io(msg); + free(msg); + return e; +} + +/* + * The shared detection engine, parameterized by the module: + * + * env_name environment override variable ("CC" / "CXX") + * display human name for diagnostics ("C" / "C++") + * candidates fallback order: {"cc","gcc","clang"} / {"c++","g++","clang++"} + * var_names {"CC","CFLAGS",NULL} / {"CXX","CXXFLAGS",NULL} + * + * Detection ORDER (locked, mirrors the plan todo 13): + * 1. env override -- authoritative: if set and not runnable, fail + * with a clean "compiler not found" error, NEVER silently fall + * back to a default; + * 2. `cc` / `c++` (the ubiquitous fallback); + * 3. probe `gcc`/`clang` / `g++`/`clang++`. + * + * Detection EXECUTES the compiler (`--version`), never guesses from + * PATH strings. On success fills `out` (the ctx owns it) and registers + * the module's variables: var_names[0] = the command, var_names[1] = + * flags from the environment (empty default). + */ +static inline struct st_error * +st_lang_detect_common(struct st_ext_ctx *ctx, const char *env_name, + const char *display, const char *const *candidates, + size_t n_candidates, const char *const *var_names, + struct st_toolchain *out) +{ + struct st_registry *reg = st_ext_var_registry(ctx); + const char *env = getenv(env_name); + const char *cmd = NULL; + const char *flags; + char *buf = NULL; + size_t buflen = 0; + size_t i; + struct st_error *err; + + if (env != NULL && env[0] != '\0') { + char *argv[3] = { (char *)env, "--version", NULL }; + + if (st_ext_run_capture(argv, &buf, &buflen) == 0) { + cmd = env; + } else { + free(buf); + return st_lang_errio("compiler not found: %s", env); + } + } else { + for (i = 0; i < n_candidates; i++) { + char *argv[3] = { (char *)candidates[i], "--version", NULL }; + + if (st_ext_run_capture(argv, &buf, &buflen) == 0) { + cmd = candidates[i]; + break; + } + free(buf); + buf = NULL; + buflen = 0; + } + if (cmd == NULL) { + return st_lang_errio("no %s compiler found", display); + } + } + + out->path = strdup(cmd); + if (out->path == NULL) { + free(buf); + return st_error_internal("out of memory"); + } + st_lang_identify(buf != NULL ? buf : "", &out->id, &out->version); + free(buf); + + /* register this module's variables with the GENERIC registry */ + err = st_registry_set_var(reg, var_names[0], cmd); + if (err != NULL) { + return err; + } + flags = getenv(var_names[1]); + return st_registry_set_var(reg, var_names[1], + flags != NULL ? flags : ""); +} + +#endif /* ST_EXT_LANG_COMMON_H */ diff --git a/src/ext/lang_cpp.c b/src/ext/lang_cpp.c new file mode 100644 index 0000000..58e7adf --- /dev/null +++ b/src/ext/lang_cpp.c @@ -0,0 +1,45 @@ +#ifndef _POSIX_C_SOURCE +#define _POSIX_C_SOURCE 200809L /* getenv/setenv surface (POSIX.1-2008) */ +#endif + +#include "lang_cpp.h" + +#include "lang_common.h" + +#include + +/* + * The C++ language, entirely as a module over the extension ABI. + * ALL C++-specific strings live here -- core never sees them. + * + * Detection order (see lang_common.h): env CXX -> c++ -> g++ -> clang++. + */ + +/* fallback candidates, in probe order (env override is checked first) */ +static const char *const candidates[] = { "c++", "g++", "clang++" }; + +/* variables this language owns and registers on successful detection */ +static const char *const var_names[] = { "CXX", "CXXFLAGS", NULL }; + +static struct st_error * +detect_cxx(struct st_ext_ctx *ctx, void *module_ctx, + struct st_toolchain *out) +{ + (void)module_ctx; + return st_lang_detect_common(ctx, "CXX", "C++", candidates, + sizeof candidates / sizeof candidates[0], + var_names, out); +} + +struct st_error * +st_ext_lang_cpp_init(struct st_ext_ctx *ctx) +{ + struct st_error *err; + + err = st_ext_register_language(ctx, "cxx", detect_cxx, NULL, var_names); + if (err != NULL) { + return err; + } + /* the `header` check is universal: register it for this language */ + return st_ext_register_check(ctx, "cxx", "header", NULL); +} diff --git a/src/ext/lang_cpp.h b/src/ext/lang_cpp.h new file mode 100644 index 0000000..8f31a94 --- /dev/null +++ b/src/ext/lang_cpp.h @@ -0,0 +1,19 @@ +#ifndef ST_EXT_LANG_CPP_H +#define ST_EXT_LANG_CPP_H + +#include "abi.h" + +/* + * Builtin C++ language module entry point (called by the builtin module + * registry in abi.c). + * + * Registers over the generic ABI: + * - language "cxx" (detect fn + the CXX/CXXFLAGS variables it owns) + * - check kind "header" for language "cxx" + * + * Returns NULL on success. Every compiler-specific string lives in + * lang_cpp.c -- nothing here is known to core. + */ +struct st_error *st_ext_lang_cpp_init(struct st_ext_ctx *ctx); + +#endif /* ST_EXT_LANG_CPP_H */ diff --git a/tests/unit/test_abi.c b/tests/unit/test_abi.c new file mode 100644 index 0000000..30ed8ff --- /dev/null +++ b/tests/unit/test_abi.c @@ -0,0 +1,462 @@ +/* LINK: ../../src/ext/abi.c ../../src/ext/lang_c.c ../../src/ext/lang_cpp.c ../../src/error.c ../../src/span.c */ +/* tests/unit/test_abi.c + * + * Unit tests for the extension ABI (src/ext/abi.h) and the builtin C/C++ + * language modules (plan todo 13). + * + * The magic LINK comment on line 1 is REQUIRED by tests/run.sh: it lists + * the extra .c sources to compile into this test binary (paths relative + * to tests/unit/, space-separated). munit.c and the include dirs are + * added automatically by the harness. + */ +#ifndef _POSIX_C_SOURCE +#define _POSIX_C_SOURCE 200809L /* setenv/unsetenv (POSIX.1-2008) */ +#endif + +#include "munit.h" + +#include "ext/abi.h" + +#include +#include + +/* ---------- helpers ---------- */ + +/* A fake detect fn proving the ABI invokes registered function pointers + * (exactly what a future Lua binding will do). module_ctx is an int* + * used as a call counter. */ +static struct st_error * +fake_detect(struct st_ext_ctx *ctx, void *module_ctx, + struct st_toolchain *out) +{ + int *called = module_ctx; + + (void)ctx; + if (called != NULL) { + *called = 1; + } + out->path = strdup("fakecc"); + out->id = strdup("fake"); + out->version = strdup("0.0"); + return NULL; +} + +static void +reset_cc_env(void) +{ + unsetenv("CC"); + unsetenv("CFLAGS"); + unsetenv("CXX"); + unsetenv("CXXFLAGS"); +} + +/* Does the host have a working ? (mirrors what detection does: + * actually execute, never guess from PATH strings). */ +static int +host_has(const char *cmd) +{ + char *argv[] = { (char *)cmd, "--version", NULL }; + char *out = NULL; + size_t out_len = 0; + int status = st_ext_run_capture(argv, &out, &out_len); + + free(out); + return status == 0; +} + +/* ---------- (a) builtin modules register over the generic ABI ---------- */ + +static MunitResult +test_builtins_register_languages(const MunitParameter params[], void *data) +{ + (void)params; + (void)data; + struct st_ext_ctx *ctx = st_ext_ctx_new(); + struct st_error *err; + + munit_assert_not_null(ctx); + + /* before init: nothing registered */ + munit_assert_size(st_ext_language_count(ctx), ==, 0); + munit_assert_size(st_ext_check_count(ctx), ==, 0); + + err = st_ext_init_builtins(ctx); + munit_assert_null(err); + + /* the C and C++ language modules registered themselves */ + munit_assert_size(st_ext_language_count(ctx), ==, 2); + munit_assert_string_equal(st_ext_language_name(ctx, 0), "c"); + munit_assert_string_equal(st_ext_language_name(ctx, 1), "cxx"); + + /* each language registered its check kinds (header is universal) */ + munit_assert_size(st_ext_check_count(ctx), ==, 2); + munit_assert_string_equal(st_ext_check_language(ctx, 0), "c"); + munit_assert_string_equal(st_ext_check_kind(ctx, 0), "header"); + munit_assert_string_equal(st_ext_check_language(ctx, 1), "cxx"); + munit_assert_string_equal(st_ext_check_kind(ctx, 1), "header"); + + /* no detection happened yet: no toolchain, no registered variables */ + munit_assert_null(st_ext_language_toolchain(ctx, 0)); + munit_assert_size(st_registry_var_count(st_ext_var_registry(ctx)), ==, 0); + + st_ext_ctx_free(ctx); + return MUNIT_OK; +} + +/* (a) detect finds a non-empty CC path + compiler id on this host. */ +static MunitResult +test_detect_c(const MunitParameter params[], void *data) +{ + (void)params; + (void)data; + struct st_ext_ctx *ctx = st_ext_ctx_new(); + struct st_error *err; + const struct st_toolchain *tc; + const char *cc; + + munit_assert_not_null(ctx); + munit_assert_null(st_ext_init_builtins(ctx)); + + 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); + munit_assert_int(strlen(tc->path), >, 0); + munit_assert_not_null(tc->id); + munit_assert_int(strlen(tc->id), >, 0); + munit_assert_not_null(tc->version); + munit_assert_int(strlen(tc->version), >, 0); + + /* detection recorded the compiler command in CC */ + cc = st_registry_get_var(st_ext_var_registry(ctx), "CC"); + munit_assert_not_null(cc); + munit_assert_string_equal(cc, tc->path); + + st_ext_ctx_free(ctx); + return MUNIT_OK; +} + +/* (b) CC=clang env override: detection must return clang, not the + * default cc. */ +static MunitResult +test_env_override_clang(const MunitParameter params[], void *data) +{ + (void)params; + (void)data; + struct st_ext_ctx *ctx = st_ext_ctx_new(); + struct st_error *err; + const struct st_toolchain *tc; + + munit_assert_not_null(ctx); + munit_assert_null(st_ext_init_builtins(ctx)); + + munit_assert_int(setenv("CC", "clang", 1), ==, 0); + err = st_ext_detect_language(ctx, "c"); + + if (host_has("clang")) { + /* Host with clang: id and CC are clang. */ + munit_assert_null(err); + munit_assert_string_equal( + st_registry_get_var(st_ext_var_registry(ctx), "CC"), "clang"); + tc = st_ext_language_toolchain_named(ctx, "c"); + munit_assert_not_null(tc); + munit_assert_string_equal(tc->id, "clang"); + munit_assert_string_equal(tc->path, "clang"); + } else { + /* Host without clang: the override is still authoritative -- a + * clean error, and NO silent fallback to cc. */ + munit_assert_not_null(err); + munit_assert_int(st_error_category_of(err), ==, ST_ERR_IO); + munit_assert_not_null(strstr(st_error_message(err), "not found")); + munit_assert_null(st_registry_get_var(st_ext_var_registry(ctx), "CC")); + st_error_free(err); + err = NULL; + } + + st_ext_ctx_free(ctx); + return MUNIT_OK; +} + +/* (b, host form) CC=gcc env override honored end to end. */ +static MunitResult +test_env_override_gcc(const MunitParameter params[], void *data) +{ + (void)params; + (void)data; + struct st_ext_ctx *ctx = st_ext_ctx_new(); + struct st_error *err; + const struct st_toolchain *tc; + + munit_assert_not_null(ctx); + munit_assert_null(st_ext_init_builtins(ctx)); + + munit_assert_int(setenv("CC", "gcc", 1), ==, 0); + err = st_ext_detect_language(ctx, "c"); + munit_assert_null(err); + + munit_assert_string_equal( + st_registry_get_var(st_ext_var_registry(ctx), "CC"), "gcc"); + tc = st_ext_language_toolchain_named(ctx, "c"); + munit_assert_not_null(tc); + munit_assert_string_equal(tc->id, "gcc"); + + st_ext_ctx_free(ctx); + return MUNIT_OK; +} + +/* (c) a bogus CC=/nonexistent yields a clean error, not a crash. */ +static MunitResult +test_bogus_cc_clean_error(const MunitParameter params[], void *data) +{ + (void)params; + (void)data; + struct st_ext_ctx *ctx = st_ext_ctx_new(); + struct st_error *err; + + munit_assert_not_null(ctx); + munit_assert_null(st_ext_init_builtins(ctx)); + + munit_assert_int( + setenv("CC", "/nonexistent/stupidtools/not-a-compiler", 1), ==, 0); + err = st_ext_detect_language(ctx, "c"); + + munit_assert_not_null(err); + munit_assert_int(st_error_category_of(err), ==, ST_ERR_IO); + munit_assert_not_null(strstr(st_error_message(err), "not found")); + + /* failed detection must not half-register variables */ + munit_assert_null(st_registry_get_var(st_ext_var_registry(ctx), "CC")); + munit_assert_null(st_ext_language_toolchain_named(ctx, "c")); + + st_error_free(err); + st_ext_ctx_free(ctx); + return MUNIT_OK; +} + +/* adversarial: empty PATH must yield a clean error, not a crash. */ +static MunitResult +test_empty_path_clean_error(const MunitParameter params[], void *data) +{ + (void)params; + (void)data; + struct st_ext_ctx *ctx = st_ext_ctx_new(); + struct st_error *err; + const char *saved = getenv("PATH"); + + munit_assert_not_null(ctx); + munit_assert_null(st_ext_init_builtins(ctx)); + + reset_cc_env(); + munit_assert_int(setenv("PATH", "", 1), ==, 0); + err = st_ext_detect_language(ctx, "c"); + munit_assert_not_null(err); + munit_assert_not_null(strstr(st_error_message(err), "no C compiler found")); + munit_assert_null(st_registry_get_var(st_ext_var_registry(ctx), "CC")); + st_error_free(err); + + if (saved != NULL) { + munit_assert_int(setenv("PATH", saved, 1), ==, 0); + } else { + munit_assert_int(unsetenv("PATH"), ==, 0); + } + + st_ext_ctx_free(ctx); + return MUNIT_OK; +} + +/* (d) registered vars CC/CFLAGS are queryable from the generic registry; + * CFLAGS defaults empty and respects the environment. */ +static MunitResult +test_registered_vars_queryable(const MunitParameter params[], void *data) +{ + (void)params; + (void)data; + struct st_ext_ctx *ctx = st_ext_ctx_new(); + struct st_registry *reg; + struct st_error *err; + + munit_assert_not_null(ctx); + munit_assert_null(st_ext_init_builtins(ctx)); + reg = st_ext_var_registry(ctx); + + reset_cc_env(); + munit_assert_int(setenv("CFLAGS", "-std=c23 -pedantic", 1), ==, 0); + err = st_ext_detect_language(ctx, "c"); + munit_assert_null(err); + + munit_assert_not_null(st_registry_get_var(reg, "CC")); + munit_assert_int(strlen(st_registry_get_var(reg, "CC")), >, 0); + munit_assert_string_equal(st_registry_get_var(reg, "CFLAGS"), + "-std=c23 -pedantic"); + + /* CXX/CXXFLAGS belong to the cxx module and are not set by C detect */ + munit_assert_null(st_registry_get_var(reg, "CXX")); + + st_ext_ctx_free(ctx); + return MUNIT_OK; +} + +/* (d) the generic var registry itself: set/get/overwrite/unset semantics + * with no compiler involved. */ +static MunitResult +test_var_registry_semantics(const MunitParameter params[], void *data) +{ + (void)params; + (void)data; + struct st_registry *reg = st_registry_new(); + + munit_assert_not_null(reg); + munit_assert_null(st_registry_get_var(reg, "X")); + munit_assert_size(st_registry_var_count(reg), ==, 0); + + st_registry_set_var(reg, "X", "v1"); + munit_assert_string_equal(st_registry_get_var(reg, "X"), "v1"); + munit_assert_size(st_registry_var_count(reg), ==, 1); + + /* overwrite replaces the value */ + st_registry_set_var(reg, "X", "v2"); + munit_assert_string_equal(st_registry_get_var(reg, "X"), "v2"); + munit_assert_size(st_registry_var_count(reg), ==, 1); + + /* NULL value unsets */ + st_registry_set_var(reg, "X", NULL); + munit_assert_null(st_registry_get_var(reg, "X")); + munit_assert_size(st_registry_var_count(reg), ==, 0); + + /* ordered iteration matches insertion order */ + st_registry_set_var(reg, "CC", "cc"); + st_registry_set_var(reg, "CFLAGS", ""); + munit_assert_size(st_registry_var_count(reg), ==, 2); + munit_assert_string_equal(st_registry_var_name(reg, 0), "CC"); + munit_assert_string_equal(st_registry_var_value(reg, 0), "cc"); + munit_assert_string_equal(st_registry_var_name(reg, 1), "CFLAGS"); + munit_assert_string_equal(st_registry_var_value(reg, 1), ""); + + /* get_var is borrowed, not owned: registry still frees cleanly */ + st_registry_free(reg); + return MUNIT_OK; +} + +/* the C++ language module detects over the same ABI (env CXX -> c++ -> + * g++/clang++) and registers CXX/CXXFLAGS. */ +static MunitResult +test_detect_cxx(const MunitParameter params[], void *data) +{ + (void)params; + (void)data; + struct st_ext_ctx *ctx = st_ext_ctx_new(); + struct st_error *err; + const struct st_toolchain *tc; + + munit_assert_not_null(ctx); + munit_assert_null(st_ext_init_builtins(ctx)); + + reset_cc_env(); + err = st_ext_detect_language(ctx, "cxx"); + + if (err != NULL) { + /* Host without a C++ compiler: clean error, no half-registration. */ + munit_assert_not_null(strstr(st_error_message(err), "found")); + munit_assert_null(st_registry_get_var(st_ext_var_registry(ctx), "CXX")); + st_error_free(err); + } else { + tc = st_ext_language_toolchain_named(ctx, "cxx"); + munit_assert_not_null(tc); + munit_assert_int(strlen(tc->path), >, 0); + munit_assert_int(strlen(tc->id), >, 0); + munit_assert_string_equal( + st_registry_get_var(st_ext_var_registry(ctx), "CXX"), tc->path); + munit_assert_not_null( + st_registry_get_var(st_ext_var_registry(ctx), "CXXFLAGS")); + } + + st_ext_ctx_free(ctx); + return MUNIT_OK; +} + +/* ABI robustness: duplicate registrations and unknown lookups error. */ +static MunitResult +test_registry_errors(const MunitParameter params[], void *data) +{ + (void)params; + (void)data; + struct st_ext_ctx *ctx = st_ext_ctx_new(); + struct st_error *err; + static const char *const vars[] = { "CC", NULL }; + const struct st_toolchain *tc; + int fake_detect_called = 0; + + munit_assert_not_null(ctx); + + err = st_ext_register_language(ctx, "c", fake_detect, + &fake_detect_called, vars); + munit_assert_null(err); + + /* duplicate language registration errors */ + err = st_ext_register_language(ctx, "c", fake_detect, NULL, vars); + munit_assert_not_null(err); + st_error_free(err); + + /* duplicate check registration errors */ + err = st_ext_register_check(ctx, "c", "header", NULL); + munit_assert_null(err); + err = st_ext_register_check(ctx, "c", "header", NULL); + munit_assert_not_null(err); + st_error_free(err); + + /* detecting an unknown language errors, does not crash */ + err = st_ext_detect_language(ctx, "nope"); + munit_assert_not_null(err); + st_error_free(err); + + /* detection routes through the registered function pointer */ + err = st_ext_detect_language(ctx, "c"); + munit_assert_null(err); + munit_assert_int(fake_detect_called, ==, 1); + tc = st_ext_language_toolchain_named(ctx, "c"); + munit_assert_not_null(tc); + munit_assert_string_equal(tc->id, "fake"); + + /* unknown-name toolchain lookup returns NULL */ + munit_assert_null(st_ext_language_toolchain_named(ctx, "nope")); + + st_ext_ctx_free(ctx); + return MUNIT_OK; +} + +static MunitTest tests[] = { + { "/abi/builtins-register", test_builtins_register_languages, NULL, NULL, + MUNIT_TEST_OPTION_NONE, NULL }, + { "/abi/detect-c", test_detect_c, NULL, NULL, MUNIT_TEST_OPTION_NONE, + NULL }, + { "/abi/env-override-clang", test_env_override_clang, NULL, NULL, + MUNIT_TEST_OPTION_NONE, NULL }, + { "/abi/env-override-gcc", test_env_override_gcc, NULL, NULL, + MUNIT_TEST_OPTION_NONE, NULL }, + { "/abi/bogus-cc-clean-error", test_bogus_cc_clean_error, NULL, NULL, + MUNIT_TEST_OPTION_NONE, NULL }, + { "/abi/empty-path-clean-error", test_empty_path_clean_error, NULL, NULL, + MUNIT_TEST_OPTION_NONE, NULL }, + { "/abi/registered-vars-queryable", test_registered_vars_queryable, NULL, + NULL, MUNIT_TEST_OPTION_NONE, NULL }, + { "/abi/var-registry-semantics", test_var_registry_semantics, NULL, NULL, + MUNIT_TEST_OPTION_NONE, NULL }, + { "/abi/detect-cxx", test_detect_cxx, NULL, NULL, MUNIT_TEST_OPTION_NONE, + NULL }, + { "/abi/registry-errors", test_registry_errors, NULL, NULL, + MUNIT_TEST_OPTION_NONE, NULL }, + { NULL, NULL, NULL, NULL, MUNIT_TEST_OPTION_NONE, NULL }, +}; + +static const MunitSuite suite = { + "/abi", 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); +}