feat(ext): add builtin C/C++ language modules

This commit is contained in:
2026-08-28 20:17:58 -04:00
parent 439aa0d856
commit 468aa1c4ef
8 changed files with 1670 additions and 0 deletions
+694
View File
@@ -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 <errno.h>
#include <stdarg.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/wait.h>
#include <unistd.h>
/*
* 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;
}
+161
View File
@@ -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 <stddef.h>
#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 */
+44
View File
@@ -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 <stddef.h>
/*
* 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);
}
+20
View File
@@ -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 */
+225
View File
@@ -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 <stdio.h>
#include <stdlib.h>
#include <string.h>
/* 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 */
+45
View File
@@ -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 <stddef.h>
/*
* 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);
}
+19
View File
@@ -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 */
+462
View File
@@ -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 <stdlib.h>
#include <string.h>
/* ---------- 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 <cmd>? (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);
}