feat(gen): emit config.status/config.log/config.h

This commit is contained in:
2026-08-29 00:15:46 -04:00
parent 03497e2c38
commit 08e1ea93dd
5 changed files with 847 additions and 17 deletions
+280
View File
@@ -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 <stdbool.h>
#include <stddef.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
/* ---- small helpers ----------------------------------------------------- */
/* True when `s` is a valid POSIX shell identifier (the have_<name> /
* HAVE_<NAME> 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_<feature> 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_<feature>=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_<FEATURE> 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 <<ST_STATUS_EOF\n"
"#!/bin/sh\n"
"# config.status - re-runs ./configure with the original arguments.\n"
"# Generated by stupidtools configure "
STUPIDTOOLS_VERSION
" - DO NOT EDIT.\n"
"\n"
"st_orig_args='$st_orig_args_q'\n"
"st_configure_path='$st_configure_path_q'\n"
"\n"
"case \"\\${1-}\" in\n"
" --recheck|'')\n"
" exec \"\\${SHELL:-sh}\" \"\\$st_configure_path\" \\$st_orig_args\n"
" ;;\n"
" --config)\n"
" printf '%s\\n' \"\\$st_orig_args\"\n"
" ;;\n"
" --help)\n"
" printf '%s\\n' 'Usage: ./config.status [OPTION]'\n"
" printf '%s\\n' 'Re-run the original ./configure invocation.'\n"
" printf '%s\\n' 'Options:'\n"
" printf '%s\\n' ' --recheck re-run ./configure with the original arguments'\n"
" printf '%s\\n' ' --config print the original configure arguments'\n"
" printf '%s\\n' ' --help print this help'\n"
" ;;\n"
" *)\n"
" printf 'config.status: error: unrecognized option %s\\n' \"\\$1\" >&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;
}
+92
View File
@@ -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_<feature>=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_<FEATURE> 1` per feature whose have_<feature>
* 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_<name> and
* #define HAVE_<NAME> 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 <stddef.h>
#include <stdio.h>
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_<name>=%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 */
+79 -9
View File
@@ -20,6 +20,7 @@
* SPDX-License-Identifier: BSD-3-Clause * SPDX-License-Identifier: BSD-3-Clause
*/ */
#include "gen/config.h"
#include "gen/configure.h" #include "gen/configure.h"
#include "cli.h" /* STUPIDTOOLS_VERSION (the generated-by version string) */ #include "cli.h" /* STUPIDTOOLS_VERSION (the generated-by version string) */
@@ -327,9 +328,7 @@ static const char SUBST_LOOKUP_TAIL[] =
" done < ./Makefile.in\n" " done < ./Makefile.in\n"
"}\n" "}\n"
"\n" "\n"
"st_subst\n" "st_subst\n";
"\n"
"exit 0\n";
/* ---- section emitters -------------------------------------------------- */ /* ---- section emitters -------------------------------------------------- */
@@ -570,6 +569,27 @@ done:
return err; 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 ---------------------------------------------------- */ /* ---- the public API ---------------------------------------------------- */
struct st_error * 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; const struct st_kdl_node *node;
char **subst_names = NULL; char **subst_names = NULL;
size_t n_subst = 0; 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) { if (out == NULL || doc == NULL || ctx == NULL) {
return st_error_usage("st_gen_configure_emit: NULL argument"); 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; 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) { if (st_sh_emit_str(out, HOST_DETECT_PRE) < 0) {
err = st_error_io("I/O error emitting host detection"); err = st_error_io("I/O error emitting host detection");
goto done; goto done;
@@ -648,7 +677,7 @@ st_gen_configure_emit(FILE *out, const struct st_kdl_document *doc,
goto done; 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) { if (st_sh_emit_str(out, TOOLCHAIN_DEFAULTS_PRE) < 0) {
err = st_error_io("I/O error emitting toolchain defaults"); err = st_error_io("I/O error emitting toolchain defaults");
goto done; goto done;
@@ -681,15 +710,18 @@ st_gen_configure_emit(FILE *out, const struct st_kdl_document *doc,
goto done; goto done;
} }
/* 5. cache boilerplate (todo 12) */ /* 6. cache boilerplate (todo 12) */
err = st_probe_emit_cache_functions(out); err = st_probe_emit_cache_functions(out);
if (err != NULL) { if (err != NULL) {
goto done; 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) { for (node = doc->nodes; node != NULL; node = node->next) {
bool is_feature = false; bool is_feature = false;
char *fname = NULL;
err = name_equals(&node->name, "feature", &is_feature); err = name_equals(&node->name, "feature", &is_feature);
if (err != NULL) { if (err != NULL) {
@@ -702,21 +734,59 @@ st_gen_configure_emit(FILE *out, const struct st_kdl_document *doc,
if (err != NULL) { if (err != NULL) {
goto done; goto done;
} }
/* a successful emit_feature guarantees the name is a valid shell
* identifier (it errors otherwise), so the have_<name> /
* HAVE_<NAME> 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); err = emit_subst_lookup(out, (const char *const *)subst_names, n_subst);
if (err != NULL) { if (err != NULL) {
goto done; 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)) { if (ferror(out)) {
err = st_error_io("I/O error emitting configure"); err = st_error_io("I/O error emitting configure");
goto done; goto done;
} }
done: done:
for (i = 0; i < n_feat; i++) {
free(feat_names[i]);
}
free(feat_names);
free(subst_names); free(subst_names);
return err; return err;
} }
+31 -7
View File
@@ -12,7 +12,7 @@
* ------------------------------------------------------------- * -------------------------------------------------------------
* 1. `#!/bin/sh` + a header comment (generated-by, version). * 1. `#!/bin/sh` + a header comment (generated-by, version).
* 2. PREAMBLE — v1 MINIMAL argument handling, structurally marked with a * 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=<dir>` stores $prefix (default /usr/local); * - `--prefix=<dir>` stores $prefix (default /usr/local);
* - `--host=<triplet>` / `--build=<triplet>` store $host / $build; * - `--host=<triplet>` / `--build=<triplet>` store $host / $build;
* $cross_compiling=yes when --host differs from --build (a simple * $cross_compiling=yes when --host differs from --build (a simple
@@ -22,16 +22,23 @@
* the positional form is accepted-but-ignored until todo 18; * the positional form is accepted-but-ignored until todo 18;
* - an unknown `--*` prints "unrecognized option" to stderr and * - an unknown `--*` prints "unrecognized option" to stderr and
* exits 1. * 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 * resolve.h's st_resolve_emit_os_norm() rewrites it in place; `st_cc_id
* =unknown` (v1, no compiler-id sniffing at configure time). * =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:-<default>}"` where <default> is the registry VALUE at * `VAR="${VAR:-<default>}"` where <default> is the registry VALUE at
* generation time (st_sh_quote'd; typically empty for CFLAGS), then * generation time (st_sh_quote'd; typically empty for CFLAGS), then
* `LIBS=""`. Emitted BEFORE the probe cache boilerplate because that * `LIBS=""`. Emitted BEFORE the probe cache boilerplate because that
* boilerplate uses `:=` fallbacks that must not clobber these values. * boilerplate uses `:=` fallbacks that must not clobber these values.
* 5. CACHE BOILERPLATE: probe.h's st_probe_emit_cache_functions (todo 12). * 6. CACHE BOILERPLATE: probe.h's st_probe_emit_cache_functions (todo 12).
* 6. PROBE SECTION: for each top-level `feature` node in document order, * 7. PROBE SECTION: for each top-level `feature` node in document order,
* for each check child, derive the checkname via resolve.h's * for each check child, derive the checkname via resolve.h's
* st_resolve_check_name, build the probe via checks.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_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 * v1 pkg-config guard `if command -v pkg-config >/dev/null 2>&1; then
* ... LIBS="$LIBS $(pkg-config --libs 'X')" ...; fi`. Generic over the * ... LIBS="$LIBS $(pkg-config --libs 'X')" ...; fi`. Generic over the
* check kinds, never hardcoded per feature name. * 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_<feature>=yes|no` line per top-level feature),
* then write ./config.h with the ST_CONFIG_H guard and one
* `#define HAVE_<FEATURE> 1` per feature whose have_<feature> resolved
* to "yes". Both reflect the configure-time probe results, never
* generation-time guesses; feature names are validated identifiers
* (see ERRORS), so the have_<name> / HAVE_<NAME> interpolation is
* safe.
* 9. SUBSTITUTION: reads ./Makefile.in (cwd, the v1 source-dir contract)
* and replaces every `@VAR@` (VAR = registered vars ∪ {LIBS, prefix, * and replaces every `@VAR@` (VAR = registered vars ∪ {LIBS, prefix,
* srcdir}) with the shell variable's value, writing ./Makefile. An * srcdir}) with the shell variable's value, writing ./Makefile. An
* UNRESOLVED `@SOMETHING@` is a CONFIGURE-TIME error naming the * UNRESOLVED `@SOMETHING@` is a CONFIGURE-TIME error naming the
* variable (the generator cannot know the user's Makefile.in), reported * variable (the generator cannot know the user's Makefile.in), reported
* after substitution via the per-line lookup; a missing Makefile.in is * after substitution via the per-line lookup; a missing Makefile.in is
* a clean error. See the SUBSTITUTION approach note below. * 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) * SUBSTITUTION APPROACH (documented because the plan flagged it)
* -------------------------------------------------------------- * --------------------------------------------------------------
+365 -1
View File
@@ -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 #ifndef _POSIX_C_SOURCE
#define _POSIX_C_SOURCE 200809L /* mkdtemp/unsetenv/setenv (POSIX.1-2008) */ #define _POSIX_C_SOURCE 200809L /* mkdtemp/unsetenv/setenv (POSIX.1-2008) */
#endif #endif
@@ -200,6 +200,90 @@ syntax_check(const char *shell, const char *path)
return WEXITSTATUS(spawned); return WEXITSTATUS(spawned);
} }
/* Run "./config.status <args>" 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/<name>, 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/<name> (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) -------------------------- */ /* ---- fixture loading (same as test_schema.c) -------------------------- */
static const char * static const char *
@@ -618,6 +702,272 @@ test_cross_compile(const MunitParameter params[], void *data)
return MUNIT_OK; 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[] = { static MunitTest tests[] = {
{ "/gen/syntax", test_syntax, setup, teardown, MUNIT_TEST_OPTION_NONE, { "/gen/syntax", test_syntax, setup, teardown, MUNIT_TEST_OPTION_NONE,
NULL }, NULL },
@@ -639,6 +989,20 @@ static MunitTest tests[] = {
teardown, MUNIT_TEST_OPTION_NONE, NULL }, teardown, MUNIT_TEST_OPTION_NONE, NULL },
{ "/gen/cross-compile", test_cross_compile, setup, teardown, { "/gen/cross-compile", test_cross_compile, setup, teardown,
MUNIT_TEST_OPTION_NONE, NULL }, 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 }, { NULL, NULL, NULL, NULL, MUNIT_TEST_OPTION_NONE, NULL },
}; };