diff --git a/src/gen/configure.c b/src/gen/configure.c new file mode 100644 index 0000000..17b9ac9 --- /dev/null +++ b/src/gen/configure.c @@ -0,0 +1,743 @@ +/* + * configure.c - the ./configure generator (todo 16). + * + * Assembles the complete POSIX-sh ./configure from a schema-validated DSL + * document and an extension ctx. See gen/configure.h for the full emitted + * layout contract, the substitution approach (split-on-@, documented), the + * LIBS accumulation rules, and the error categories. + * + * Two load-bearing safety decisions carry through from the detect chain: + * - Every user-supplied byte (feature names, check targets, registered + * var values, package names) reaches the emitted script either through + * st_sh_quote (single-quoted literal, inert) or through a + * validated-identifier position (have_, `VAR="${VAR:-...}"`). + * - The substitution runs AT CONFIGURE TIME in shell, so the generated + * script errors on an unknown @VAR@ it encounters (the generator + * cannot know the user's Makefile.in contents); the value is carried + * verbatim through `printf '%s'` so nothing is ever re-interpreted. + * + * Copyright (c) 2026 huntedbytheirs + * SPDX-License-Identifier: BSD-3-Clause + */ + +#include "gen/configure.h" + +#include "cli.h" /* STUPIDTOOLS_VERSION (the generated-by version string) */ + +#include "detect/check_registry.h" +#include "detect/checks.h" +#include "detect/probe.h" +#include "detect/resolve.h" +#include "error.h" +#include "ext/abi.h" +#include "gen/sh_emit.h" +#include "kdl/ast.h" +#include "kdl/value.h" + +#include +#include +#include +#include +#include +#include + +/* ---- small helpers ----------------------------------------------------- */ + +/* True when `s` is a valid POSIX shell identifier: letter/underscore, then + * letters/digits/underscores, non-empty. Feature names and registered var + * names must pass this (they land in variable-NAME positions). */ +static bool +valid_ident(const char *s) +{ + const unsigned char *p; + + if (s == NULL || *s == '\0') { + return false; + } + p = (const unsigned char *)s; + if (!((*p >= 'a' && *p <= 'z') || (*p >= 'A' && *p <= 'Z') || + *p == '_')) { + return false; + } + for (p++; *p != '\0'; p++) { + if (!((*p >= 'a' && *p <= 'z') || (*p >= 'A' && *p <= 'Z') || + (*p >= '0' && *p <= '9') || *p == '_')) { + return false; + } + } + return true; +} + +/* Build an owned ST_ERR_KDL_SCHEMA error whose span lives in the same + * allocation as the error struct (the parser.c pattern). Returns NULL only + * on allocation failure. */ +static struct st_error * +err_at_owned(struct st_span sp, const char *msg) +{ + const size_t align = _Alignof(struct st_span); + const size_t esize = + (sizeof(struct st_error) + align - 1) & ~(align - 1); + struct st_error *e; + struct st_span *spc; + size_t mlen; + + if (msg == NULL) { + msg = ""; + } + e = malloc(esize + sizeof(struct st_span)); + if (e == NULL) { + return NULL; + } + mlen = strlen(msg); + e->message = malloc(mlen + 1); + if (e->message == NULL) { + free(e); + return NULL; + } + memcpy(e->message, msg, mlen + 1); + e->category = ST_ERR_KDL_SCHEMA; + spc = (struct st_span *)((unsigned char *)e + esize); + *spc = sp; + e->span = spc; + return e; +} + +/* Interpret `tok` as the non-empty unannotated string a slot requires; on + * success *out owns the string. Mirrors the value-model re-owning in + * checks.c (value failures become schema errors). */ +static struct st_error * +extract_string(const struct st_kdl_token_ref *tok, const char *ctx, + char **out) +{ + struct st_kdl_value v; + struct st_error *e; + char msg[192]; + + *out = NULL; + e = st_kdl_value_from_token(tok, &v); + if (e != NULL) { + struct st_span sp = e->span != NULL ? *e->span + : (struct st_span){ NULL, 0, 0 }; + struct st_error *r = err_at_owned(sp, e->message != NULL + ? e->message : ""); + + st_error_free(e); + return r; + } + if (v.kind != ST_KDL_VAL_STRING) { + snprintf(msg, sizeof msg, "%s must be a non-empty string", ctx); + e = err_at_owned(tok->span, msg); + st_kdl_value_free(&v); + return e; + } + if (v.as.str[0] == '\0') { + snprintf(msg, sizeof msg, "%s must be a non-empty string", ctx); + e = err_at_owned(tok->span, msg); + st_kdl_value_free(&v); + return e; + } + *out = v.as.str; + v.as.str = NULL; + st_kdl_value_free(&v); + return NULL; +} + +/* vsnprintf-based string builder (no reliance on POSIX strdup). */ +static char * +strbuild(const char *fmt, ...) +{ + va_list ap; + va_list ap2; + int n; + char *out; + + va_start(ap, fmt); + va_copy(ap2, ap); + n = vsnprintf(NULL, 0, fmt, ap); + va_end(ap); + if (n < 0) { + va_end(ap2); + return NULL; + } + out = malloc((size_t)n + 1); + if (out == NULL) { + va_end(ap2); + return NULL; + } + (void)vsnprintf(out, (size_t)n + 1, fmt, ap2); + va_end(ap2); + return out; +} + +/* Compare a node name against a literal, accepting any string form + * (identifiers directly; other forms via the value model). */ +static struct st_error * +name_equals(const struct st_kdl_token_ref *name, const char *s, bool *out) +{ + if (name->kind == ST_TOK_IDENT) { + *out = name->len == strlen(s) && memcmp(name->text, s, name->len) == 0; + return NULL; + } + { + struct st_kdl_value v; + struct st_error *e = st_kdl_value_from_token(name, &v); + + if (e != NULL) { + struct st_error *r; + struct st_span sp = e->span != NULL + ? *e->span + : (struct st_span){ NULL, 0, 0 }; + + r = err_at_owned(sp, e->message != NULL ? e->message : ""); + st_error_free(e); + *out = false; + return r; + } + *out = v.kind == ST_KDL_VAL_STRING && strcmp(v.as.str, s) == 0; + st_kdl_value_free(&v); + return NULL; + } +} + +/* ---- fixed shell text (emitted verbatim via st_sh_emit_str) ------------ */ + +/* The `%` in `printf '... %s ...'` is why these are emitted with + * st_sh_emit_str (verbatim), not fprintf (which would eat `%s`). */ + +static const char PREAMBLE[] = + "\n" + "# --- PREAMBLE ---\n" + "# v1 MINIMAL argument handling. todo 18 replaces this whole section\n" + "# with the full --help / --enable-*/--with-*/VAR=VALUE parser.\n" + "prefix=/usr/local\n" + "srcdir=.\n" + "build=\n" + "host=\n" + "cross_compiling=no\n" + "\n" + "# VAR=VALUE overrides reach configure as environment variables (the\n" + "# invoking shell exported them, e.g. `CC=clang ./configure`), so v1\n" + "# needs no handling here. The positional form (`./configure CC=clang`)\n" + "# is accepted but ignored until todo 18 exports it.\n" + "for st_arg in \"$@\"; do\n" + " case \"$st_arg\" in\n" + " --prefix=*) prefix=${st_arg#--prefix=} ;;\n" + " --build=*) build=${st_arg#--build=} ;;\n" + " --host=*) host=${st_arg#--host=} ;;\n" + " *=*) : ;;\n" + " *)\n" + " printf 'configure: error: unrecognized option %s\\n' \"$st_arg\" >&2\n" + " exit 1\n" + " ;;\n" + " esac\n" + "done\n" + "\n" + "# cross-compiling when --host differs from --build (both set)\n" + "if [ -n \"$host\" ] && [ \"$host\" != \"$build\" ]; then\n" + " cross_compiling=yes\n" + "fi\n"; + +static const char HOST_DETECT_PRE[] = + "\n" + "# --- host detection ---\n"; + +static const char HOST_DETECT_POST[] = + "st_os=$(uname -s)\n" + "st_os_norm\n" + "st_cc_id=unknown\n"; + +static const char TOOLCHAIN_DEFAULTS_PRE[] = + "\n" + "# --- toolchain defaults (registered variables) ---\n" + "# Each registered variable defaults to its generation-time value, and\n" + "# may be overridden by an environment variable of the same name (the\n" + "# `${VAR:-...}` form). Emitted BEFORE the cache boilerplate so the\n" + "# boilerplate's `:=` fallbacks never clobber these.\n"; + +static const char TOOLCHAIN_LIBS[] = + "# built-in link flags, accumulated by library/pkg_config probes\n" + "LIBS=\"\"\n"; + +static const char SUBST_PRE[] = + "\n" + "# --- substitution: Makefile.in -> Makefile ---\n" + "# The @VAR@ token set is the registered variables plus the built-ins\n" + "# LIBS, prefix, srcdir. Substitution runs at CONFIGURE time (the\n" + "# generator cannot know the user's Makefile.in), so an unknown @VAR@\n" + "# is a configure-time error naming the variable.\n" + "\n" + "# st_subst_lookup NAME: print the value of substitution variable NAME\n" + "# verbatim via printf '%s' (no metacharacter is re-interpreted), or\n" + "# return 1 when NAME is unknown.\n" + "st_subst_lookup() {\n" + " case \"$1\" in\n"; + +static const char SUBST_LOOKUP_TAIL[] = + " *) return 1 ;;\n" + " esac\n" + "}\n" + "\n" + "# st_subst_line LINE: echo LINE with every @VAR@ replaced by $VAR.\n" + "# Why not sed: its 's' replacement re-interprets '&', '\\' and the\n" + "# delimiter, so a value with those bytes is corrupted unless escaped -\n" + "# fragile. Why not ${var//...}: a bash/zsh/ksh extension, not POSIX.\n" + "# Instead the line is split on '@' with pure POSIX parameter expansion\n" + "# (${v%%'@'*}, ${v#*'@'}): a '@' only starts a substitution when a\n" + "# second '@' follows on the same line, so a lone '@' is literal; the\n" + "# name between the two must be known, else configure errors naming it.\n" + "# Values are carried verbatim through printf '%s', so '/', spaces,\n" + "# '&', '$' and backticks all survive byte-exact and never execute.\n" + "# (v1 limitation: a value may not END in a newline - command\n" + "# substitution strips trailing newlines.)\n" + "st_subst_line() {\n" + " st_rest=$1\n" + " st_out=\n" + " while :; do\n" + " case \"$st_rest\" in\n" + " *'@'*'@'*) : ;;\n" + " *)\n" + " st_out=\"$st_out$st_rest\"\n" + " break\n" + " ;;\n" + " esac\n" + " st_out=\"$st_out${st_rest%%'@'*}\"\n" + " st_after=${st_rest#*'@'}\n" + " st_name=${st_after%%'@'*}\n" + " st_rest=${st_after#*'@'}\n" + " if st_val=$(st_subst_lookup \"$st_name\"); then\n" + " st_out=\"$st_out$st_val\"\n" + " else\n" + " printf 'configure: error: unknown variable @%s@ in Makefile.in\\n' \"$st_name\" >&2\n" + " exit 1\n" + " fi\n" + " done\n" + " printf '%s\\n' \"$st_out\"\n" + "}\n" + "\n" + "# st_subst: read ./Makefile.in (the source directory; v1 cwd contract),\n" + "# substitute every @VAR@, write ./Makefile.\n" + "st_subst() {\n" + " if [ ! -f ./Makefile.in ]; then\n" + " printf '%s\\n' 'configure: error: Makefile.in not found' >&2\n" + " exit 1\n" + " fi\n" + " : > ./Makefile\n" + " while IFS= read -r st_line || [ -n \"$st_line\" ]; do\n" + " st_subst_line \"$st_line\" >> ./Makefile\n" + " done < ./Makefile.in\n" + "}\n" + "\n" + "st_subst\n" + "\n" + "exit 0\n"; + +/* ---- section emitters -------------------------------------------------- */ + +/* One library/pkg_config check's link-flags contribution. */ +struct st_lib_entry { + enum st_check_kind kind; /* ST_CHECK_LIBRARY or ST_CHECK_PKG_CONFIG */ + char *target; /* owned library/package name */ +}; + +/* Emit the substitution lookup function's case arms. `names` are already + * validated identifiers, safe in variable-NAME positions. */ +static struct st_error * +emit_subst_lookup(FILE *out, const char *const *names, size_t count) +{ + size_t i; + + if (st_sh_emit_str(out, SUBST_PRE) < 0) { + return st_error_io("I/O error emitting substitution section"); + } + for (i = 0; i < count; i++) { + if (fprintf(out, " %s) printf '%%s' \"$%s\" ;;\n", + names[i], names[i]) < 0) { + return st_error_io("I/O error emitting substitution section"); + } + } + if (st_sh_emit_str(out, SUBST_LOOKUP_TAIL) < 0) { + return st_error_io("I/O error emitting substitution section"); + } + return NULL; +} + +/* Emit the LIBS accumulation for one feature: for each library/pkg_config + * check, append the link flags when the feature resolved yes. Generic over + * the check kind; the pkg_config form is guarded by `command -v pkg-config` + * (v1). */ +static struct st_error * +emit_libs_accumulation(FILE *out, const char *feature, + const struct st_lib_entry *libs, size_t count) +{ + size_t i; + + for (i = 0; i < count; i++) { + char *q; + + if (libs[i].kind == ST_CHECK_LIBRARY) { + q = st_sh_quote(libs[i].target); + if (q == NULL) { + return st_error_internal("out of memory quoting library name"); + } + /* `-l` inside the double quotes, the st_sh_quote'd name OUTSIDE + * them (single quotes are literal inside double quotes, which + * would turn -l'pthread' into a literal-quoted flag). */ + if (fprintf(out, + "if [ \"$have_%s\" = \"yes\" ]; then " + "LIBS=\"$LIBS -l\"%s; fi\n", + feature, q) < 0) { + free(q); + return st_error_io("I/O error emitting LIBS accumulation"); + } + free(q); + } else { /* ST_CHECK_PKG_CONFIG */ + q = st_sh_quote(libs[i].target); + if (q == NULL) { + return st_error_internal("out of memory quoting package name"); + } + /* v1 pkg-config guard: only accumulate when pkg-config exists. */ + if (fprintf(out, + "if command -v pkg-config >/dev/null 2>&1; then\n" + " if [ \"$have_%s\" = \"yes\" ]; then " + "LIBS=\"$LIBS $(pkg-config --libs %s)\"; fi\n" + "fi\n", + feature, q) < 0) { + free(q); + return st_error_io("I/O error emitting LIBS accumulation"); + } + free(q); + } + } + if (ferror(out)) { + return st_error_io("I/O error emitting LIBS accumulation"); + } + return NULL; +} + +/* Emit ONE feature node: its probe snippets, its resolution (aggregation + + * when-gate), then its LIBS accumulation. */ +static struct st_error * +emit_feature(FILE *out, const struct st_kdl_node *node) +{ + struct st_error *err = NULL; + struct st_when_ast *guard = NULL; + char *fname = NULL; + const struct st_kdl_node *child; + const struct st_kdl_prop *prop; + size_t n_checks = 0; + size_t i = 0; + char **check_names = NULL; + struct st_check_probe *probes = NULL; + struct st_lib_entry *libs = NULL; + size_t n_libs = 0; + + /* the feature name (must be a valid shell identifier) */ + if (node->args == NULL) { + return err_at_owned(node->name.span, "feature requires a name"); + } + err = extract_string(&node->args->value, "feature name", &fname); + if (err != NULL) { + return err; + } + if (!valid_ident(fname)) { + char msg[192]; + + snprintf(msg, sizeof msg, + "feature name '%s' is not a valid shell identifier " + "(have_ requires [A-Za-z_][A-Za-z0-9_]*)", fname); + err = err_at_owned(node->args->value.span, msg); + goto done; + } + + /* count checks */ + for (child = node->children; child != NULL; child = child->next) { + n_checks++; + } + + if (n_checks > 0) { + check_names = calloc(n_checks, sizeof(*check_names)); + probes = calloc(n_checks, sizeof(*probes)); + libs = calloc(n_checks, sizeof(*libs)); + if (check_names == NULL || probes == NULL || libs == NULL) { + err = st_error_internal("out of memory building feature"); + goto done; + } + } + + /* build every check's probe + checkname, collecting lib/pkg_config */ + for (child = node->children; child != NULL; child = child->next) { + enum st_check_kind kind; + char namebuf[128]; + + kind = st_check_kind_from_node(child, &err); + if (err != NULL) { + goto done; + } + if (st_resolve_check_name(fname, i, n_checks, namebuf, + sizeof namebuf) != 0) { + err = st_error_internal("failed to derive check name"); + goto done; + } + check_names[i] = strbuild("%s", namebuf); + if (check_names[i] == NULL) { + err = st_error_internal("out of memory deriving check name"); + goto done; + } + err = st_check_probe_build(kind, child, &probes[i]); + if (err != NULL) { + goto done; + } + if (kind == ST_CHECK_LIBRARY || kind == ST_CHECK_PKG_CONFIG) { + libs[n_libs].kind = kind; + err = extract_string(&child->args->value, + kind == ST_CHECK_LIBRARY + ? "library check argument" + : "pkg_config check argument", + &libs[n_libs].target); + if (err != NULL) { + goto done; + } + n_libs++; + } + i++; + } + + /* emit the probe snippets (todo 12), in document order */ + for (i = 0; i < n_checks; i++) { + err = st_probe_emit_snippet(check_names[i], &probes[i], out); + if (err != NULL) { + goto done; + } + } + + /* the `when` guard (optional feature property) */ + for (prop = node->props; prop != NULL; prop = prop->next) { + bool is_when = false; + + err = name_equals(&prop->key, "when", &is_when); + if (err != NULL) { + goto done; + } + if (is_when) { + char *guard_str = NULL; + + err = extract_string(&prop->value, "feature 'when'", &guard_str); + if (err != NULL) { + goto done; + } + err = st_when_parse_at(guard_str, &node->name.span, &guard); + free(guard_str); + if (err != NULL) { + goto done; + } + break; + } + } + + /* feature resolution: aggregation + when-gate (todo 14) */ + { + struct st_resolve_feature f; + + f.name = fname; + f.check_names = (const char *const *)check_names; + f.check_count = n_checks; + f.guard = guard; + err = st_resolve_emit_feature(out, &f); + if (err != NULL) { + goto done; + } + } + + /* LIBS accumulation (library/pkg_config checks), gated on have_ */ + err = emit_libs_accumulation(out, fname, libs, n_libs); + if (err != NULL) { + goto done; + } + +done: + for (i = 0; i < n_checks; i++) { + st_check_probe_free(&probes[i]); + free(check_names != NULL ? check_names[i] : NULL); + } + for (i = 0; i < n_libs; i++) { + free(libs[i].target); + } + free(check_names); + free(probes); + free(libs); + st_when_free(guard); + free(fname); + return err; +} + +/* ---- the public API ---------------------------------------------------- */ + +struct st_error * +st_gen_configure_emit(FILE *out, const struct st_kdl_document *doc, + struct st_ext_ctx *ctx) +{ + struct st_registry *reg; + size_t n_reg; + size_t i; + struct st_error *err = NULL; + const struct st_kdl_node *node; + char **subst_names = NULL; + size_t n_subst = 0; + + if (out == NULL || doc == NULL || ctx == NULL) { + return st_error_usage("st_gen_configure_emit: NULL argument"); + } + reg = st_ext_var_registry(ctx); + if (reg == NULL) { + return st_error_usage("st_gen_configure_emit: ctx has no variable " + "registry"); + } + n_reg = st_registry_var_count(reg); + + /* collect the full @VAR@ substitution set: registered vars + the + * built-ins LIBS/prefix/srcdir */ + n_subst = n_reg + 3; + subst_names = calloc(n_subst, sizeof(*subst_names)); + if (subst_names == NULL) { + return st_error_internal("out of memory building substitution set"); + } + for (i = 0; i < n_reg; i++) { + const char *name = st_registry_var_name(reg, i); + + if (!valid_ident(name)) { + char msg[192]; + + snprintf(msg, sizeof msg, + "registered variable name '%s' is not a valid shell " + "identifier", name != NULL ? name : "?"); + err = st_error_usage(msg); + goto done; + } + subst_names[i] = (char *)name; + } + subst_names[n_reg + 0] = "LIBS"; + subst_names[n_reg + 1] = "prefix"; + subst_names[n_reg + 2] = "srcdir"; + + /* 1. shebang + header comment */ + if (st_sh_emit_str(out, "#!/bin/sh\n") < 0 || + st_sh_emit_comment(out, + "Generated by stupidtools " STUPIDTOOLS_VERSION + " - DO NOT EDIT") < 0) { + err = st_error_io("I/O error emitting configure header"); + goto done; + } + + /* 2. preamble (minimal args; todo 18 replaces it) */ + if (st_sh_emit_str(out, PREAMBLE) < 0) { + err = st_error_io("I/O error emitting preamble"); + goto done; + } + + /* 3. host detection */ + if (st_sh_emit_str(out, HOST_DETECT_PRE) < 0) { + err = st_error_io("I/O error emitting host detection"); + goto done; + } + err = st_resolve_emit_os_norm(out); + if (err != NULL) { + goto done; + } + if (st_sh_emit_str(out, HOST_DETECT_POST) < 0) { + err = st_error_io("I/O error emitting host detection"); + goto done; + } + + /* 4. toolchain defaults (before the cache boilerplate; see the header) */ + if (st_sh_emit_str(out, TOOLCHAIN_DEFAULTS_PRE) < 0) { + err = st_error_io("I/O error emitting toolchain defaults"); + goto done; + } + for (i = 0; i < n_reg; i++) { + const char *name = st_registry_var_name(reg, i); + const char *value = st_registry_var_value(reg, i); + char *q = st_sh_quote(value != NULL ? value : ""); + + if (q == NULL) { + err = st_error_internal("out of memory quoting registered " + "variable"); + goto done; + } + /* No outer double quotes: inside "${VAR:-...}" the single quotes + * of a st_sh_quote'd default are LITERAL, so an empty default would + * become the two-character string "''". The unquoted ${VAR:-'...'} + * form is safe because assignment RHS is not field-split or globbed + * and quote-removal strips the single quotes, yielding the exact + * registry value (spaces/&/$ preserved). */ + if (fprintf(out, "%s=${%s:-%s}\n", name, name, q) < 0) { + free(q); + err = st_error_io("I/O error emitting toolchain defaults"); + goto done; + } + free(q); + } + if (st_sh_emit_str(out, TOOLCHAIN_LIBS) < 0) { + err = st_error_io("I/O error emitting toolchain defaults"); + goto done; + } + + /* 5. cache boilerplate (todo 12) */ + err = st_probe_emit_cache_functions(out); + if (err != NULL) { + goto done; + } + + /* 6. probe section: each top-level `feature` node in document order */ + for (node = doc->nodes; node != NULL; node = node->next) { + bool is_feature = false; + + err = name_equals(&node->name, "feature", &is_feature); + if (err != NULL) { + goto done; + } + if (!is_feature) { + continue; + } + err = emit_feature(out, node); + if (err != NULL) { + goto done; + } + } + + /* 7. substitution section */ + err = emit_subst_lookup(out, (const char *const *)subst_names, n_subst); + if (err != NULL) { + goto done; + } + + /* 8. exit 0 is already the last line of the substitution tail */ + if (ferror(out)) { + err = st_error_io("I/O error emitting configure"); + goto done; + } + +done: + free(subst_names); + return err; +} + +struct st_error * +st_gen_configure_emit_path(const char *path, const struct st_kdl_document *doc, + struct st_ext_ctx *ctx) +{ + FILE *f; + struct st_error *err; + + if (path == NULL) { + return st_error_usage("st_gen_configure_emit_path: NULL path"); + } + f = fopen(path, "w"); + if (f == NULL) { + return st_error_io("cannot open output file for writing"); + } + err = st_gen_configure_emit(f, doc, ctx); + if (fclose(f) != 0 && err == NULL) { + err = st_error_io("I/O error closing output file"); + } + return err; +} diff --git a/src/gen/configure.h b/src/gen/configure.h new file mode 100644 index 0000000..06a0cad --- /dev/null +++ b/src/gen/configure.h @@ -0,0 +1,116 @@ +/* + * configure.h - the ./configure generator (todo 16). + * + * THE INTEGRATION TODO: assembles everything the detect chain (todos + * 9/10/11/12/14) and the sh-emitter (15) built into a WORKING generated + * ./configure. Given a schema-validated DSL document (src/kdl/schema.h) + * plus an extension context (src/ext/abi.h) whose variable registry the + * C/C++ language modules have already populated, it EMITS a complete + * POSIX-sh ./configure to a FILE. + * + * GENERATED SCRIPT LAYOUT (the contract todos 17/18/23 consume) + * ------------------------------------------------------------- + * 1. `#!/bin/sh` + a header comment (generated-by, version). + * 2. PREAMBLE — v1 MINIMAL argument handling, structurally marked with a + * `# === preamble ===` comment so todo 18 replaces it whole: + * - `--prefix=` stores $prefix (default /usr/local); + * - `--host=` / `--build=` store $host / $build; + * $cross_compiling=yes when --host differs from --build (a simple + * string comparison; both must be set), else no; + * - `VAR=VALUE` overrides are documented as already being environment + * variables when configure runs (the invoking shell exported them); + * the positional form is accepted-but-ignored until todo 18; + * - an unknown `--*` prints "unrecognized option" to stderr and + * exits 1. + * 3. HOST DETECTION: `st_os=$(uname -s)`, then the st_os_norm() emitted by + * resolve.h's st_resolve_emit_os_norm() rewrites it in place; `st_cc_id + * =unknown` (v1, no compiler-id sniffing at configure time). + * 4. TOOLCHAIN DEFAULTS: for each registered var name (CC/CFLAGS/...) emit + * `VAR="${VAR:-}"` where is the registry VALUE at + * generation time (st_sh_quote'd; typically empty for CFLAGS), then + * `LIBS=""`. Emitted BEFORE the probe cache boilerplate because that + * boilerplate uses `:=` fallbacks that must not clobber these values. + * 5. CACHE BOILERPLATE: probe.h's st_probe_emit_cache_functions (todo 12). + * 6. PROBE SECTION: for each top-level `feature` node in document order, + * for each check child, derive the checkname via resolve.h's + * st_resolve_check_name, build the probe via checks.h's + * st_check_probe_build, and emit the snippet via probe.h's + * st_probe_emit_snippet; then the feature aggregation + when-gate via + * st_resolve_emit_feature (todo 14); then the LIBS accumulation: for a + * `library "X"` check, `if [ "$have_" = "yes" ]; then + * LIBS="$LIBS -l'X'"; fi`; for `pkg_config "X"`, the same wrapped in a + * v1 pkg-config guard `if command -v pkg-config >/dev/null 2>&1; then + * ... LIBS="$LIBS $(pkg-config --libs 'X')" ...; fi`. Generic over the + * check kinds, never hardcoded per feature name. + * 7. SUBSTITUTION: reads ./Makefile.in (cwd, the v1 source-dir contract) + * and replaces every `@VAR@` (VAR = registered vars ∪ {LIBS, prefix, + * srcdir}) with the shell variable's value, writing ./Makefile. An + * UNRESOLVED `@SOMETHING@` is a CONFIGURE-TIME error naming the + * variable (the generator cannot know the user's Makefile.in), reported + * after substitution via the per-line lookup; a missing Makefile.in is + * a clean error. See the SUBSTITUTION approach note below. + * 8. `exit 0`. + * + * SUBSTITUTION APPROACH (documented because the plan flagged it) + * -------------------------------------------------------------- + * `${var//...}` is NOT POSIX, and sed's `s` replacement re-interprets `&`, + * `\` and the delimiter (values with `/`, spaces, `&` would be corrupted or + * need fragile escaping). The generated script therefore splits each line + * on `@` with pure POSIX parameter expansion (${v%%'@'*}, ${v#*'@'}) and + * looks each token up in a generated `case` (one arm per known variable, + * `printf '%s' "$VAR"` — the value is carried verbatim and never + * re-interpreted, so `$`, backticks, `/`, spaces, `&` all survive + * byte-exact and never execute). A `@` starts a substitution only when a + * second `@` follows on the same line, so a lone `@` stays literal. Known + * v1 limitation: a value may not END in a newline (command substitution + * strips trailing newlines); documented in the emitted script. + * + * POSIX-SH ONLY (AGENTS.md §4): the emitted script uses no `[[ ]]`, no + * arrays, no `local`, no `==`, no `<<<`, no `&>`, no `set -e`; `$(...)` + * is used. `sh -n` / `bash -n` / `zsh -n` must all pass (dash is not + * installed on this host — see .omo/notepads/stupidtools/issues.md). + * + * ERRORS + * ------ + * All functions return NULL on success or an owned st_error. Failures are + * ST_ERR_USAGE for NULL arguments; ST_ERR_KDL_SCHEMA (span heap-allocated + * in the same block as the error, the parser.c pattern) for a feature name + * that is not a valid POSIX shell identifier; ST_ERR_INTERNAL for quoting + * allocation failures; ST_ERR_IO for stream write failures or (emit_path) + * an unopenable path. The document is only read, never modified or freed; + * the ctx is only read (its registry values are borrowed). + * + * Copyright (c) 2026 huntedbytheirs + * SPDX-License-Identifier: BSD-3-Clause + */ + +#ifndef ST_GEN_CONFIGURE_H +#define ST_GEN_CONFIGURE_H + +#include + +struct st_error; +struct st_ext_ctx; +struct st_kdl_document; + +/* + * Emit the complete POSIX-sh ./configure for `doc` (schema-validated) into + * `out`. Reads the registered variables from `ctx`'s variable registry and + * substitutes them by name; never hardcodes CC/CFLAGS/... in core. Returns + * NULL on success, or an owned st_error (see the header's ERRORS section). + */ +struct st_error *st_gen_configure_emit(FILE *out, + const struct st_kdl_document *doc, + struct st_ext_ctx *ctx); + +/* + * Convenience wrapper: open `path` for writing ("w", text) and emit into + * it. On success the file is fully written and closed; on failure it is + * closed (best-effort) and an owned st_error is returned (ST_ERR_IO when + * the file cannot be created, otherwise the emit error). + */ +struct st_error *st_gen_configure_emit_path(const char *path, + const struct st_kdl_document *doc, + struct st_ext_ctx *ctx); + +#endif /* ST_GEN_CONFIGURE_H */ diff --git a/tests/unit/test_gen_configure.c b/tests/unit/test_gen_configure.c new file mode 100644 index 0000000..38eb796 --- /dev/null +++ b/tests/unit/test_gen_configure.c @@ -0,0 +1,653 @@ +/* LINK: ../../src/gen/configure.c ../../src/gen/sh_emit.c ../../src/detect/probe.c ../../src/detect/checks.c ../../src/detect/check_registry.c ../../src/detect/resolve.c ../../src/kdl/schema.c ../../src/kdl/parser.c ../../src/kdl/lexer.c ../../src/kdl/value.c ../../src/ext/abi.c ../../src/ext/lang_c.c ../../src/ext/lang_cpp.c ../../src/error.c ../../src/span.c */ +#ifndef _POSIX_C_SOURCE +#define _POSIX_C_SOURCE 200809L /* mkdtemp/unsetenv/setenv (POSIX.1-2008) */ +#endif +/* tests/unit/test_gen_configure.c + * + * Unit + integration tests for the ./configure generator (todo 16): + * src/gen/configure.c. THE MODEL: the generator EMITS a POSIX-sh script + * from a schema-validated DSL document + an extension ctx; the emitted + * script RUNS at configure time (probes + @VAR@ substitution). These tests + * prove the emitted script is syntactically valid (sh/bash/zsh -n, no + * banned constructs), REALLY runs (exit 0, Makefile written, CC/LIBS/ + * prefix correct), honors --prefix, errors cleanly on an unknown @VAR@, a + * missing Makefile.in and an unrecognized option, carries values with + * '/', spaces and '&' byte-exact, keeps $(...) / backticks inert, and + * cross-compile-skips RUN probes. + * + * The magic LINK comment on line 1 is REQUIRED by tests/run.sh (extra .c + * sources, relative to tests/unit/). configure.c pulls in the whole detect + * chain (probe/checks/check_registry/resolve) + sh_emit + abi (the C/C++ + * modules register CC/CFLAGS) + kdl (parse/lexer/value/schema) + error/span. + */ +#include "munit.h" + +#include "detect/check_registry.h" +#include "error.h" +#include "ext/abi.h" +#include "gen/configure.h" +#include "kdl/ast.h" +#include "kdl/schema.h" + +#include +#include +#include +#include +#include +#include + +/* ---- per-test temp dir ------------------------------------------------ */ + +static char temp_dir[128]; + +static void * +setup(const MunitParameter params[], void *user_data) +{ + (void)params; + (void)user_data; + if (snprintf(temp_dir, sizeof temp_dir, "/tmp/st_gen_XXXXXX") < 0) { + return NULL; + } + if (mkdtemp(temp_dir) == NULL) { + return NULL; + } + return (void *)1; +} + +static void +teardown(void *fixture) +{ + char cmd[160]; + + if (fixture == NULL || temp_dir[0] == '\0') { + return; + } + if (snprintf(cmd, sizeof cmd, "rm -rf -- '%s'", temp_dir) < 0) { + return; + } + (void)system(cmd); /* best-effort; mkdtemp names are [A-Za-z0-9_]* */ + temp_dir[0] = '\0'; +} + +/* ---- small file/process helpers --------------------------------------- */ + +/* Join temp_dir/`name` into `buf`; returns the snprintf result. */ +static int +mkpath(char *buf, size_t sz, const char *name) +{ + return snprintf(buf, sz, "%s/%s", temp_dir, name); +} + +/* Slurp a file into a NUL-terminated heap buffer, or NULL. Caller frees. */ +static char * +slurp(const char *path) +{ + FILE *f; + long n; + char *buf; + + f = fopen(path, "rb"); + if (f == NULL) { + return NULL; + } + if (fseek(f, 0, SEEK_END) != 0) { + fclose(f); + return NULL; + } + n = ftell(f); + if (n < 0 || fseek(f, 0, SEEK_SET) != 0) { + fclose(f); + return NULL; + } + buf = malloc((size_t)n + 1); + if (buf == NULL) { + fclose(f); + return NULL; + } + if (fread(buf, 1, (size_t)n, f) != (size_t)n) { + free(buf); + fclose(f); + return NULL; + } + fclose(f); + buf[n] = '\0'; + return buf; +} + +/* Slurp temp_dir/Makefile, or NULL. Caller frees. */ +static char * +makefile_text(void) +{ + char path[600]; + + if (mkpath(path, sizeof path, "Makefile") < 0) { + return NULL; + } + return slurp(path); +} + +/* Slurp temp_dir/run.err, or NULL. Caller frees. */ +static char * +stderr_text(void) +{ + char path[600]; + + if (mkpath(path, sizeof path, "run.err") < 0) { + return NULL; + } + return slurp(path); +} + +/* Write `content` to temp_dir/Makefile.in. Returns 0 on success. */ +static int +write_makefile_in(const char *content) +{ + char path[600]; + FILE *f; + + if (mkpath(path, sizeof path, "Makefile.in") < 0) { + return -1; + } + f = fopen(path, "w"); + if (f == NULL) { + return -1; + } + if (fputs(content, f) == EOF) { + fclose(f); + return -1; + } + fclose(f); + return 0; +} + +/* Run "sh ./configure " with cwd = temp_dir, stderr to run.err. + * Returns the configure exit status, or -1 if it could not be spawned. */ +static int +run_configure(const char *args) +{ + char cmd[2400]; + int n; + int spawned; + + n = snprintf(cmd, sizeof cmd, "cd '%s' && sh ./configure %s 2>'%s/run.err'", + temp_dir, args != NULL ? args : "", temp_dir); + if (n < 0 || (size_t)n >= sizeof cmd) { + return -1; + } + spawned = system(cmd); + if (spawned == -1) { + return -1; + } + return WEXITSTATUS(spawned); +} + +/* " -n ": the shell's exit status or -1. */ +static int +syntax_check(const char *shell, const char *path) +{ + char cmd[700]; + int n; + int spawned; + + n = snprintf(cmd, sizeof cmd, "%s -n '%s'", shell, path); + if (n < 0 || (size_t)n >= sizeof cmd) { + return -1; + } + spawned = system(cmd); + if (spawned == -1) { + return -1; + } + return WEXITSTATUS(spawned); +} + +/* ---- fixture loading (same as test_schema.c) -------------------------- */ + +static const char * +fixture_path(void) +{ + static const char *const candidates[] = { + "tests/fixtures/stupid.kdl", + "../fixtures/stupid.kdl", + }; + size_t i; + + for (i = 0; i < sizeof(candidates) / sizeof(candidates[0]); i++) { + FILE *f = fopen(candidates[i], "rb"); + + if (f != NULL) { + fclose(f); + return candidates[i]; + } + } + return NULL; +} + +/* Build a ctx with the builtins + C detected (env CC/CFLAGS/etc. reset). + * Returns NULL on any failure. */ +static struct st_ext_ctx * +build_ctx(void) +{ + struct st_ext_ctx *ctx = st_ext_ctx_new(); + struct st_error *err; + + if (ctx == NULL) { + return NULL; + } + unsetenv("CC"); + unsetenv("CFLAGS"); + unsetenv("CXX"); + unsetenv("CXXFLAGS"); + err = st_ext_init_builtins(ctx); + if (err != NULL) { + st_error_free(err); + st_ext_ctx_free(ctx); + return NULL; + } + err = st_ext_detect_language(ctx, "c"); + if (err != NULL) { + st_error_free(err); + st_ext_ctx_free(ctx); + return NULL; + } + return ctx; +} + +/* Parse + validate + emit the fixture into temp_dir/configure, using + * `ctx`. Returns 0 on success, -1 on failure. */ +static int +emit_fixture_configure(struct st_ext_ctx *ctx) +{ + const char *path = fixture_path(); + char conf[600]; + char *src; + struct st_kdl_document *doc = NULL; + struct st_error *err = NULL; + + if (path == NULL || ctx == NULL) { + return -1; + } + src = slurp(path); + if (src == NULL) { + return -1; + } + doc = st_kdl_parse(src, path, &err); + if (err != NULL) { + st_error_free(err); + free(src); + return -1; + } + if (doc == NULL) { + free(src); + return -1; + } + err = st_kdl_validate(doc); + if (err != NULL) { + st_error_free(err); + st_kdl_document_free(doc); + free(src); + return -1; + } + if (mkpath(conf, sizeof conf, "configure") < 0) { + st_kdl_document_free(doc); + free(src); + return -1; + } + err = st_gen_configure_emit_path(conf, doc, ctx); + /* the document borrows `src`'s token slices, so free src only after + * the document is released */ + st_kdl_document_free(doc); + free(src); + if (err != NULL) { + st_error_free(err); + return -1; + } + return 0; +} + +/* The standard Makefile.in every end-to-end case uses. */ +#define STD_MAKEFILE_IN \ + "CC = @CC@\n" \ + "LIBS = @LIBS@\n" \ + "prefix = @prefix@\n" \ + "FLAGS = @CFLAGS@\n" + +/* ---- (a) the emitted configure passes the three-shell syntax oracle --- */ + +static MunitResult +test_syntax(const MunitParameter params[], void *data) +{ + (void)params; + (void)data; + static const char *const banned[] = { + "[[ ", "]]", "local ", "==", "<<<", "&>", "set -e", + }; + struct st_ext_ctx *ctx; + char conf[600]; + char *bytes; + size_t i; + + ctx = build_ctx(); + munit_assert_not_null(ctx); + munit_assert_int(emit_fixture_configure(ctx), ==, 0); + st_ext_ctx_free(ctx); + + munit_assert_int(mkpath(conf, sizeof conf, "configure"), >, 0); + munit_assert_int(syntax_check("sh", conf), ==, 0); + munit_assert_int(syntax_check("bash", conf), ==, 0); + munit_assert_int(syntax_check("zsh", conf), ==, 0); + + bytes = slurp(conf); + munit_assert_not_null(bytes); + for (i = 0; i < sizeof(banned) / sizeof(banned[0]); i++) { + munit_assert_null(strstr(bytes, banned[i])); + } + /* the preamble is structurally marked for todo 18 */ + munit_assert_not_null(strstr(bytes, "# --- PREAMBLE ---")); + free(bytes); + return MUNIT_OK; +} + +/* ---- (b) real end-to-end run ------------------------------------------ */ + +static MunitResult +test_end_to_end(const MunitParameter params[], void *data) +{ + (void)params; + (void)data; + struct st_ext_ctx *ctx; + const char *cc; + char cc_buf[64]; + char want[128]; + char *mk; + int cc_len; + + ctx = build_ctx(); + munit_assert_not_null(ctx); + cc = st_registry_get_var(st_ext_var_registry(ctx), "CC"); + munit_assert_not_null(cc); + /* copy before st_ext_ctx_free() — the registry value is borrowed */ + cc_len = snprintf(cc_buf, sizeof cc_buf, "%s", cc); + munit_assert_int(cc_len, >, 0); + munit_assert_int(emit_fixture_configure(ctx), ==, 0); + st_ext_ctx_free(ctx); + + munit_assert_int(write_makefile_in(STD_MAKEFILE_IN), ==, 0); + munit_assert_int(run_configure(NULL), ==, 0); + + mk = makefile_text(); + munit_assert_not_null(mk); + /* CC substituted from the registry value */ + { + int n = snprintf(want, sizeof want, "CC = %s", cc_buf); + + munit_assert_int(n, >, 0); + munit_assert_not_null(strstr(mk, want)); + } + /* LIBS accumulated from library checks (pthread + m) */ + munit_assert_not_null(strstr(mk, "-lpthread")); + munit_assert_not_null(strstr(mk, "-lm")); + /* prefix defaults to /usr/local */ + munit_assert_not_null(strstr(mk, "prefix = /usr/local")); + free(mk); + return MUNIT_OK; +} + +/* ---- (c) --prefix is honored ------------------------------------------ */ + +static MunitResult +test_prefix(const MunitParameter params[], void *data) +{ + (void)params; + (void)data; + struct st_ext_ctx *ctx; + char *mk; + + ctx = build_ctx(); + munit_assert_not_null(ctx); + munit_assert_int(emit_fixture_configure(ctx), ==, 0); + st_ext_ctx_free(ctx); + + munit_assert_int(write_makefile_in(STD_MAKEFILE_IN), ==, 0); + munit_assert_int(run_configure("--prefix=/opt/x"), ==, 0); + + mk = makefile_text(); + munit_assert_not_null(mk); + munit_assert_not_null(strstr(mk, "prefix = /opt/x")); + free(mk); + return MUNIT_OK; +} + +/* ---- (d) an unknown @VAR@ errors naming the variable ------------------ */ + +static MunitResult +test_unknown_var(const MunitParameter params[], void *data) +{ + (void)params; + (void)data; + struct st_ext_ctx *ctx; + char *err; + + ctx = build_ctx(); + munit_assert_not_null(ctx); + munit_assert_int(emit_fixture_configure(ctx), ==, 0); + st_ext_ctx_free(ctx); + + munit_assert_int(write_makefile_in("CC = @NOPE@\n"), ==, 0); + munit_assert_int(run_configure(NULL), !=, 0); + + err = stderr_text(); + munit_assert_not_null(err); + munit_assert_not_null(strstr(err, "NOPE")); + free(err); + return MUNIT_OK; +} + +/* ---- (e) a missing Makefile.in is a clean error ----------------------- */ + +static MunitResult +test_missing_makefile(const MunitParameter params[], void *data) +{ + (void)params; + (void)data; + struct st_ext_ctx *ctx; + char *err; + + ctx = build_ctx(); + munit_assert_not_null(ctx); + munit_assert_int(emit_fixture_configure(ctx), ==, 0); + st_ext_ctx_free(ctx); + + /* no Makefile.in written at all */ + munit_assert_int(run_configure(NULL), !=, 0); + + err = stderr_text(); + munit_assert_not_null(err); + munit_assert_not_null(strstr(err, "Makefile.in")); + free(err); + return MUNIT_OK; +} + +/* ---- (f) '/', spaces and '&' survive byte-exact ----------------------- */ + +static MunitResult +test_bytes_exact(const MunitParameter params[], void *data) +{ + (void)params; + (void)data; + struct st_ext_ctx *ctx; + char *mk; + + ctx = build_ctx(); + munit_assert_not_null(ctx); + munit_assert_int(emit_fixture_configure(ctx), ==, 0); + st_ext_ctx_free(ctx); + + /* spaces via CFLAGS; '&' and '/' via --prefix */ + munit_assert_int(write_makefile_in(STD_MAKEFILE_IN), ==, 0); + munit_assert_int(setenv("CFLAGS", "a b c", 1), ==, 0); + munit_assert_int(run_configure("--prefix='/opt/x & y/z'"), ==, 0); + unsetenv("CFLAGS"); + + mk = makefile_text(); + munit_assert_not_null(mk); + munit_assert_not_null(strstr(mk, "FLAGS = a b c")); + munit_assert_not_null(strstr(mk, "prefix = /opt/x & y/z")); + free(mk); + return MUNIT_OK; +} + +/* ---- unrecognized --flag is a clean error ----------------------------- */ + +static MunitResult +test_unrecognized_option(const MunitParameter params[], void *data) +{ + (void)params; + (void)data; + struct st_ext_ctx *ctx; + char *err; + + ctx = build_ctx(); + munit_assert_not_null(ctx); + munit_assert_int(emit_fixture_configure(ctx), ==, 0); + st_ext_ctx_free(ctx); + + munit_assert_int(run_configure("--bogus"), !=, 0); + + err = stderr_text(); + munit_assert_not_null(err); + munit_assert_not_null(strstr(err, "unrecognized option")); + free(err); + return MUNIT_OK; +} + +/* ---- injection: a $(...) VALUE stays inert (not executed) ------------- */ + +static MunitResult +test_injection_value(const MunitParameter params[], void *data) +{ + (void)params; + (void)data; + struct st_ext_ctx *ctx; + char marker[600]; + char *mk; + + ctx = build_ctx(); + munit_assert_not_null(ctx); + munit_assert_int(emit_fixture_configure(ctx), ==, 0); + st_ext_ctx_free(ctx); + + munit_assert_int(mkpath(marker, sizeof marker, "pwned1"), >, 0); + + /* prefix gets a command-substitution payload; it must be a literal. */ + munit_assert_int(write_makefile_in("prefix = @prefix@\n"), ==, 0); + munit_assert_int(run_configure("--prefix='$(touch pwned1)'"), ==, 0); + + munit_assert_int(access(marker, F_OK), ==, -1); + mk = makefile_text(); + munit_assert_not_null(mk); + munit_assert_not_null(strstr(mk, "prefix = $(touch pwned1)")); + free(mk); + return MUNIT_OK; +} + +/* ---- injection: an unknown @$(...)@ name stays inert (not executed) --- */ + +static MunitResult +test_injection_unknown_name(const MunitParameter params[], void *data) +{ + (void)params; + (void)data; + struct st_ext_ctx *ctx; + char marker[600]; + char *err; + + ctx = build_ctx(); + munit_assert_not_null(ctx); + munit_assert_int(emit_fixture_configure(ctx), ==, 0); + st_ext_ctx_free(ctx); + + munit_assert_int(mkpath(marker, sizeof marker, "pwned2"), >, 0); + + munit_assert_int(write_makefile_in("X = @$(touch pwned2)@\n"), ==, 0); + munit_assert_int(run_configure(NULL), !=, 0); + + munit_assert_int(access(marker, F_OK), ==, -1); + err = stderr_text(); + munit_assert_not_null(err); + munit_assert_not_null(strstr(err, "unknown variable")); + free(err); + return MUNIT_OK; +} + +/* ---- cross-compile: RUN probes skip with a warning -------------------- */ + +static MunitResult +test_cross_compile(const MunitParameter params[], void *data) +{ + (void)params; + (void)data; + static const char *const docsrc = + "project \"x\" version \"1.0\"\n" + "feature \"sz\" { sizeof \"long\" }\n"; + struct st_ext_ctx *ctx; + struct st_kdl_document *doc = NULL; + struct st_error *err = NULL; + char conf[600]; + char *e; + + ctx = build_ctx(); + munit_assert_not_null(ctx); + + doc = st_kdl_parse(docsrc, "inline.kdl", &err); + munit_assert_null(err); + munit_assert_not_null(doc); + munit_assert_null(st_kdl_validate(doc)); + munit_assert_int(mkpath(conf, sizeof conf, "configure"), >, 0); + munit_assert_null(st_gen_configure_emit_path(conf, doc, ctx)); + st_kdl_document_free(doc); + st_ext_ctx_free(ctx); + + munit_assert_int(write_makefile_in("CC = @CC@\n"), ==, 0); + munit_assert_int(run_configure("--host=aarch64-linux --build=x86_64-linux"), + ==, 0); + + e = stderr_text(); + munit_assert_not_null(e); + munit_assert_not_null(strstr(e, "cross-compiling")); + free(e); + return MUNIT_OK; +} + +static MunitTest tests[] = { + { "/gen/syntax", test_syntax, setup, teardown, MUNIT_TEST_OPTION_NONE, + NULL }, + { "/gen/end-to-end", test_end_to_end, setup, teardown, + MUNIT_TEST_OPTION_NONE, NULL }, + { "/gen/prefix", test_prefix, setup, teardown, MUNIT_TEST_OPTION_NONE, + NULL }, + { "/gen/unknown-var", test_unknown_var, setup, teardown, + MUNIT_TEST_OPTION_NONE, NULL }, + { "/gen/missing-makefile", test_missing_makefile, setup, teardown, + MUNIT_TEST_OPTION_NONE, NULL }, + { "/gen/bytes-exact", test_bytes_exact, setup, teardown, + MUNIT_TEST_OPTION_NONE, NULL }, + { "/gen/unrecognized-option", test_unrecognized_option, setup, teardown, + MUNIT_TEST_OPTION_NONE, NULL }, + { "/gen/injection-value", test_injection_value, setup, teardown, + MUNIT_TEST_OPTION_NONE, NULL }, + { "/gen/injection-unknown-name", test_injection_unknown_name, setup, + teardown, MUNIT_TEST_OPTION_NONE, NULL }, + { "/gen/cross-compile", test_cross_compile, setup, teardown, + MUNIT_TEST_OPTION_NONE, NULL }, + { NULL, NULL, NULL, NULL, MUNIT_TEST_OPTION_NONE, NULL }, +}; + +static const MunitSuite suite = { + "/gen", 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); +}