/* * 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/args.h" #include "gen/config.h" #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 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"; /* ---- 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; } /* Append `name` (owned) to the feature-name list, growing it as needed. * Used by the probe-section loop to collect the names the todo-17 results * summary + config.h defines are emitted from. */ static struct st_error * collect_feature_name(char ***names, size_t *count, size_t *cap, char *name) { if (*count == *cap) { size_t ncap = *cap == 0 ? 8 : *cap * 2; char **nn = realloc(*names, ncap * sizeof(*nn)); if (nn == NULL) { return st_error_internal("out of memory collecting feature " "names"); } *names = nn; *cap = ncap; } (*names)[(*count)++] = name; return NULL; } /* ---- 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; struct st_gen_args args; char **subst_names = NULL; size_t n_subst = 0; char **feat_names = NULL; size_t n_feat = 0; size_t feat_cap = 0; if (out == NULL || doc == NULL || ctx == NULL) { return st_error_usage("st_gen_configure_emit: NULL argument"); } memset(&args, 0, sizeof args); err = st_gen_args_collect(doc, &args); if (err != NULL) { goto done; } reg = st_ext_var_registry(ctx); if (reg == NULL) { err = st_error_usage("st_gen_configure_emit: ctx has no variable " "registry"); goto done; } n_reg = st_registry_var_count(reg); /* collect the full @VAR@ substitution set: registered vars + the * built-ins LIBS/prefix/exec_prefix/srcdir/host/build/cross_compiling * + one enable_ per DSL option (todo 18) */ n_subst = n_reg + 7 + args.option_count; subst_names = calloc(n_subst, sizeof(*subst_names)); if (subst_names == NULL) { err = st_error_internal("out of memory building substitution set"); goto done; } 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] = "exec_prefix"; subst_names[n_reg + 3] = "srcdir"; subst_names[n_reg + 4] = "host"; subst_names[n_reg + 5] = "build"; subst_names[n_reg + 6] = "cross_compiling"; for (i = 0; i < args.option_count; i++) { subst_names[n_reg + 7 + i] = strbuild("enable_%s", args.options[i].name); if (subst_names[n_reg + 7 + i] == NULL) { err = st_error_internal("out of memory building substitution " "set"); goto done; } } /* 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: full argument parsing (todo 18) */ err = st_gen_args_emit(out, &args); if (err != NULL) { goto done; } /* 3. config.log init + original-args capture (todo 17) */ err = st_gen_config_log_init_emit(out); if (err != NULL) { goto done; } /* 4. 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; } /* 5. 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; } /* 6. cache boilerplate (todo 12) */ err = st_probe_emit_cache_functions(out); if (err != NULL) { goto done; } /* 7. probe section: each top-level `feature` node in document order. * The resolved feature names are collected here for the todo-17 * results summary + config.h defines, emitted right after the loop. */ for (node = doc->nodes; node != NULL; node = node->next) { bool is_feature = false; char *fname = NULL; 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; } /* a successful emit_feature guarantees the name is a valid shell * identifier (it errors otherwise), so the have_ / * HAVE_ interpolation below is safe */ err = extract_string(&node->args->value, "feature name", &fname); if (err != NULL) { goto done; } err = collect_feature_name(&feat_names, &n_feat, &feat_cap, fname); if (err != NULL) { free(fname); goto done; } } /* 8. probe-results summary + config.h (todo 17) */ err = st_gen_config_results_emit(out, (const char *const *)feat_names, n_feat); if (err != NULL) { goto done; } err = st_gen_config_h_emit(out, (const char *const *)feat_names, n_feat); if (err != NULL) { goto done; } /* 9. substitution section */ err = emit_subst_lookup(out, (const char *const *)subst_names, n_subst); if (err != NULL) { goto done; } /* 10. config.status (todo 17): written after substitution, before the * final exit 0 */ err = st_gen_config_status_emit(out); if (err != NULL) { goto done; } if (st_sh_emit_str(out, "\nexit 0\n") < 0) { err = st_error_io("I/O error emitting configure"); goto done; } /* 11. final stream check */ if (ferror(out)) { err = st_error_io("I/O error emitting configure"); goto done; } done: for (i = 0; i < n_feat; i++) { free(feat_names[i]); } free(feat_names); /* the enable_ entries are owned (strbuild); the rest of * subst_names holds borrowed literals/registry pointers */ if (subst_names != NULL && n_reg + 7 <= n_subst) { for (i = n_reg + 7; i < n_subst; i++) { free(subst_names[i]); } } free(subst_names); st_gen_args_free(&args); 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; }