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
+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
#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 <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) -------------------------- */
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 },
};