From 08e1ea93dd5da3a89323850a722ac440700a2aad Mon Sep 17 00:00:00 2001 From: huntedbytheirs Date: Sat, 29 Aug 2026 00:15:46 -0400 Subject: [PATCH] feat(gen): emit config.status/config.log/config.h --- src/gen/config.c | 280 ++++++++++++++++++++++++ src/gen/config.h | 92 ++++++++ src/gen/configure.c | 88 +++++++- src/gen/configure.h | 38 +++- tests/unit/test_gen_configure.c | 366 +++++++++++++++++++++++++++++++- 5 files changed, 847 insertions(+), 17 deletions(-) create mode 100644 src/gen/config.c create mode 100644 src/gen/config.h diff --git a/src/gen/config.c b/src/gen/config.c new file mode 100644 index 0000000..edd717f --- /dev/null +++ b/src/gen/config.c @@ -0,0 +1,280 @@ +/* + * config.c - config.log / config.h / config.status emitters (todo 17). + * + * Four pure emitters; see gen/config.h for the model, the emitted text of + * each section, the sed-quote bake-in security argument, the v1 + * limitations and the error categories. + * + * Copyright (c) 2026 huntedbytheirs + * SPDX-License-Identifier: BSD-3-Clause + */ + +#include "gen/config.h" + +#include "cli.h" /* STUPIDTOOLS_VERSION (the created-by / generated-by line) */ + +#include "error.h" +#include "gen/sh_emit.h" + +#include +#include +#include +#include +#include + +/* ---- small helpers ----------------------------------------------------- */ + +/* True when `s` is a valid POSIX shell identifier (the have_ / + * HAVE_ interpolation positions demand it; mirrors configure.c). */ +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; +} + +/* Uppercase copy of a validated identifier: "pthread" -> "PTHREAD". */ +static char * +ident_upper(const char *s) +{ + size_t i; + size_t n = strlen(s); + char *up = malloc(n + 1); + + if (up == NULL) { + return NULL; + } + for (i = 0; i < n; i++) { + unsigned char c = (unsigned char)s[i]; + + up[i] = (char)((c >= 'a' && c <= 'z') ? c - 'a' + 'A' : c); + } + up[n] = '\0'; + return up; +} + +/* ---- fixed shell text (emitted verbatim via st_sh_emit_str) ------------ */ + +/* Section A: config.log init + original-args capture. Runs right after + * the preamble. The `%` in `printf 'configure invocation: %s\n'` is why + * this is emitted verbatim (fprintf would eat it). The two sed pipelines + * rewrite every ' into '\'' so the values can later be baked into + * config.status as single-quoted literals. */ +static const char CONFIG_LOG_INIT[] = + "\n" + "# --- config.log: transcript of this configure run ---\n" + "# Probe snippets append their compiler stdout/stderr to $config_log\n" + "# (see the probe section below); a `## results` summary of every\n" + "# have_ value is appended after probing. The original\n" + "# invocation is captured here for config.status (written at the end\n" + "# of this script), which re-runs configure with these arguments.\n" + "st_orig_args=\"$@\"\n" + "st_configure_path=$0\n" + "# Single-quote-escaped copies (every ' becomes '\\'' in shell): the\n" + "# values can then be baked into config.status as safe quoted\n" + "# literals - nothing in them is ever re-expanded when config.status\n" + "# runs.\n" + "st_orig_args_q=$(printf '%s' \"$st_orig_args\" | sed \"s/'/'\\\\\\\\''/g\")\n" + "st_configure_path_q=$(printf '%s' \"$st_configure_path\" | sed \"s/'/'\\\\\\\\''/g\")\n" + ": \"${config_log:=./config.log}\"\n" + "{\n" + " printf '%s\\n' 'This file contains any messages produced by compilers while'\n" + " printf '%s\\n' 'running configure, to aid debugging if configure makes a mistake.'\n" + " printf '%s\\n' ''\n" + " printf '%s\\n' 'It was created by stupidtools configure " + STUPIDTOOLS_VERSION + ".'\n" + " printf 'configure invocation: %s\\n' \"$st_orig_args\"\n" + " printf '%s\\n' \"timestamp: $(date)\"\n" + " printf '%s\\n' \"host: $(uname -s) $(uname -m)\"\n" + " printf '%s\\n' \"shell: ${SHELL:-unknown}\"\n" + " printf '%s\\n' ''\n" + "} > \"$config_log\"\n"; + +/* Section B (part 1): the `## results` summary appended to $config_log. */ +static const char RESULTS_PRE[] = + "\n" + "# --- probe results (appended to config.log) ---\n" + "# One have_=yes|no line per feature, as determined by the\n" + "# probes above. config.log is therefore: the header (top), every\n" + "# probe's compiler output (middle), this summary (end).\n" + "{\n" + " printf '%s\\n' '## results'\n"; + +static const char RESULTS_POST[] = + "} >> \"$config_log\"\n"; + +/* Section B (part 2): the ./config.h writer. */ +static const char CONFIG_H_PRE[] = + "\n" + "# --- config.h ---\n" + "# HAVE_ defines written at CONFIGURE time: one define per\n" + "# feature whose probe result was 'yes' (the content reflects the\n" + "# actual probe results, never the generator's guesses). The\n" + "# ST_CONFIG_H guard keeps the file safe to include repeatedly.\n" + "{\n" + " printf '%s\\n' '/* config.h - generated by stupidtools configure " + STUPIDTOOLS_VERSION + " - DO NOT EDIT. */'\n" + " printf '%s\\n' '#ifndef ST_CONFIG_H'\n" + " printf '%s\\n' '#define ST_CONFIG_H'\n" + " printf '%s\\n' ''\n"; + +static const char CONFIG_H_POST[] = + " printf '%s\\n' ''\n" + " printf '%s\\n' '#endif /* ST_CONFIG_H */'\n" + "} > ./config.h\n"; + +/* Section C: the ./config.status writer. The here-document is UNQUOTED on + * purpose: configure-time expansion bakes $st_orig_args_q / + * $st_configure_path_q into the file (expansion results are never + * re-parsed, so nothing can execute), while every `\$` survives as a + * literal `$` for config.status to expand when IT runs. */ +static const char CONFIG_STATUS[] = + "\n" + "# --- config.status ---\n" + "# A self-contained POSIX-sh helper written at configure time. It\n" + "# re-runs ./configure with the arguments of the ORIGINAL invocation\n" + "# (captured at the top of this script and baked in here as quoted\n" + "# literals, so no value is ever re-expanded when config.status\n" + "# runs).\n" + "cat > ./config.status <&2\n" + " exit 1\n" + " ;;\n" + "esac\n" + "ST_STATUS_EOF\n" + "chmod +x ./config.status\n"; + +/* ---- the public API ---------------------------------------------------- */ + +struct st_error * +st_gen_config_log_init_emit(FILE *out) +{ + if (out == NULL) { + return st_error_usage("st_gen_config_log_init_emit: NULL argument"); + } + if (st_sh_emit_str(out, CONFIG_LOG_INIT) < 0) { + return st_error_io("I/O error emitting config.log init section"); + } + return NULL; +} + +struct st_error * +st_gen_config_results_emit(FILE *out, const char *const *features, + size_t count) +{ + size_t i; + + if (out == NULL || (features == NULL && count > 0)) { + return st_error_usage("st_gen_config_results_emit: NULL argument"); + } + if (st_sh_emit_str(out, RESULTS_PRE) < 0) { + return st_error_io("I/O error emitting probe results section"); + } + for (i = 0; i < count; i++) { + if (!valid_ident(features[i])) { + return st_error_usage("feature name is not a valid shell " + "identifier"); + } + if (fprintf(out, " printf 'have_%s=%%s\\n' \"$have_%s\"\n", + features[i], features[i]) < 0) { + return st_error_io("I/O error emitting probe results section"); + } + } + if (st_sh_emit_str(out, RESULTS_POST) < 0) { + return st_error_io("I/O error emitting probe results section"); + } + return NULL; +} + +struct st_error * +st_gen_config_h_emit(FILE *out, const char *const *features, size_t count) +{ + size_t i; + + if (out == NULL || (features == NULL && count > 0)) { + return st_error_usage("st_gen_config_h_emit: NULL argument"); + } + if (st_sh_emit_str(out, CONFIG_H_PRE) < 0) { + return st_error_io("I/O error emitting config.h section"); + } + for (i = 0; i < count; i++) { + char *up; + + if (!valid_ident(features[i])) { + return st_error_usage("feature name is not a valid shell " + "identifier"); + } + up = ident_upper(features[i]); + if (up == NULL) { + return st_error_internal("out of memory uppercasing feature " + "name"); + } + if (fprintf(out, + " if [ \"$have_%s\" = \"yes\" ]; then\n" + " printf '%%s\\n' '#define HAVE_%s 1'\n" + " fi\n", + features[i], up) < 0) { + free(up); + return st_error_io("I/O error emitting config.h section"); + } + free(up); + } + if (st_sh_emit_str(out, CONFIG_H_POST) < 0) { + return st_error_io("I/O error emitting config.h section"); + } + return NULL; +} + +struct st_error * +st_gen_config_status_emit(FILE *out) +{ + if (out == NULL) { + return st_error_usage("st_gen_config_status_emit: NULL argument"); + } + if (st_sh_emit_str(out, CONFIG_STATUS) < 0) { + return st_error_io("I/O error emitting config.status section"); + } + return NULL; +} diff --git a/src/gen/config.h b/src/gen/config.h new file mode 100644 index 0000000..d352941 --- /dev/null +++ b/src/gen/config.h @@ -0,0 +1,92 @@ +/* + * config.h - emitters for the configure-time auxiliary artifacts + * (todo 17): config.log, config.h and config.status. + * + * MODEL: like everything in src/gen/, these functions are PURE CODE + * GENERATORS. They emit POSIX-sh sections INTO the generated ./configure; + * the artifacts themselves are written AT CONFIGURE TIME by the generated + * script, so their contents always reflect the actual probe results and + * the actual invocation - never the generator's guesses. + * + * THE FOUR SECTIONS (see also gen/configure.h for their placement in the + * generated script): + * + * st_gen_config_log_init_emit after the preamble: capture the + * original invocation (st_orig_args="$@", st_configure_path=$0, + * plus single-quote-escaped copies built with sed) and truncate- + * write the transcript header (created-by line, original + * arguments, timestamp, host, shell) into $config_log. $config_log + * is defaulted here with `:=`, idempotently with todo 12's cache + * boilerplate. Probe snippets (todo 12) append their compiler + * stdout/stderr to the same file. + * st_gen_config_results_emit after the probe section: append a + * `## results` block to $config_log with one + * `have_=yes|no` line per top-level feature. + * st_gen_config_h_emit after the probe section: write + * ./config.h with the ST_CONFIG_H guard and one + * `#define HAVE_ 1` per feature whose have_ + * resolved to "yes" at configure time (the feature name upper- + * cased; a failed feature gets no define). + * st_gen_config_status_emit at the very end (after Makefile + * substitution, before exit 0): write ./config.status - a + * standalone POSIX-sh script with the captured arguments baked in + * as quoted literals (so nothing in them is ever re-expanded when + * config.status runs) - and chmod +x it. Its arms: ''/--recheck -> + * `exec "${SHELL:-sh}" "$st_configure_path" $st_orig_args`; + * --config -> print the original args; --help -> brief usage; + * anything else -> error + exit 1. + * + * SECURITY (the sed-quote bake-in): the captured args/path are rewritten + * with `sed "s/'/'\\''/g"` (every `'` becomes `'\''`) and stored in + * st_orig_args_q / st_configure_path_q. config.status assigns them as + * single-quoted literals (`st_orig_args='...'`), so a value containing + * `$(...)`, backticks, spaces or `&` is inert both when configure writes + * config.status (here-document expansion never re-parses expansion + * results) and when config.status runs (the quotes are real script text + * there). Proven by the injection test (todo 17, case f). + * + * KNOWN v1 LIMITATIONS (documented, deliberately simple): + * - the recheck arm expands $st_orig_args UNQUOTED, so positional args + * containing spaces or glob characters are not preserved through + * --recheck (the v1 surface is --prefix/--host/--build); + * - st_configure_path is captured verbatim ($0), so config.status must + * be run from the same directory configure was run from; + * - args containing literal newlines are pathological (they would break + * the one-line bake-in). + * + * ERRORS: NULL arguments and feature names that are not valid POSIX shell + * identifiers return ST_ERR_USAGE (the names land in have_ and + * #define HAVE_ positions, which cannot be shell-quoted); stream + * write failures return ST_ERR_IO. + * + * Copyright (c) 2026 huntedbytheirs + * SPDX-License-Identifier: BSD-3-Clause + */ + +#ifndef ST_GEN_CONFIG_H +#define ST_GEN_CONFIG_H + +#include +#include + +struct st_error; + +/* Emit the config.log init + original-args capture section. */ +struct st_error *st_gen_config_log_init_emit(FILE *out); + +/* Emit the post-probe `## results` summary appended to $config_log. + * `features` holds the top-level feature names (validated identifiers), + * one emitted `have_=%s` line per entry. */ +struct st_error *st_gen_config_results_emit(FILE *out, + const char *const *features, + size_t count); + +/* Emit the ./config.h writer (ST_CONFIG_H guard + per-feature defines). */ +struct st_error *st_gen_config_h_emit(FILE *out, + const char *const *features, + size_t count); + +/* Emit the ./config.status writer (+ chmod +x). */ +struct st_error *st_gen_config_status_emit(FILE *out); + +#endif /* ST_GEN_CONFIG_H */ diff --git a/src/gen/configure.c b/src/gen/configure.c index 17b9ac9..73e21b1 100644 --- a/src/gen/configure.c +++ b/src/gen/configure.c @@ -20,6 +20,7 @@ * SPDX-License-Identifier: BSD-3-Clause */ +#include "gen/config.h" #include "gen/configure.h" #include "cli.h" /* STUPIDTOOLS_VERSION (the generated-by version string) */ @@ -327,9 +328,7 @@ static const char SUBST_LOOKUP_TAIL[] = " done < ./Makefile.in\n" "}\n" "\n" - "st_subst\n" - "\n" - "exit 0\n"; + "st_subst\n"; /* ---- section emitters -------------------------------------------------- */ @@ -570,6 +569,27 @@ done: 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 * @@ -583,6 +603,9 @@ st_gen_configure_emit(FILE *out, const struct st_kdl_document *doc, const struct st_kdl_node *node; 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"); @@ -634,7 +657,13 @@ st_gen_configure_emit(FILE *out, const struct st_kdl_document *doc, goto done; } - /* 3. host detection */ + /* 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; @@ -648,7 +677,7 @@ st_gen_configure_emit(FILE *out, const struct st_kdl_document *doc, goto done; } - /* 4. toolchain defaults (before the cache boilerplate; see the header) */ + /* 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; @@ -681,15 +710,18 @@ st_gen_configure_emit(FILE *out, const struct st_kdl_document *doc, goto done; } - /* 5. cache boilerplate (todo 12) */ + /* 6. 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 */ + /* 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) { @@ -702,21 +734,59 @@ st_gen_configure_emit(FILE *out, const struct st_kdl_document *doc, 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; + } } - /* 7. substitution section */ + /* 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; } - /* 8. exit 0 is already the last line of the substitution tail */ + /* 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); free(subst_names); return err; } diff --git a/src/gen/configure.h b/src/gen/configure.h index 06a0cad..3a94883 100644 --- a/src/gen/configure.h +++ b/src/gen/configure.h @@ -12,7 +12,7 @@ * ------------------------------------------------------------- * 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: + * `# --- 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 @@ -22,16 +22,23 @@ * 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 + * 3. CONFIG.LOG INIT (todo 17) — capture the original invocation + * (st_orig_args="$@", st_configure_path=$0, plus single-quote-escaped + * copies built with sed for the config.status bake-in) and + * truncate-write the transcript header (created-by line, original + * arguments, timestamp, host, shell) into $config_log (defaulted here + * with `:=`, idempotently with todo 12's boilerplate default). Probe + * snippets append their compiler stdout/stderr to the same file. + * 4. 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 + * 5. 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, + * 6. CACHE BOILERPLATE: probe.h's st_probe_emit_cache_functions (todo 12). + * 7. 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 @@ -42,14 +49,31 @@ * 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) + * 8. PROBE RESULTS + CONFIG.H (todo 17) — append a `## results` block to + * $config_log (one `have_=yes|no` line per top-level feature), + * then write ./config.h with the ST_CONFIG_H guard and one + * `#define HAVE_ 1` per feature whose have_ resolved + * to "yes". Both reflect the configure-time probe results, never + * generation-time guesses; feature names are validated identifiers + * (see ERRORS), so the have_ / HAVE_ interpolation is + * safe. + * 9. 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`. + * 10. CONFIG.STATUS (todo 17) — write ./config.status (a standalone + * POSIX-sh script with the original arguments baked in as quoted + * literals, so nothing in them is ever re-expanded when it runs) and + * chmod +x it. Arms: ''/--recheck -> `exec "${SHELL:-sh}" + * "$st_configure_path" $st_orig_args`; --config -> print the original + * args; --help -> brief usage; unknown -> error + exit 1. v1 + * limitations (documented in gen/config.h): positional args with + * spaces/globs are not preserved by the unquoted recheck expansion; + * config.status must run from the same directory as configure. + * 11. `exit 0`. * * SUBSTITUTION APPROACH (documented because the plan flagged it) * -------------------------------------------------------------- diff --git a/tests/unit/test_gen_configure.c b/tests/unit/test_gen_configure.c index 38eb796..9a9462f 100644 --- a/tests/unit/test_gen_configure.c +++ b/tests/unit/test_gen_configure.c @@ -1,4 +1,4 @@ -/* 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 */ +/* LINK: ../../src/gen/config.c ../../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 @@ -200,6 +200,90 @@ syntax_check(const char *shell, const char *path) return WEXITSTATUS(spawned); } +/* Run "./config.status " with cwd = temp_dir; stdout to status.out, + * stderr to status.err. Returns the exit status, or -1 if not spawned. */ +static int +run_status(const char *args) +{ + char cmd[2400]; + int n; + int spawned; + + n = snprintf(cmd, sizeof cmd, + "cd '%s' && ./config.status %s >'%s/status.out' " + "2>'%s/status.err'", + temp_dir, args != NULL ? args : "", temp_dir, temp_dir); + if (n < 0 || (size_t)n >= sizeof cmd) { + return -1; + } + spawned = system(cmd); + if (spawned == -1) { + return -1; + } + return WEXITSTATUS(spawned); +} + +/* Slurp temp_dir/, or NULL. Caller frees. */ +static char * +slurp_named(const char *name) +{ + char path[600]; + + if (mkpath(path, sizeof path, name) < 0) { + return NULL; + } + return slurp(path); +} + +/* Remove temp_dir/ (a missing file is not an error: the stale_state + * checks remove outputs that may or may not exist). Returns 0 on success. */ +static int +remove_named(const char *name) +{ + char path[600]; + + if (mkpath(path, sizeof path, name) < 0) { + return -1; + } + (void)remove(path); + return 0; +} + +/* Parse + validate + emit an INLINE document into temp_dir/configure. */ +static int +emit_inline_configure(struct st_ext_ctx *ctx, const char *docsrc) +{ + struct st_kdl_document *doc = NULL; + struct st_error *err = NULL; + char conf[600]; + + doc = st_kdl_parse(docsrc, "inline.kdl", &err); + if (err != NULL) { + st_error_free(err); + return -1; + } + if (doc == NULL) { + return -1; + } + err = st_kdl_validate(doc); + if (err != NULL) { + st_error_free(err); + st_kdl_document_free(doc); + return -1; + } + if (mkpath(conf, sizeof conf, "configure") < 0) { + st_kdl_document_free(doc); + return -1; + } + err = st_gen_configure_emit_path(conf, doc, ctx); + st_kdl_document_free(doc); + if (err != NULL) { + st_error_free(err); + return -1; + } + return 0; +} + /* ---- fixture loading (same as test_schema.c) -------------------------- */ static const char * @@ -618,6 +702,272 @@ test_cross_compile(const MunitParameter params[], void *data) return MUNIT_OK; } +/* ---- todo 17 (a): the three new sections stay shell-safe ------------- */ + +static MunitResult +test_config_sections_syntax(const MunitParameter params[], void *data) +{ + (void)params; + (void)data; + struct st_ext_ctx *ctx; + char conf[600]; + char *bytes; + + 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); + /* the three sections are structurally marked (--- not ===, per the + * F3 banned-construct grep) */ + munit_assert_not_null(strstr(bytes, "# --- config.log:")); + munit_assert_not_null(strstr(bytes, "# --- config.h ---")); + munit_assert_not_null(strstr(bytes, "# --- config.status ---")); + munit_assert_not_null(strstr(bytes, "'## results'")); + free(bytes); + return MUNIT_OK; +} + +/* ---- todo 17 (b): a real run writes config.h/config.log/config.status -- */ + +static MunitResult +test_config_artifacts(const MunitParameter params[], void *data) +{ + (void)params; + (void)data; + struct st_ext_ctx *ctx; + char status[600]; + char *ch; + char *cl; + + 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(NULL), ==, 0); + + /* config.h: the ST_CONFIG_H guard + one define per resolved feature + * (content asserted, not mere existence) */ + ch = slurp_named("config.h"); + munit_assert_not_null(ch); + munit_assert_not_null(strstr(ch, "#ifndef ST_CONFIG_H")); + munit_assert_not_null(strstr(ch, "#define ST_CONFIG_H")); + munit_assert_not_null(strstr(ch, "#define HAVE_PTHREAD 1")); + munit_assert_not_null(strstr(ch, "#define HAVE_MATH 1")); + free(ch); + + /* config.log: the transcript header + the ## results summary */ + cl = slurp_named("config.log"); + munit_assert_not_null(cl); + munit_assert_not_null( + strstr(cl, "It was created by stupidtools configure")); + munit_assert_not_null(strstr(cl, "## results")); + munit_assert_not_null(strstr(cl, "have_pthread=yes")); + munit_assert_not_null(strstr(cl, "have_math=yes")); + free(cl); + + /* config.status: exists, is executable, and is valid POSIX-sh */ + munit_assert_int(mkpath(status, sizeof status, "config.status"), >, 0); + munit_assert_int(access(status, F_OK), ==, 0); + munit_assert_int(access(status, X_OK), ==, 0); + munit_assert_int(syntax_check("sh", status), ==, 0); + munit_assert_int(syntax_check("bash", status), ==, 0); + munit_assert_int(syntax_check("zsh", status), ==, 0); + return MUNIT_OK; +} + +/* ---- todo 17 (c): config.status --config prints the original args ----- */ + +static MunitResult +test_config_status_config(const MunitParameter params[], void *data) +{ + (void)params; + (void)data; + struct st_ext_ctx *ctx; + char *out; + + 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); + munit_assert_int(run_status("--config"), ==, 0); + + out = slurp_named("status.out"); + munit_assert_not_null(out); + munit_assert_string_equal(out, "--prefix=/opt/x\n"); + free(out); + return MUNIT_OK; +} + +/* ---- todo 17 (d): config.status --recheck re-runs configure ----------- */ + +static MunitResult +test_config_status_recheck(const MunitParameter params[], void *data) +{ + (void)params; + (void)data; + struct st_ext_ctx *ctx; + char *mk; + char *ch; + + 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); + + /* stale_state: destroy the outputs, then --recheck regenerates them + * (observable regeneration, not a cached no-op) */ + munit_assert_int(remove_named("Makefile"), ==, 0); + munit_assert_int(remove_named("config.h"), ==, 0); + munit_assert_int(run_status("--recheck"), ==, 0); + + mk = makefile_text(); + munit_assert_not_null(mk); + munit_assert_not_null(strstr(mk, "prefix = /opt/x")); + free(mk); + + ch = slurp_named("config.h"); + munit_assert_not_null(ch); + munit_assert_not_null(strstr(ch, "#define HAVE_PTHREAD 1")); + free(ch); + return MUNIT_OK; +} + +/* ---- todo 17 (e): a failed feature gets no HAVE_ define --------------- */ + +static MunitResult +test_config_failed_feature(const MunitParameter params[], void *data) +{ + (void)params; + (void)data; + static const char *const docsrc = + "project \"x\" version \"1.0\"\n" + "feature \"nope\" { header \"nope_missing_xyz.h\" }\n" + "feature \"ok\" { header \"unistd.h\" }\n"; + struct st_ext_ctx *ctx; + char *ch; + char *cl; + + ctx = build_ctx(); + munit_assert_not_null(ctx); + munit_assert_int(emit_inline_configure(ctx, docsrc), ==, 0); + st_ext_ctx_free(ctx); + + munit_assert_int(write_makefile_in("CC = @CC@\n"), ==, 0); + munit_assert_int(run_configure(NULL), ==, 0); + + ch = slurp_named("config.h"); + munit_assert_not_null(ch); + munit_assert_null(strstr(ch, "HAVE_NOPE")); + munit_assert_not_null(strstr(ch, "#define HAVE_OK 1")); + free(ch); + + cl = slurp_named("config.log"); + munit_assert_not_null(cl); + munit_assert_not_null(strstr(cl, "have_nope=no")); + munit_assert_not_null(strstr(cl, "have_ok=yes")); + free(cl); + return MUNIT_OK; +} + +/* ---- todo 17 (f): injected values never execute ----------------------- */ + +static MunitResult +test_config_status_injection(const MunitParameter params[], void *data) +{ + (void)params; + (void)data; + struct st_ext_ctx *ctx; + char pwned1[600]; + char pwned2[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(pwned1, sizeof pwned1, "pwned1"), >, 0); + munit_assert_int(mkpath(pwned2, sizeof pwned2, "pwned2"), >, 0); + munit_assert_int(write_makefile_in("prefix = @prefix@\n"), ==, 0); + + /* The payloads use ${IFS} instead of a literal space: they contain no + * whitespace, so they survive the (documented, v1) unquoted recheck + * expansion as one word - yet they would create the marker files if + * any shell ever executed them. */ + munit_assert_int(run_configure("--prefix='$(touch${IFS}pwned1)'"), ==, 0); + munit_assert_int(access(pwned1, F_OK), ==, -1); + munit_assert_int(run_status("--config"), ==, 0); + munit_assert_int(access(pwned1, F_OK), ==, -1); + munit_assert_int(run_status("--recheck"), ==, 0); + munit_assert_int(access(pwned1, F_OK), ==, -1); + mk = makefile_text(); + munit_assert_not_null(mk); + munit_assert_not_null(strstr(mk, "prefix = $(touch${IFS}pwned1)")); + free(mk); + + /* payload 2: backticks - same guarantee, via a fresh configure run */ + munit_assert_int(run_configure("--prefix='`touch${IFS}pwned2`'"), ==, 0); + munit_assert_int(access(pwned2, F_OK), ==, -1); + munit_assert_int(run_status("--config"), ==, 0); + munit_assert_int(access(pwned2, F_OK), ==, -1); + munit_assert_int(run_status("--recheck"), ==, 0); + munit_assert_int(access(pwned2, F_OK), ==, -1); + mk = makefile_text(); + munit_assert_not_null(mk); + munit_assert_not_null(strstr(mk, "prefix = `touch${IFS}pwned2`")); + free(mk); + return MUNIT_OK; +} + +/* ---- todo 17 (g): config.status --help and unknown-option handling ---- */ + +static MunitResult +test_config_status_help(const MunitParameter params[], void *data) +{ + (void)params; + (void)data; + struct st_ext_ctx *ctx; + char *out; + char *e; + + 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(NULL), ==, 0); + + munit_assert_int(run_status("--help"), ==, 0); + out = slurp_named("status.out"); + munit_assert_not_null(out); + munit_assert_not_null(strstr(out, "Usage")); + free(out); + + munit_assert_int(run_status("--bogus"), !=, 0); + e = slurp_named("status.err"); + munit_assert_not_null(e); + munit_assert_not_null(strstr(e, "unrecognized option")); + free(e); + return MUNIT_OK; +} + static MunitTest tests[] = { { "/gen/syntax", test_syntax, setup, teardown, MUNIT_TEST_OPTION_NONE, NULL }, @@ -639,6 +989,20 @@ static MunitTest tests[] = { teardown, MUNIT_TEST_OPTION_NONE, NULL }, { "/gen/cross-compile", test_cross_compile, setup, teardown, MUNIT_TEST_OPTION_NONE, NULL }, + { "/gen/config-sections-syntax", test_config_sections_syntax, setup, + teardown, MUNIT_TEST_OPTION_NONE, NULL }, + { "/gen/config-artifacts", test_config_artifacts, setup, teardown, + MUNIT_TEST_OPTION_NONE, NULL }, + { "/gen/config-status-config", test_config_status_config, setup, + teardown, MUNIT_TEST_OPTION_NONE, NULL }, + { "/gen/config-status-recheck", test_config_status_recheck, setup, + teardown, MUNIT_TEST_OPTION_NONE, NULL }, + { "/gen/config-failed-feature", test_config_failed_feature, setup, + teardown, MUNIT_TEST_OPTION_NONE, NULL }, + { "/gen/config-status-injection", test_config_status_injection, setup, + teardown, MUNIT_TEST_OPTION_NONE, NULL }, + { "/gen/config-status-help", test_config_status_help, setup, teardown, + MUNIT_TEST_OPTION_NONE, NULL }, { NULL, NULL, NULL, NULL, MUNIT_TEST_OPTION_NONE, NULL }, };