feat(gen): assemble configure script with substitution

This commit is contained in:
2026-08-28 23:50:24 -04:00
parent 2fff47460e
commit 03497e2c38
3 changed files with 1512 additions and 0 deletions
+653
View File
@@ -0,0 +1,653 @@
/* 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 */
#ifndef _POSIX_C_SOURCE
#define _POSIX_C_SOURCE 200809L /* mkdtemp/unsetenv/setenv (POSIX.1-2008) */
#endif
/* tests/unit/test_gen_configure.c
*
* Unit + integration tests for the ./configure generator (todo 16):
* src/gen/configure.c. THE MODEL: the generator EMITS a POSIX-sh script
* from a schema-validated DSL document + an extension ctx; the emitted
* script RUNS at configure time (probes + @VAR@ substitution). These tests
* prove the emitted script is syntactically valid (sh/bash/zsh -n, no
* banned constructs), REALLY runs (exit 0, Makefile written, CC/LIBS/
* prefix correct), honors --prefix, errors cleanly on an unknown @VAR@, a
* missing Makefile.in and an unrecognized option, carries values with
* '/', spaces and '&' byte-exact, keeps $(...) / backticks inert, and
* cross-compile-skips RUN probes.
*
* The magic LINK comment on line 1 is REQUIRED by tests/run.sh (extra .c
* sources, relative to tests/unit/). configure.c pulls in the whole detect
* chain (probe/checks/check_registry/resolve) + sh_emit + abi (the C/C++
* modules register CC/CFLAGS) + kdl (parse/lexer/value/schema) + error/span.
*/
#include "munit.h"
#include "detect/check_registry.h"
#include "error.h"
#include "ext/abi.h"
#include "gen/configure.h"
#include "kdl/ast.h"
#include "kdl/schema.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include <sys/wait.h>
#include <unistd.h>
/* ---- per-test temp dir ------------------------------------------------ */
static char temp_dir[128];
static void *
setup(const MunitParameter params[], void *user_data)
{
(void)params;
(void)user_data;
if (snprintf(temp_dir, sizeof temp_dir, "/tmp/st_gen_XXXXXX") < 0) {
return NULL;
}
if (mkdtemp(temp_dir) == NULL) {
return NULL;
}
return (void *)1;
}
static void
teardown(void *fixture)
{
char cmd[160];
if (fixture == NULL || temp_dir[0] == '\0') {
return;
}
if (snprintf(cmd, sizeof cmd, "rm -rf -- '%s'", temp_dir) < 0) {
return;
}
(void)system(cmd); /* best-effort; mkdtemp names are [A-Za-z0-9_]* */
temp_dir[0] = '\0';
}
/* ---- small file/process helpers --------------------------------------- */
/* Join temp_dir/`name` into `buf`; returns the snprintf result. */
static int
mkpath(char *buf, size_t sz, const char *name)
{
return snprintf(buf, sz, "%s/%s", temp_dir, name);
}
/* Slurp a file into a NUL-terminated heap buffer, or NULL. Caller frees. */
static char *
slurp(const char *path)
{
FILE *f;
long n;
char *buf;
f = fopen(path, "rb");
if (f == NULL) {
return NULL;
}
if (fseek(f, 0, SEEK_END) != 0) {
fclose(f);
return NULL;
}
n = ftell(f);
if (n < 0 || fseek(f, 0, SEEK_SET) != 0) {
fclose(f);
return NULL;
}
buf = malloc((size_t)n + 1);
if (buf == NULL) {
fclose(f);
return NULL;
}
if (fread(buf, 1, (size_t)n, f) != (size_t)n) {
free(buf);
fclose(f);
return NULL;
}
fclose(f);
buf[n] = '\0';
return buf;
}
/* Slurp temp_dir/Makefile, or NULL. Caller frees. */
static char *
makefile_text(void)
{
char path[600];
if (mkpath(path, sizeof path, "Makefile") < 0) {
return NULL;
}
return slurp(path);
}
/* Slurp temp_dir/run.err, or NULL. Caller frees. */
static char *
stderr_text(void)
{
char path[600];
if (mkpath(path, sizeof path, "run.err") < 0) {
return NULL;
}
return slurp(path);
}
/* Write `content` to temp_dir/Makefile.in. Returns 0 on success. */
static int
write_makefile_in(const char *content)
{
char path[600];
FILE *f;
if (mkpath(path, sizeof path, "Makefile.in") < 0) {
return -1;
}
f = fopen(path, "w");
if (f == NULL) {
return -1;
}
if (fputs(content, f) == EOF) {
fclose(f);
return -1;
}
fclose(f);
return 0;
}
/* Run "sh ./configure <args>" with cwd = temp_dir, stderr to run.err.
* Returns the configure exit status, or -1 if it could not be spawned. */
static int
run_configure(const char *args)
{
char cmd[2400];
int n;
int spawned;
n = snprintf(cmd, sizeof cmd, "cd '%s' && sh ./configure %s 2>'%s/run.err'",
temp_dir, args != NULL ? args : "", temp_dir);
if (n < 0 || (size_t)n >= sizeof cmd) {
return -1;
}
spawned = system(cmd);
if (spawned == -1) {
return -1;
}
return WEXITSTATUS(spawned);
}
/* "<shell> -n <path>": the shell's exit status or -1. */
static int
syntax_check(const char *shell, const char *path)
{
char cmd[700];
int n;
int spawned;
n = snprintf(cmd, sizeof cmd, "%s -n '%s'", shell, path);
if (n < 0 || (size_t)n >= sizeof cmd) {
return -1;
}
spawned = system(cmd);
if (spawned == -1) {
return -1;
}
return WEXITSTATUS(spawned);
}
/* ---- fixture loading (same as test_schema.c) -------------------------- */
static const char *
fixture_path(void)
{
static const char *const candidates[] = {
"tests/fixtures/stupid.kdl",
"../fixtures/stupid.kdl",
};
size_t i;
for (i = 0; i < sizeof(candidates) / sizeof(candidates[0]); i++) {
FILE *f = fopen(candidates[i], "rb");
if (f != NULL) {
fclose(f);
return candidates[i];
}
}
return NULL;
}
/* Build a ctx with the builtins + C detected (env CC/CFLAGS/etc. reset).
* Returns NULL on any failure. */
static struct st_ext_ctx *
build_ctx(void)
{
struct st_ext_ctx *ctx = st_ext_ctx_new();
struct st_error *err;
if (ctx == NULL) {
return NULL;
}
unsetenv("CC");
unsetenv("CFLAGS");
unsetenv("CXX");
unsetenv("CXXFLAGS");
err = st_ext_init_builtins(ctx);
if (err != NULL) {
st_error_free(err);
st_ext_ctx_free(ctx);
return NULL;
}
err = st_ext_detect_language(ctx, "c");
if (err != NULL) {
st_error_free(err);
st_ext_ctx_free(ctx);
return NULL;
}
return ctx;
}
/* Parse + validate + emit the fixture into temp_dir/configure, using
* `ctx`. Returns 0 on success, -1 on failure. */
static int
emit_fixture_configure(struct st_ext_ctx *ctx)
{
const char *path = fixture_path();
char conf[600];
char *src;
struct st_kdl_document *doc = NULL;
struct st_error *err = NULL;
if (path == NULL || ctx == NULL) {
return -1;
}
src = slurp(path);
if (src == NULL) {
return -1;
}
doc = st_kdl_parse(src, path, &err);
if (err != NULL) {
st_error_free(err);
free(src);
return -1;
}
if (doc == NULL) {
free(src);
return -1;
}
err = st_kdl_validate(doc);
if (err != NULL) {
st_error_free(err);
st_kdl_document_free(doc);
free(src);
return -1;
}
if (mkpath(conf, sizeof conf, "configure") < 0) {
st_kdl_document_free(doc);
free(src);
return -1;
}
err = st_gen_configure_emit_path(conf, doc, ctx);
/* the document borrows `src`'s token slices, so free src only after
* the document is released */
st_kdl_document_free(doc);
free(src);
if (err != NULL) {
st_error_free(err);
return -1;
}
return 0;
}
/* The standard Makefile.in every end-to-end case uses. */
#define STD_MAKEFILE_IN \
"CC = @CC@\n" \
"LIBS = @LIBS@\n" \
"prefix = @prefix@\n" \
"FLAGS = @CFLAGS@\n"
/* ---- (a) the emitted configure passes the three-shell syntax oracle --- */
static MunitResult
test_syntax(const MunitParameter params[], void *data)
{
(void)params;
(void)data;
static const char *const banned[] = {
"[[ ", "]]", "local ", "==", "<<<", "&>", "set -e",
};
struct st_ext_ctx *ctx;
char conf[600];
char *bytes;
size_t i;
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);
for (i = 0; i < sizeof(banned) / sizeof(banned[0]); i++) {
munit_assert_null(strstr(bytes, banned[i]));
}
/* the preamble is structurally marked for todo 18 */
munit_assert_not_null(strstr(bytes, "# --- PREAMBLE ---"));
free(bytes);
return MUNIT_OK;
}
/* ---- (b) real end-to-end run ------------------------------------------ */
static MunitResult
test_end_to_end(const MunitParameter params[], void *data)
{
(void)params;
(void)data;
struct st_ext_ctx *ctx;
const char *cc;
char cc_buf[64];
char want[128];
char *mk;
int cc_len;
ctx = build_ctx();
munit_assert_not_null(ctx);
cc = st_registry_get_var(st_ext_var_registry(ctx), "CC");
munit_assert_not_null(cc);
/* copy before st_ext_ctx_free() — the registry value is borrowed */
cc_len = snprintf(cc_buf, sizeof cc_buf, "%s", cc);
munit_assert_int(cc_len, >, 0);
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);
mk = makefile_text();
munit_assert_not_null(mk);
/* CC substituted from the registry value */
{
int n = snprintf(want, sizeof want, "CC = %s", cc_buf);
munit_assert_int(n, >, 0);
munit_assert_not_null(strstr(mk, want));
}
/* LIBS accumulated from library checks (pthread + m) */
munit_assert_not_null(strstr(mk, "-lpthread"));
munit_assert_not_null(strstr(mk, "-lm"));
/* prefix defaults to /usr/local */
munit_assert_not_null(strstr(mk, "prefix = /usr/local"));
free(mk);
return MUNIT_OK;
}
/* ---- (c) --prefix is honored ------------------------------------------ */
static MunitResult
test_prefix(const MunitParameter params[], void *data)
{
(void)params;
(void)data;
struct st_ext_ctx *ctx;
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(write_makefile_in(STD_MAKEFILE_IN), ==, 0);
munit_assert_int(run_configure("--prefix=/opt/x"), ==, 0);
mk = makefile_text();
munit_assert_not_null(mk);
munit_assert_not_null(strstr(mk, "prefix = /opt/x"));
free(mk);
return MUNIT_OK;
}
/* ---- (d) an unknown @VAR@ errors naming the variable ------------------ */
static MunitResult
test_unknown_var(const MunitParameter params[], void *data)
{
(void)params;
(void)data;
struct st_ext_ctx *ctx;
char *err;
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("CC = @NOPE@\n"), ==, 0);
munit_assert_int(run_configure(NULL), !=, 0);
err = stderr_text();
munit_assert_not_null(err);
munit_assert_not_null(strstr(err, "NOPE"));
free(err);
return MUNIT_OK;
}
/* ---- (e) a missing Makefile.in is a clean error ----------------------- */
static MunitResult
test_missing_makefile(const MunitParameter params[], void *data)
{
(void)params;
(void)data;
struct st_ext_ctx *ctx;
char *err;
ctx = build_ctx();
munit_assert_not_null(ctx);
munit_assert_int(emit_fixture_configure(ctx), ==, 0);
st_ext_ctx_free(ctx);
/* no Makefile.in written at all */
munit_assert_int(run_configure(NULL), !=, 0);
err = stderr_text();
munit_assert_not_null(err);
munit_assert_not_null(strstr(err, "Makefile.in"));
free(err);
return MUNIT_OK;
}
/* ---- (f) '/', spaces and '&' survive byte-exact ----------------------- */
static MunitResult
test_bytes_exact(const MunitParameter params[], void *data)
{
(void)params;
(void)data;
struct st_ext_ctx *ctx;
char *mk;
ctx = build_ctx();
munit_assert_not_null(ctx);
munit_assert_int(emit_fixture_configure(ctx), ==, 0);
st_ext_ctx_free(ctx);
/* spaces via CFLAGS; '&' and '/' via --prefix */
munit_assert_int(write_makefile_in(STD_MAKEFILE_IN), ==, 0);
munit_assert_int(setenv("CFLAGS", "a b c", 1), ==, 0);
munit_assert_int(run_configure("--prefix='/opt/x & y/z'"), ==, 0);
unsetenv("CFLAGS");
mk = makefile_text();
munit_assert_not_null(mk);
munit_assert_not_null(strstr(mk, "FLAGS = a b c"));
munit_assert_not_null(strstr(mk, "prefix = /opt/x & y/z"));
free(mk);
return MUNIT_OK;
}
/* ---- unrecognized --flag is a clean error ----------------------------- */
static MunitResult
test_unrecognized_option(const MunitParameter params[], void *data)
{
(void)params;
(void)data;
struct st_ext_ctx *ctx;
char *err;
ctx = build_ctx();
munit_assert_not_null(ctx);
munit_assert_int(emit_fixture_configure(ctx), ==, 0);
st_ext_ctx_free(ctx);
munit_assert_int(run_configure("--bogus"), !=, 0);
err = stderr_text();
munit_assert_not_null(err);
munit_assert_not_null(strstr(err, "unrecognized option"));
free(err);
return MUNIT_OK;
}
/* ---- injection: a $(...) VALUE stays inert (not executed) ------------- */
static MunitResult
test_injection_value(const MunitParameter params[], void *data)
{
(void)params;
(void)data;
struct st_ext_ctx *ctx;
char marker[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(marker, sizeof marker, "pwned1"), >, 0);
/* prefix gets a command-substitution payload; it must be a literal. */
munit_assert_int(write_makefile_in("prefix = @prefix@\n"), ==, 0);
munit_assert_int(run_configure("--prefix='$(touch pwned1)'"), ==, 0);
munit_assert_int(access(marker, F_OK), ==, -1);
mk = makefile_text();
munit_assert_not_null(mk);
munit_assert_not_null(strstr(mk, "prefix = $(touch pwned1)"));
free(mk);
return MUNIT_OK;
}
/* ---- injection: an unknown @$(...)@ name stays inert (not executed) --- */
static MunitResult
test_injection_unknown_name(const MunitParameter params[], void *data)
{
(void)params;
(void)data;
struct st_ext_ctx *ctx;
char marker[600];
char *err;
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(marker, sizeof marker, "pwned2"), >, 0);
munit_assert_int(write_makefile_in("X = @$(touch pwned2)@\n"), ==, 0);
munit_assert_int(run_configure(NULL), !=, 0);
munit_assert_int(access(marker, F_OK), ==, -1);
err = stderr_text();
munit_assert_not_null(err);
munit_assert_not_null(strstr(err, "unknown variable"));
free(err);
return MUNIT_OK;
}
/* ---- cross-compile: RUN probes skip with a warning -------------------- */
static MunitResult
test_cross_compile(const MunitParameter params[], void *data)
{
(void)params;
(void)data;
static const char *const docsrc =
"project \"x\" version \"1.0\"\n"
"feature \"sz\" { sizeof \"long\" }\n";
struct st_ext_ctx *ctx;
struct st_kdl_document *doc = NULL;
struct st_error *err = NULL;
char conf[600];
char *e;
ctx = build_ctx();
munit_assert_not_null(ctx);
doc = st_kdl_parse(docsrc, "inline.kdl", &err);
munit_assert_null(err);
munit_assert_not_null(doc);
munit_assert_null(st_kdl_validate(doc));
munit_assert_int(mkpath(conf, sizeof conf, "configure"), >, 0);
munit_assert_null(st_gen_configure_emit_path(conf, doc, ctx));
st_kdl_document_free(doc);
st_ext_ctx_free(ctx);
munit_assert_int(write_makefile_in("CC = @CC@\n"), ==, 0);
munit_assert_int(run_configure("--host=aarch64-linux --build=x86_64-linux"),
==, 0);
e = stderr_text();
munit_assert_not_null(e);
munit_assert_not_null(strstr(e, "cross-compiling"));
free(e);
return MUNIT_OK;
}
static MunitTest tests[] = {
{ "/gen/syntax", test_syntax, setup, teardown, MUNIT_TEST_OPTION_NONE,
NULL },
{ "/gen/end-to-end", test_end_to_end, setup, teardown,
MUNIT_TEST_OPTION_NONE, NULL },
{ "/gen/prefix", test_prefix, setup, teardown, MUNIT_TEST_OPTION_NONE,
NULL },
{ "/gen/unknown-var", test_unknown_var, setup, teardown,
MUNIT_TEST_OPTION_NONE, NULL },
{ "/gen/missing-makefile", test_missing_makefile, setup, teardown,
MUNIT_TEST_OPTION_NONE, NULL },
{ "/gen/bytes-exact", test_bytes_exact, setup, teardown,
MUNIT_TEST_OPTION_NONE, NULL },
{ "/gen/unrecognized-option", test_unrecognized_option, setup, teardown,
MUNIT_TEST_OPTION_NONE, NULL },
{ "/gen/injection-value", test_injection_value, setup, teardown,
MUNIT_TEST_OPTION_NONE, NULL },
{ "/gen/injection-unknown-name", test_injection_unknown_name, setup,
teardown, MUNIT_TEST_OPTION_NONE, NULL },
{ "/gen/cross-compile", test_cross_compile, setup, teardown,
MUNIT_TEST_OPTION_NONE, NULL },
{ NULL, NULL, NULL, NULL, MUNIT_TEST_OPTION_NONE, NULL },
};
static const MunitSuite suite = {
"/gen", tests, NULL, 1, MUNIT_SUITE_OPTION_NONE,
};
int
main(int argc, char *argv[MUNIT_ARRAY_PARAM(argc + 1)])
{
return munit_suite_main(&suite, NULL, argc, argv);
}