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
*/
#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_<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);
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;
}
+31 -7
View File
@@ -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=<dir>` stores $prefix (default /usr/local);
* - `--host=<triplet>` / `--build=<triplet>` 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:-<default>}"` where <default> 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_<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,
* 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)
* --------------------------------------------------------------