From 9f410341975c68e16e98a2b65caa46a245022399 Mon Sep 17 00:00:00 2001 From: huntedbytheirs Date: Sat, 29 Aug 2026 00:35:07 -0400 Subject: [PATCH] feat(gen): generate --help and argument parsing Replace the v1 minimal preamble in the generated ./configure with the full argument-parsing section (src/gen/args.{h,c}): -h/--help usage, --version, --prefix/--exec-prefix/--host/--build in = and space forms, per-DSL-option --enable-/--disable- (default from the option's default property), generic positional VAR=VALUE overrides (identifier-validated, single-quote-escaped eval assignment), unknown --* -> usage on stderr + exit 1, cross_compiling=yes when --host differs from --build. The substitution set gains exec_prefix/host/build/ cross_compiling plus one enable_ per option. Option names are validated as shell identifiers at generation time. Tests: 12 new /gen/args/* cases (help lists --prefix/--enable-debug/ --disable-debug; CC=clang positional + env; enable/debug default; cross_compiling observable; --bogus/--bogus=1/--enable- errors; --prefix missing/empty; space form; exec_prefix defaulting; VAR=VALUE injection inert; hostile option name errors at generation) - 28/28 green, three-shell -n + banned-construct sweep clean. --- src/gen/args.c | 655 ++++++++++++++++++++++++++++++++ src/gen/args.h | 142 +++++++ src/gen/configure.c | 82 ++-- src/gen/configure.h | 53 ++- tests/unit/test_gen_configure.c | 462 +++++++++++++++++++++- 5 files changed, 1333 insertions(+), 61 deletions(-) create mode 100644 src/gen/args.c create mode 100644 src/gen/args.h diff --git a/src/gen/args.c b/src/gen/args.c new file mode 100644 index 0000000..faf300e --- /dev/null +++ b/src/gen/args.c @@ -0,0 +1,655 @@ +/* + * args.c - the generated ./configure preamble emitter (todo 18). + * + * Generates the full --help / argument-parsing section that replaces the + * v1 minimal preamble todo 16 emitted inline in configure.c. See + * gen/args.h for the emitted-section layout, the option -> --enable + * mapping, the VAR=VALUE eval-assignment security argument, the + * cross_compiling contract and the error categories. + * + * Safety invariants, carried through from the detect chain: + * - every user-supplied byte (option names, project name/version) + * reaches the emitted script either through st_sh_quote (single- + * quoted literal) or through a validated-identifier position + * (enable_, --enable-); + * - the positional VAR=VALUE assignment is handled by the EMITTED + * script with the sed-quote + eval pattern (see args.h), never by + * this generator - the value is only ever quoted at configure time. + * + * Copyright (c) 2026 huntedbytheirs + * SPDX-License-Identifier: BSD-3-Clause + */ + +#include "gen/args.h" + +#include "cli.h" /* STUPIDTOOLS_VERSION (the generated-by line) */ + +#include "error.h" +#include "gen/sh_emit.h" +#include "kdl/ast.h" +#include "kdl/value.h" + +#include +#include +#include +#include +#include + +/* ---- small helpers (mirror configure.c) -------------------------------- */ + +/* True when `s` is a valid POSIX shell identifier: letter/underscore, + * then letters/digits/underscores, non-empty. Option names must pass + * this (they land in enable_ / --enable- positions). */ +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; +} + +/* Build an owned ST_ERR_KDL_SCHEMA error whose span lives in the same + * allocation as the error struct (the parser.c pattern). Returns NULL + * only on allocation failure. */ +static struct st_error * +err_at_owned(struct st_span sp, const char *msg) +{ + const size_t align = _Alignof(struct st_span); + const size_t esize = + (sizeof(struct st_error) + align - 1) & ~(align - 1); + struct st_error *e; + struct st_span *spc; + size_t mlen; + + if (msg == NULL) { + msg = ""; + } + e = malloc(esize + sizeof(struct st_span)); + if (e == NULL) { + return NULL; + } + mlen = strlen(msg); + e->message = malloc(mlen + 1); + if (e->message == NULL) { + free(e); + return NULL; + } + memcpy(e->message, msg, mlen + 1); + e->category = ST_ERR_KDL_SCHEMA; + spc = (struct st_span *)((unsigned char *)e + esize); + *spc = sp; + e->span = spc; + return e; +} + +/* Interpret `tok` as the non-empty unannotated string a slot requires; + * on success *out owns the string. Value-model failures are re-owned as + * schema errors (the checks.c pattern). */ +static struct st_error * +extract_string(const struct st_kdl_token_ref *tok, const char *ctx, + char **out) +{ + struct st_kdl_value v; + struct st_error *e; + char msg[192]; + + *out = NULL; + e = st_kdl_value_from_token(tok, &v); + if (e != NULL) { + struct st_span sp = e->span != NULL ? *e->span + : (struct st_span){ NULL, 0, 0 }; + struct st_error *r = err_at_owned(sp, e->message != NULL + ? e->message : ""); + + st_error_free(e); + return r; + } + if (v.kind != ST_KDL_VAL_STRING) { + snprintf(msg, sizeof msg, "%s must be a non-empty string", ctx); + e = err_at_owned(tok->span, msg); + st_kdl_value_free(&v); + return e; + } + if (v.as.str[0] == '\0') { + snprintf(msg, sizeof msg, "%s must be a non-empty string", ctx); + e = err_at_owned(tok->span, msg); + st_kdl_value_free(&v); + return e; + } + *out = v.as.str; + v.as.str = NULL; + st_kdl_value_free(&v); + return NULL; +} + +/* Compare a property key against a literal identifier. */ +static struct st_error * +key_equals(const struct st_kdl_token_ref *key, const char *s, bool *out) +{ + struct st_kdl_value v; + struct st_error *e; + + if (key->kind == ST_TOK_IDENT) { + *out = key->len == strlen(s) && memcmp(key->text, s, key->len) == 0; + return NULL; + } + e = st_kdl_value_from_token(key, &v); + if (e != NULL) { + struct st_error *r; + struct st_span sp = e->span != NULL + ? *e->span + : (struct st_span){ NULL, 0, 0 }; + + r = err_at_owned(sp, e->message != NULL ? e->message : ""); + st_error_free(e); + *out = false; + return r; + } + *out = v.kind == ST_KDL_VAL_STRING && strcmp(v.as.str, s) == 0; + st_kdl_value_free(&v); + return NULL; +} + +/* ---- collection -------------------------------------------------------- */ + +/* Read the `default` bool property of an option node. */ +static struct st_error * +collect_option_default(const struct st_kdl_node *node, bool *out) +{ + const struct st_kdl_prop *prop; + + *out = false; + for (prop = node->props; prop != NULL; prop = prop->next) { + bool is_default = false; + struct st_error *e = key_equals(&prop->key, "default", &is_default); + struct st_kdl_value v; + + if (e != NULL) { + return e; + } + if (!is_default) { + continue; + } + e = st_kdl_value_from_token(&prop->value, &v); + if (e != NULL) { + struct st_error *r; + struct st_span sp = e->span != NULL + ? *e->span + : (struct st_span){ NULL, 0, 0 }; + + r = err_at_owned(sp, e->message != NULL ? e->message : ""); + st_error_free(e); + return r; + } + if (v.kind != ST_KDL_VAL_BOOL) { + e = err_at_owned(prop->value.span, + "option 'default' must be an unannotated " + "#true or #false"); + st_kdl_value_free(&v); + return e; + } + *out = v.as.b; + st_kdl_value_free(&v); + return NULL; + } + return NULL; +} + +/* Parse the `project "" version ""` node into owned + * strings. The schema guarantees the shape; this is defense in depth. */ +static struct st_error * +collect_project(const struct st_kdl_node *node, struct st_gen_args *out) +{ + const struct st_kdl_arg *a0; + const struct st_kdl_arg *a1; + const struct st_kdl_arg *a2; + struct st_error *e; + + a0 = node->args; + a1 = a0 != NULL ? a0->next : NULL; + a2 = a1 != NULL ? a1->next : NULL; + if (a0 == NULL || a1 == NULL || a2 == NULL) { + return err_at_owned(node->name.span, + "project node is malformed (expected 'project " + "\"name\" version \"semver\"')"); + } + if (a1->value.kind != ST_TOK_IDENT || a1->value.len != 7 || + memcmp(a1->value.text, "version", 7) != 0) { + return err_at_owned(a1->value.span, + "project node is malformed (expected the " + "'version' keyword before the version string)"); + } + e = extract_string(&a0->value, "project name", &out->project_name); + if (e != NULL) { + return e; + } + e = extract_string(&a2->value, "project version", &out->project_version); + if (e != NULL) { + return e; + } + return NULL; +} + +struct st_error * +st_gen_args_collect(const struct st_kdl_document *doc, struct st_gen_args *out) +{ + struct st_error *err = NULL; + const struct st_kdl_node *node; + bool have_project = false; + + if (doc == NULL || out == NULL) { + return st_error_usage("st_gen_args_collect: NULL argument"); + } + memset(out, 0, sizeof(*out)); + for (node = doc->nodes; node != NULL; node = node->next) { + struct st_kdl_value v; + struct st_error *e; + bool is_option = false; + bool is_project = false; + + /* identify the node kind by name (any string form) */ + if (node->name.kind == ST_TOK_IDENT) { + is_option = node->name.len == 6 && + memcmp(node->name.text, "option", 6) == 0; + is_project = node->name.len == 7 && + memcmp(node->name.text, "project", 7) == 0; + } else { + e = st_kdl_value_from_token(&node->name, &v); + if (e != NULL) { + struct st_span sp = e->span != NULL + ? *e->span + : (struct st_span){ NULL, 0, 0 }; + + err = err_at_owned(sp, e->message != NULL ? e->message : ""); + st_error_free(e); + goto done; + } + is_option = v.kind == ST_KDL_VAL_STRING && + strcmp(v.as.str, "option") == 0; + is_project = v.kind == ST_KDL_VAL_STRING && + strcmp(v.as.str, "project") == 0; + st_kdl_value_free(&v); + } + + if (is_project && !have_project) { + have_project = true; + err = collect_project(node, out); + if (err != NULL) { + goto done; + } + continue; + } + if (!is_option) { + continue; + } + { + char *name = NULL; + bool default_on = false; + size_t i; + char msg[256]; + + e = extract_string(&node->args->value, "option name", &name); + if (e != NULL) { + err = e; + goto done; + } + if (!valid_ident(name)) { + snprintf(msg, sizeof msg, + "option name '%s' is not a valid shell identifier " + "(--enable- requires [A-Za-z_][A-Za-z0-9_]*)", + name); + err = err_at_owned(node->args->value.span, msg); + free(name); + goto done; + } + e = collect_option_default(node, &default_on); + if (e != NULL) { + free(name); + err = e; + goto done; + } + for (i = 0; i < out->option_count; i++) { + if (strcmp(out->options[i].name, name) == 0) { + snprintf(msg, sizeof msg, "duplicate option '%s'", name); + err = err_at_owned(node->args->value.span, msg); + free(name); + goto done; + } + } + { + struct st_gen_option *grown = realloc( + out->options, + (out->option_count + 1) * sizeof(*out->options)); + + if (grown == NULL) { + free(name); + err = st_error_internal("out of memory collecting " + "options"); + goto done; + } + out->options = grown; + out->options[out->option_count].name = name; + out->options[out->option_count].default_on = default_on; + out->option_count++; + } + } + } + if (!have_project) { + err = st_error_kdl_schema("missing project node"); + goto done; + } + +done: + if (err != NULL) { + st_gen_args_free(out); + } + return err; +} + +void +st_gen_args_free(struct st_gen_args *args) +{ + size_t i; + + if (args == NULL) { + return; + } + for (i = 0; i < args->option_count; i++) { + free(args->options[i].name); + } + free(args->options); + free(args->project_name); + free(args->project_version); + memset(args, 0, sizeof(*args)); +} + +/* ---- fixed shell text (emitted verbatim via st_sh_emit_str) ------------ */ + +/* The `%` in the emitted printf formats is why all of this is emitted + * with st_sh_emit_str (verbatim), not fprintf (which would eat `%s`). */ + +static const char ARGS_PRE[] = + "\n" + "# --- PREAMBLE ---\n" + "# Full argument parsing (todo 18): --help/-h, --version, --prefix/\n" + "# --exec-prefix/--host/--build in both '=' and space forms, the\n" + "# per-option --enable-/--disable- flags, and generic\n" + "# VAR=VALUE overrides (positional, e.g. `./configure CC=clang`, or\n" + "# exported by the invoking shell, e.g. `CC=clang ./configure`).\n" + "# Unknown --* options error. The loop only READS \"$@\", so the\n" + "# config.log init right after this section still sees the complete\n" + "# original invocation.\n" + "prefix=/usr/local\n" + "exec_prefix=\n" + "srcdir=.\n" + "build=\n" + "host=\n" + "cross_compiling=no\n"; + +static const char USAGE_HEAD[] = + "st_usage() {\n" + " printf '%s\\n' 'Usage: ./configure [OPTION]... [VAR=VALUE]...'\n" + " printf '%s\\n' ''\n" + " printf '%s\\n' 'Configuration:'\n" + " printf '%s\\n' ' --prefix=DIR, --prefix DIR'\n" + " printf '%s\\n' ' install architecture-independent files in DIR (default: /usr/local)'\n" + " printf '%s\\n' ' --exec-prefix=DIR, --exec-prefix DIR'\n" + " printf '%s\\n' ' install architecture-dependent files in DIR (default: same as --prefix)'\n" + " printf '%s\\n' ' --host=TRIPLET, --host TRIPLET'\n" + " printf '%s\\n' ' the platform the program will run on;'\n" + " printf '%s\\n' ' differs from --build -> cross-compiling'\n" + " printf '%s\\n' ' --build=TRIPLET, --build TRIPLET'\n" + " printf '%s\\n' ' the platform the build is running on'\n" + " printf '%s\\n' ' VAR=VALUE override the variable VAR (e.g. CC=clang);'\n" + " printf '%s\\n' ' both VAR=VALUE ./configure and'\n" + " printf '%s\\n' ' ./configure VAR=VALUE work'\n"; + +static const char USAGE_TAIL[] = + " printf '%s\\n' ''\n" + " printf '%s\\n' 'Informational:'\n" + " printf '%s\\n' ' -h, --help print this help and exit'\n" + " printf '%s\\n' ' --version print the program version and exit'\n" + "}\n" + "\n"; + +static const char LOOP_PRE[] = + "# The argument parser. A loop-carried $st_need implements the two-\n" + "# token lookahead the space forms need: when $st_need is set, the\n" + "# NEXT argument is the value of that option (a value that looks like\n" + "# an option is rejected). A still-pending $st_need after the loop\n" + "# is an error.\n" + "st_need=\n" + "for st_arg in \"$@\"; do\n" + " if [ -n \"$st_need\" ]; then\n" + " case \"$st_arg\" in\n" + " -*)\n" + " printf \"configure: error: option '--%s' requires an argument\\n\" \"$st_need\" >&2\n" + " st_usage >&2\n" + " exit 1\n" + " ;;\n" + " *)\n" + " case \"$st_need\" in\n" + " prefix) prefix=$st_arg ;;\n" + " exec-prefix) exec_prefix=$st_arg ;;\n" + " host) host=$st_arg ;;\n" + " build) build=$st_arg ;;\n" + " esac\n" + " st_need=\n" + " continue\n" + " ;;\n" + " esac\n" + " fi\n" + " case \"$st_arg\" in\n" + " -h|--help)\n" + " st_usage\n" + " exit 0\n" + " ;;\n"; + +static const char LOOP_VALUE_ARMS[] = + " --prefix=*)\n" + " prefix=${st_arg#--prefix=}\n" + " if [ -z \"$prefix\" ]; then\n" + " printf '%s\\n' \"configure: error: option '--prefix' requires a non-empty argument\" >&2\n" + " st_usage >&2\n" + " exit 1\n" + " fi\n" + " ;;\n" + " --exec-prefix=*)\n" + " exec_prefix=${st_arg#--exec-prefix=}\n" + " if [ -z \"$exec_prefix\" ]; then\n" + " printf '%s\\n' \"configure: error: option '--exec-prefix' requires a non-empty argument\" >&2\n" + " st_usage >&2\n" + " exit 1\n" + " fi\n" + " ;;\n" + " --host=*)\n" + " host=${st_arg#--host=}\n" + " if [ -z \"$host\" ]; then\n" + " printf '%s\\n' \"configure: error: option '--host' requires a non-empty argument\" >&2\n" + " st_usage >&2\n" + " exit 1\n" + " fi\n" + " ;;\n" + " --build=*)\n" + " build=${st_arg#--build=}\n" + " if [ -z \"$build\" ]; then\n" + " printf '%s\\n' \"configure: error: option '--build' requires a non-empty argument\" >&2\n" + " st_usage >&2\n" + " exit 1\n" + " fi\n" + " ;;\n" + " --prefix|--exec-prefix|--host|--build)\n" + " st_need=${st_arg#--}\n" + " ;;\n"; + +static const char LOOP_TAIL[] = + " --*=*)\n" + " # an unknown option written in =value form\n" + " printf 'configure: error: unrecognized option %s\\n' \"$st_arg\" >&2\n" + " st_usage >&2\n" + " exit 1\n" + " ;;\n" + " *=*)\n" + " # VAR=VALUE: split at the FIRST '=', validate VAR as a\n" + " # shell identifier, single-quote-escape the value and eval\n" + " # the assignment. The value lands between REAL single\n" + " # quotes in the eval string (the expansion happens in the\n" + " # outer pass, eval re-parses the result), so nothing in it\n" + " # is ever re-parsed or executed (autoconf's pattern).\n" + " st_var=${st_arg%%=*}\n" + " st_val=${st_arg#*=}\n" + " case \"$st_var\" in\n" + " ''|*[!A-Za-z0-9_]*|[0-9]*)\n" + " printf 'configure: error: invalid variable assignment %s\\n' \"$st_arg\" >&2\n" + " st_usage >&2\n" + " exit 1\n" + " ;;\n" + " esac\n" + " st_val_q=$(printf '%s' \"$st_val\" | sed \"s/'/'\\\\\\\\''/g\")\n" + " eval \"$st_var='$st_val_q'\"\n" + " ;;\n" + " *)\n" + " printf 'configure: error: unrecognized option %s\\n' \"$st_arg\" >&2\n" + " st_usage >&2\n" + " exit 1\n" + " ;;\n" + " esac\n" + "done\n" + "\n" + "# a still-pending space-form option had no value\n" + "if [ -n \"$st_need\" ]; then\n" + " printf \"configure: error: option '--%s' requires an argument\\n\" \"$st_need\" >&2\n" + " st_usage >&2\n" + " exit 1\n" + "fi\n" + "\n" + "# exec_prefix defaults to prefix (autoconf semantics)\n" + "if [ -z \"$exec_prefix\" ]; then\n" + " exec_prefix=$prefix\n" + "fi\n" + "\n" + "# cross-compiling when --host differs from --build (both set)\n" + "if [ -n \"$host\" ] && [ \"$host\" != \"$build\" ]; then\n" + " cross_compiling=yes\n" + "fi\n"; + +/* ---- emission ---------------------------------------------------------- */ + +struct st_error * +st_gen_args_emit(FILE *out, const struct st_gen_args *args) +{ + size_t i; + char *qn; + char *qv; + + if (out == NULL || args == NULL) { + return st_error_usage("st_gen_args_emit: NULL argument"); + } + if (st_sh_emit_str(out, ARGS_PRE) < 0) { + return st_error_io("I/O error emitting the preamble"); + } + + /* per-option defaults: enable_=yes|no */ + for (i = 0; i < args->option_count; i++) { + if (fprintf(out, "enable_%s=%s\n", args->options[i].name, + args->options[i].default_on ? "yes" : "no") < 0) { + return st_error_io("I/O error emitting the preamble"); + } + } + + /* the st_usage() help text */ + if (st_sh_emit_str(out, "\n") < 0 || + st_sh_emit_str(out, USAGE_HEAD) < 0) { + return st_error_io("I/O error emitting the preamble"); + } + if (args->option_count > 0) { + if (st_sh_emit_str(out, + " printf '%s\\n' ''\n" + " printf '%s\\n' 'Optional Features'\n" + " printf '%s\\n' ' (DSL option nodes; --enable " + "sets the enable_ variable to yes)'\n") < 0) { + return st_error_io("I/O error emitting the preamble"); + } + } + for (i = 0; i < args->option_count; i++) { + const char *name = args->options[i].name; + + if (fprintf(out, + " printf '%%s\\n' ' --enable-%s enable " + "the %s option (default: %s)'\n" + " printf '%%s\\n' ' --disable-%s disable " + "the %s option'\n", + name, name, args->options[i].default_on ? "yes" : "no", + name, name) < 0) { + return st_error_io("I/O error emitting the preamble"); + } + } + if (st_sh_emit_str(out, USAGE_TAIL) < 0) { + return st_error_io("I/O error emitting the preamble"); + } + + /* the parser loop */ + if (st_sh_emit_str(out, LOOP_PRE) < 0) { + return st_error_io("I/O error emitting the preamble"); + } + qn = st_sh_quote(args->project_name != NULL ? args->project_name : ""); + qv = st_sh_quote(args->project_version != NULL ? args->project_version + : ""); + if (qn == NULL || qv == NULL) { + free(qn); + free(qv); + return st_error_internal("out of memory quoting the project " + "identity"); + } + if (fprintf(out, + " --version)\n" + " printf 'configure for %%s %%s\\n' %s %s\n" + " printf '%%s\\n' 'generated by stupidtools " + STUPIDTOOLS_VERSION + " - DO NOT EDIT'\n" + " exit 0\n" + " ;;\n", + qn, qv) < 0) { + free(qn); + free(qv); + return st_error_io("I/O error emitting the preamble"); + } + free(qn); + free(qv); + if (st_sh_emit_str(out, LOOP_VALUE_ARMS) < 0) { + return st_error_io("I/O error emitting the preamble"); + } + + /* per-option enable/disable arms (names are validated identifiers) */ + for (i = 0; i < args->option_count; i++) { + const char *name = args->options[i].name; + + if (fprintf(out, + " --enable-%s) enable_%s=yes ;;\n" + " --disable-%s) enable_%s=no ;;\n", + name, name, name, name) < 0) { + return st_error_io("I/O error emitting the preamble"); + } + } + if (st_sh_emit_str(out, LOOP_TAIL) < 0) { + return st_error_io("I/O error emitting the preamble"); + } + if (ferror(out)) { + return st_error_io("I/O error emitting the preamble"); + } + return NULL; +} diff --git a/src/gen/args.h b/src/gen/args.h new file mode 100644 index 0000000..8668c62 --- /dev/null +++ b/src/gen/args.h @@ -0,0 +1,142 @@ +/* + * args.h - the generated ./configure preamble: --help + argument parsing + * (todo 18). + * + * This module REPLACES the v1 minimal preamble todo 16 emitted inline in + * configure.c (structurally marked `# --- PREAMBLE ---` there for exactly + * this). It generates the FULL argument-parsing section of the emitted + * POSIX-sh ./configure: + * + * - `--help` / `-h`: a real usage text - the standard options + * (--prefix=, --exec-prefix=, --host=, + * --build=), the generic VAR=VALUE override, the per-option + * enable/disable flags (--enable- and --disable-), and + * --help/--version - then exit 0. + * - `--version`: the DSL project's name + version (the project node), + * exit 0. + * - `--prefix=` / `--prefix ` (both forms) and the same two + * forms for `--exec-prefix`, `--host`, `--build`. An empty value + * (`--prefix=`), a missing value, or a value that looks like an + * option is a clean error: message + usage to stderr, exit 1. + * - per-DSL-option `--enable-` / `--disable-` setting + * `enable_=yes|no`, defaulted from the option's `default` + * property (see THE OPTION -> FLAG MAPPING below). + * - generic positional `VAR=VALUE` overrides (`./configure CC=clang`); + * the exported form (`CC=clang ./configure`) is the invoking shell's + * ordinary environment contract and needs no parser support (both + * land in $VAR, which the substitution and the toolchain defaults + * read). VAR must be a valid shell identifier; the value is + * single-quote-escaped and assigned via eval so nothing in it is + * ever re-parsed or executed (autoconf's pattern; see EMITTED + * SECTION LAYOUT). + * - an unknown `--*` prints "unrecognized option" + the usage to + * stderr and exits 1. + * - `$cross_compiling=yes` when `--host` differs from `--build` (both + * must be set; a simple string comparison). + * - `$exec_prefix` defaults to `$prefix` (autoconf semantics). + * + * THE OPTION -> FLAG MAPPING (the contract todo 23 consumes) + * ----------------------------------------------------------- + * A top-level DSL `option "name" default=#bool` node (src/kdl/schema.h) + * maps to the flag pair `--enable-` / `--disable-` and the + * shell variable `enable_` (yes|no; the DSL `default` property, + * false when the property is absent). So `option "debug" default=#false` + * yields `--enable-debug` / `--disable-debug` with `enable_debug=no` by + * default. v1 maps EVERY option to --enable/--disable; the + * `--with-` / `--without-` pair is reserved for a future DSL node + * kind and is not generated today. The option name must be a valid POSIX + * shell identifier ([A-Za-z_][A-Za-z0-9_]*) - enforced at GENERATION + * time (a hostile name errors before any shell text is emitted), as is + * name uniqueness. The section is generated from the document, never + * hardcoded. + * + * EMITTED SECTION LAYOUT (inside the generated configure) + * ------------------------------------------------------- + * # --- PREAMBLE --- + * prefix=/usr/local; exec_prefix=; srcdir=.; build=; host=; + * cross_compiling=no; enable_= per option + * st_usage() { ... } the help text + * for st_arg in "$@"; do ... the parser; a loop-carried $st_need + * variable implements the two-token + * lookahead the space forms need (when + * $st_need is set, the NEXT argument is + * the value of that option) + * done + * trailing checks: a still-pending $st_need is an error; exec_prefix + * defaults to $prefix; cross_compiling when host differs from build. + * + * The VAR=VALUE assignment is `st_val_q=$(printf '%s' "$st_val" | sed + * "s/'/'\\''/g")` followed by `eval "$st_var='$st_val_q'"`: the value + * lands between REAL single quotes in the eval string (the expansion + * happens in the outer pass, eval re-parses the result), so quotes, + * `$()`, backticks, `;` and newlines are all literal - the same + * sed-quote bake-in config.c uses for config.status. + * + * CONTRACT PRESERVED (todo 17): the parser only READS "$@" (a for loop + * never shifts), so the config.log init emitted right after this section + * still captures the complete original invocation into st_orig_args. + * + * POSIX-SH ONLY (AGENTS.md §4): no [[ ]], no arrays, no local, no ==, no + * <<<, no &>, no set -e; `$(...)` is used. sh -n / bash -n / zsh -n must + * pass (dash is not installed on this host - see issues.md). + * + * ERRORS + * ------ + * st_gen_args_collect: NULL arguments -> ST_ERR_USAGE; a non-identifier + * or duplicate option name, or a malformed project node -> + * ST_ERR_KDL_SCHEMA (span heap-allocated in the same block as the error, + * the parser.c pattern); a value-model failure on a name/version/default + * is re-owned as a schema error. st_gen_args_emit: NULL arguments -> + * ST_ERR_USAGE; quoting/allocation failure -> ST_ERR_INTERNAL; stream + * write failure -> ST_ERR_IO. Both leave no owned memory behind; a + * failed collect leaves *out zeroed (free-able with st_gen_args_free). + * + * Copyright (c) 2026 huntedbytheirs + * SPDX-License-Identifier: BSD-3-Clause + */ + +#ifndef ST_GEN_ARGS_H +#define ST_GEN_ARGS_H + +#include +#include +#include + +struct st_error; +struct st_kdl_document; + +/* One collected DSL option: the --enable-/--disable- pair and + * its default. */ +struct st_gen_option { + char *name; /* owned; validated POSIX shell identifier */ + bool default_on; /* the `default` property; false when absent */ +}; + +/* Everything the preamble emitter needs, collected from a validated + * document. All strings are owned; release with st_gen_args_free() (NULL + * args is a safe no-op). */ +struct st_gen_args { + char *project_name; /* project "" */ + char *project_version; /* project ... version "" */ + struct st_gen_option *options; + size_t option_count; +}; + +/* Collect the project name + version and the top-level option nodes from + * `doc` (schema-validated) into `out`. `out` is zeroed first; on error + * any partially-collected memory is released and `out` is left zeroed. + * Returns NULL on success, an owned st_error otherwise (see ERRORS). */ +struct st_error *st_gen_args_collect(const struct st_kdl_document *doc, + struct st_gen_args *out); + +/* Release everything owned by `args`. NULL is a safe no-op. */ +void st_gen_args_free(struct st_gen_args *args); + +/* Emit the `# --- PREAMBLE ---` section (the full argument parser, the + * st_usage() help text, the per-option enable_ defaults and flags, the + * VAR=VALUE handler, the trailing exec_prefix/cross_compiling checks) + * into `out` from a collected `args`. Returns NULL on success, an owned + * st_error otherwise (see ERRORS). */ +struct st_error *st_gen_args_emit(FILE *out, const struct st_gen_args *args); + +#endif /* ST_GEN_ARGS_H */ diff --git a/src/gen/configure.c b/src/gen/configure.c index 73e21b1..4f2ee01 100644 --- a/src/gen/configure.c +++ b/src/gen/configure.c @@ -20,6 +20,7 @@ * SPDX-License-Identifier: BSD-3-Clause */ +#include "gen/args.h" #include "gen/config.h" #include "gen/configure.h" @@ -205,39 +206,6 @@ name_equals(const struct st_kdl_token_ref *name, const char *s, bool *out) /* The `%` in `printf '... %s ...'` is why these are emitted with * st_sh_emit_str (verbatim), not fprintf (which would eat `%s`). */ -static const char PREAMBLE[] = - "\n" - "# --- PREAMBLE ---\n" - "# v1 MINIMAL argument handling. todo 18 replaces this whole section\n" - "# with the full --help / --enable-*/--with-*/VAR=VALUE parser.\n" - "prefix=/usr/local\n" - "srcdir=.\n" - "build=\n" - "host=\n" - "cross_compiling=no\n" - "\n" - "# VAR=VALUE overrides reach configure as environment variables (the\n" - "# invoking shell exported them, e.g. `CC=clang ./configure`), so v1\n" - "# needs no handling here. The positional form (`./configure CC=clang`)\n" - "# is accepted but ignored until todo 18 exports it.\n" - "for st_arg in \"$@\"; do\n" - " case \"$st_arg\" in\n" - " --prefix=*) prefix=${st_arg#--prefix=} ;;\n" - " --build=*) build=${st_arg#--build=} ;;\n" - " --host=*) host=${st_arg#--host=} ;;\n" - " *=*) : ;;\n" - " *)\n" - " printf 'configure: error: unrecognized option %s\\n' \"$st_arg\" >&2\n" - " exit 1\n" - " ;;\n" - " esac\n" - "done\n" - "\n" - "# cross-compiling when --host differs from --build (both set)\n" - "if [ -n \"$host\" ] && [ \"$host\" != \"$build\" ]; then\n" - " cross_compiling=yes\n" - "fi\n"; - static const char HOST_DETECT_PRE[] = "\n" "# --- host detection ---\n"; @@ -601,6 +569,7 @@ st_gen_configure_emit(FILE *out, const struct st_kdl_document *doc, size_t i; struct st_error *err = NULL; const struct st_kdl_node *node; + struct st_gen_args args; char **subst_names = NULL; size_t n_subst = 0; char **feat_names = NULL; @@ -610,19 +579,27 @@ st_gen_configure_emit(FILE *out, const struct st_kdl_document *doc, if (out == NULL || doc == NULL || ctx == NULL) { return st_error_usage("st_gen_configure_emit: NULL argument"); } + memset(&args, 0, sizeof args); + err = st_gen_args_collect(doc, &args); + if (err != NULL) { + goto done; + } reg = st_ext_var_registry(ctx); if (reg == NULL) { - return st_error_usage("st_gen_configure_emit: ctx has no variable " - "registry"); + err = st_error_usage("st_gen_configure_emit: ctx has no variable " + "registry"); + goto done; } n_reg = st_registry_var_count(reg); /* collect the full @VAR@ substitution set: registered vars + the - * built-ins LIBS/prefix/srcdir */ - n_subst = n_reg + 3; + * built-ins LIBS/prefix/exec_prefix/srcdir/host/build/cross_compiling + * + one enable_ per DSL option (todo 18) */ + n_subst = n_reg + 7 + args.option_count; subst_names = calloc(n_subst, sizeof(*subst_names)); if (subst_names == NULL) { - return st_error_internal("out of memory building substitution set"); + err = st_error_internal("out of memory building substitution set"); + goto done; } for (i = 0; i < n_reg; i++) { const char *name = st_registry_var_name(reg, i); @@ -640,7 +617,20 @@ st_gen_configure_emit(FILE *out, const struct st_kdl_document *doc, } subst_names[n_reg + 0] = "LIBS"; subst_names[n_reg + 1] = "prefix"; - subst_names[n_reg + 2] = "srcdir"; + subst_names[n_reg + 2] = "exec_prefix"; + subst_names[n_reg + 3] = "srcdir"; + subst_names[n_reg + 4] = "host"; + subst_names[n_reg + 5] = "build"; + subst_names[n_reg + 6] = "cross_compiling"; + for (i = 0; i < args.option_count; i++) { + subst_names[n_reg + 7 + i] = strbuild("enable_%s", + args.options[i].name); + if (subst_names[n_reg + 7 + i] == NULL) { + err = st_error_internal("out of memory building substitution " + "set"); + goto done; + } + } /* 1. shebang + header comment */ if (st_sh_emit_str(out, "#!/bin/sh\n") < 0 || @@ -651,9 +641,9 @@ st_gen_configure_emit(FILE *out, const struct st_kdl_document *doc, goto done; } - /* 2. preamble (minimal args; todo 18 replaces it) */ - if (st_sh_emit_str(out, PREAMBLE) < 0) { - err = st_error_io("I/O error emitting preamble"); + /* 2. preamble: full argument parsing (todo 18) */ + err = st_gen_args_emit(out, &args); + if (err != NULL) { goto done; } @@ -787,7 +777,15 @@ done: free(feat_names[i]); } free(feat_names); + /* the enable_ entries are owned (strbuild); the rest of + * subst_names holds borrowed literals/registry pointers */ + if (subst_names != NULL && n_reg + 7 <= n_subst) { + for (i = n_reg + 7; i < n_subst; i++) { + free(subst_names[i]); + } + } free(subst_names); + st_gen_args_free(&args); return err; } diff --git a/src/gen/configure.h b/src/gen/configure.h index 3a94883..8903199 100644 --- a/src/gen/configure.h +++ b/src/gen/configure.h @@ -11,17 +11,31 @@ * GENERATED SCRIPT LAYOUT (the contract todos 17/18/23 consume) * ------------------------------------------------------------- * 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: - * - `--prefix=` stores $prefix (default /usr/local); - * - `--host=` / `--build=` store $host / $build; - * $cross_compiling=yes when --host differs from --build (a simple - * string comparison; both must be set), else no; - * - `VAR=VALUE` overrides are documented as already being environment - * variables when configure runs (the invoking shell exported them); - * the positional form is accepted-but-ignored until todo 18; - * - an unknown `--*` prints "unrecognized option" to stderr and - * exits 1. + * 2. PREAMBLE — FULL argument handling (todo 18, emitted by + * src/gen/args.h's st_gen_args_emit from the collected DSL project + + * option nodes): the `# --- PREAMBLE ---` section sets + * prefix=/usr/local, exec_prefix=, srcdir=., build=, host=, + * cross_compiling=no and enable_= per DSL option, + * defines st_usage(), then parses "$@" (READ-ONLY — the loop never + * shifts, so st_orig_args stays complete for section 3): + * - `-h`/`--help`: usage on stdout, exit 0; + * - `--version`: project name + version, exit 0; + * - `--prefix` / `--exec-prefix` / `--host` / `--build` in both + * `--opt=value` and `--opt value` forms (a missing, empty or + * option-looking value errors with usage on stderr, exit 1); + * - per-option `--enable-` / `--disable-` setting + * enable_=yes|no (default from the option's `default` + * property; a DSL `option "debug" default=#false` maps to + * --enable-debug/--disable-debug with default no); + * - generic positional VAR=VALUE (e.g. `./configure CC=clang`): + * VAR is identifier-validated, the value is single-quote-escaped + * and eval-assigned (nothing re-parsed); the exported form + * (`CC=clang ./configure`) is the invoking shell's environment + * contract and needs no parser support; + * - an unknown `--*` prints "unrecognized option" + usage to + * stderr and exits 1. + * Trailing: exec_prefix defaults to $prefix; cross_compiling=yes + * when --host differs from --build (both set). * 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 @@ -59,8 +73,9 @@ * 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 + * exec_prefix, srcdir, host, build, cross_compiling} ∪ {enable_ + * per DSL option}) 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. @@ -98,11 +113,13 @@ * ------ * All functions return NULL on success or an owned st_error. Failures are * ST_ERR_USAGE for NULL arguments; ST_ERR_KDL_SCHEMA (span heap-allocated - * in the same block as the error, the parser.c pattern) for a feature name - * that is not a valid POSIX shell identifier; ST_ERR_INTERNAL for quoting - * allocation failures; ST_ERR_IO for stream write failures or (emit_path) - * an unopenable path. The document is only read, never modified or freed; - * the ctx is only read (its registry values are borrowed). + * in the same block as the error, the parser.c pattern) for a feature or + * option name that is not a valid POSIX shell identifier (option errors + * come from st_gen_args_collect, src/gen/args.h); ST_ERR_INTERNAL for + * quoting allocation failures; ST_ERR_IO for stream write failures or + * (emit_path) an unopenable path. The document is only read, never + * modified or freed; the ctx is only read (its registry values are + * borrowed). * * Copyright (c) 2026 huntedbytheirs * SPDX-License-Identifier: BSD-3-Clause diff --git a/tests/unit/test_gen_configure.c b/tests/unit/test_gen_configure.c index 9a9462f..4ac4039 100644 --- a/tests/unit/test_gen_configure.c +++ b/tests/unit/test_gen_configure.c @@ -1,4 +1,4 @@ -/* 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 */ +/* LINK: ../../src/gen/args.c ../../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 @@ -181,6 +181,31 @@ run_configure(const char *args) return WEXITSTATUS(spawned); } +/* Run "cd temp_dir && sh ./configure " with stdout captured to + * run.out and stderr to run.err. `env` is a prefix like "CC=clang " (the + * exported VAR=VALUE form) or NULL/"" for none. Returns the exit status, + * or -1 if configure could not be spawned. */ +static int +run_configure_full(const char *env, const char *args) +{ + char cmd[2400]; + int n; + int spawned; + + n = snprintf(cmd, sizeof cmd, + "cd '%s' && %ssh ./configure %s >'%s/run.out' 2>'%s/run.err'", + temp_dir, env != NULL ? env : "", 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); +} + /* " -n ": the shell's exit status or -1. */ static int syntax_check(const char *shell, const char *path) @@ -235,6 +260,14 @@ slurp_named(const char *name) return slurp(path); } +/* Slurp temp_dir/run.out (stdout of the last run_configure_full), or + * NULL. Caller frees. */ +static char * +stdout_text(void) +{ + return slurp_named("run.out"); +} + /* 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 @@ -968,6 +1001,411 @@ test_config_status_help(const MunitParameter params[], void *data) return MUNIT_OK; } +/* ---- todo 18 (a): the full preamble stays shell-safe + carries the + * parser machinery ------------------------------------------------------ */ + +static MunitResult +test_args_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); + munit_assert_not_null(strstr(bytes, "# --- PREAMBLE ---")); + munit_assert_not_null(strstr(bytes, "st_usage()")); + /* the fixture's option "debug" -> enable/disable arms + default */ + munit_assert_not_null(strstr(bytes, "--enable-debug) enable_debug=yes")); + munit_assert_not_null(strstr(bytes, "--disable-debug) enable_debug=no")); + munit_assert_not_null(strstr(bytes, "enable_debug=no")); + /* VAR=VALUE parser machinery (identifier check + quoted eval) */ + munit_assert_not_null(strstr(bytes, "st_val_q=")); + munit_assert_not_null(strstr(bytes, "st_var=${st_arg%%=*}")); + free(bytes); + return MUNIT_OK; +} + +/* ---- todo 18 (b): --help lists the standard + per-option flags -------- */ + +static MunitResult +test_args_help(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); + + /* help exits 0 BEFORE probing/substitution (no Makefile.in needed) */ + munit_assert_int(run_configure_full(NULL, "--help"), ==, 0); + out = stdout_text(); + munit_assert_not_null(out); + munit_assert_not_null(strstr(out, "Usage:")); + munit_assert_not_null(strstr(out, "--prefix")); + munit_assert_not_null(strstr(out, "--exec-prefix")); + munit_assert_not_null(strstr(out, "--host")); + munit_assert_not_null(strstr(out, "--build")); + munit_assert_not_null(strstr(out, "VAR=VALUE")); + munit_assert_not_null(strstr(out, "--enable-debug")); + munit_assert_not_null(strstr(out, "--disable-debug")); + munit_assert_not_null(strstr(out, "--version")); + free(out); + return MUNIT_OK; +} + +/* ---- todo 18 (g): --version prints name + version, exits 0 ------------ */ + +static MunitResult +test_args_version(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(run_configure_full(NULL, "--version"), ==, 0); + out = stdout_text(); + munit_assert_not_null(out); + munit_assert_not_null(strstr(out, "stupidtools")); + munit_assert_not_null(strstr(out, "1.0.0")); + free(out); + return MUNIT_OK; +} + +/* ---- todo 18 (c): CC=clang as positional AND as env override ---------- */ + +static MunitResult +test_args_cc_override(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); + + /* positional form: ./configure CC=clang */ + munit_assert_int(write_makefile_in("CC = @CC@\n"), ==, 0); + munit_assert_int(run_configure_full(NULL, "CC=clang"), ==, 0); + mk = makefile_text(); + munit_assert_not_null(mk); + munit_assert_not_null(strstr(mk, "CC = clang")); + free(mk); + + /* exported form: CC=clang ./configure */ + munit_assert_int(run_configure_full("CC=clang ", NULL), ==, 0); + mk = makefile_text(); + munit_assert_not_null(mk); + munit_assert_not_null(strstr(mk, "CC = clang")); + free(mk); + return MUNIT_OK; +} + +/* ---- todo 18 (d): --enable-debug / --disable-debug / default ---------- */ + +static MunitResult +test_args_enable_debug(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("DEBUG = @enable_debug@\n"), ==, 0); + + munit_assert_int(run_configure_full(NULL, "--enable-debug"), ==, 0); + mk = makefile_text(); + munit_assert_not_null(mk); + munit_assert_not_null(strstr(mk, "DEBUG = yes")); + free(mk); + + /* the fixture option has default=#false, so no flag -> no */ + munit_assert_int(run_configure_full(NULL, NULL), ==, 0); + mk = makefile_text(); + munit_assert_not_null(mk); + munit_assert_not_null(strstr(mk, "DEBUG = no")); + free(mk); + + munit_assert_int(run_configure_full(NULL, "--disable-debug"), ==, 0); + mk = makefile_text(); + munit_assert_not_null(mk); + munit_assert_not_null(strstr(mk, "DEBUG = no")); + free(mk); + return MUNIT_OK; +} + +/* ---- todo 18 (e): --host != --build -> cross_compiling=yes ------------ */ + +static MunitResult +test_args_cross_compiling(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("CROSS = @cross_compiling@\n" + "HOST = @host@\n" + "BUILD = @build@\n"), ==, 0); + munit_assert_int(run_configure_full(NULL, + "--host=aarch64-linux-gnu " + "--build=x86_64-linux-gnu"), ==, 0); + mk = makefile_text(); + munit_assert_not_null(mk); + munit_assert_not_null(strstr(mk, "CROSS = yes")); + munit_assert_not_null(strstr(mk, "HOST = aarch64-linux-gnu")); + munit_assert_not_null(strstr(mk, "BUILD = x86_64-linux-gnu")); + free(mk); + + munit_assert_int(run_configure_full(NULL, NULL), ==, 0); + mk = makefile_text(); + munit_assert_not_null(mk); + munit_assert_not_null(strstr(mk, "CROSS = no")); + free(mk); + return MUNIT_OK; +} + +/* ---- todo 18 (f): unknown --* -> exit 1 + "unrecognized option" ------- */ + +static MunitResult +test_args_bogus(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_full(NULL, "--bogus"), !=, 0); + err = stderr_text(); + munit_assert_not_null(err); + munit_assert_not_null(strstr(err, "unrecognized option")); + munit_assert_not_null(strstr(err, "Usage:")); /* usage goes to stderr */ + free(err); + + munit_assert_int(run_configure_full(NULL, "--bogus=1"), !=, 0); + err = stderr_text(); + munit_assert_not_null(err); + munit_assert_not_null(strstr(err, "unrecognized option")); + free(err); + + /* --enable- with no name falls through to the unknown-option error */ + munit_assert_int(run_configure_full(NULL, "--enable-"), !=, 0); + err = stderr_text(); + munit_assert_not_null(err); + munit_assert_not_null(strstr(err, "unrecognized option")); + free(err); + return MUNIT_OK; +} + +/* ---- malformed values: missing/empty/option-looking --prefix ---------- */ + +static MunitResult +test_args_malformed(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); + + /* missing value (space form, end of argv) */ + munit_assert_int(run_configure_full(NULL, "--prefix"), !=, 0); + err = stderr_text(); + munit_assert_not_null(err); + munit_assert_not_null(strstr(err, "requires an argument")); + free(err); + + /* empty value */ + munit_assert_int(run_configure_full(NULL, "--prefix="), !=, 0); + err = stderr_text(); + munit_assert_not_null(err); + munit_assert_not_null(strstr(err, "non-empty argument")); + free(err); + + /* an option-looking value is rejected */ + munit_assert_int(run_configure_full(NULL, "--prefix --bogus"), !=, 0); + err = stderr_text(); + munit_assert_not_null(err); + munit_assert_not_null(strstr(err, "requires an argument")); + free(err); + return MUNIT_OK; +} + +/* ---- --prefix (space form) and --exec-prefix -------------------- */ + +static MunitResult +test_args_prefix_space(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("prefix = @prefix@\n" + "EP = @exec_prefix@\n"), ==, 0); + + munit_assert_int(run_configure_full(NULL, "--prefix /opt/space"), ==, 0); + mk = makefile_text(); + munit_assert_not_null(mk); + munit_assert_not_null(strstr(mk, "prefix = /opt/space")); + /* exec_prefix defaults to prefix when not given */ + munit_assert_not_null(strstr(mk, "EP = /opt/space")); + free(mk); + + munit_assert_int(run_configure_full(NULL, "--prefix '/opt/x y'"), ==, 0); + mk = makefile_text(); + munit_assert_not_null(mk); + munit_assert_not_null(strstr(mk, "prefix = /opt/x y")); + free(mk); + + munit_assert_int(run_configure_full(NULL, "--exec-prefix=/opt/ep"), ==, 0); + mk = makefile_text(); + munit_assert_not_null(mk); + munit_assert_not_null(strstr(mk, "EP = /opt/ep")); + munit_assert_not_null(strstr(mk, "prefix = /usr/local")); + free(mk); + return MUNIT_OK; +} + +/* ---- positional VAR=VALUE injection stays inert ----------------------- */ + +static MunitResult +test_args_injection(const MunitParameter params[], void *data) +{ + (void)params; + (void)data; + struct st_ext_ctx *ctx; + char pwned3[600]; + char pwned4[600]; + char *mk; + 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(pwned3, sizeof pwned3, "pwned3"), >, 0); + munit_assert_int(mkpath(pwned4, sizeof pwned4, "pwned4"), >, 0); + munit_assert_int(write_makefile_in("FLAGS = @CFLAGS@\n"), ==, 0); + + munit_assert_int(run_configure_full(NULL, "'CFLAGS=$(touch pwned3)'"), + ==, 0); + munit_assert_int(access(pwned3, F_OK), ==, -1); + mk = makefile_text(); + munit_assert_not_null(mk); + munit_assert_not_null(strstr(mk, "FLAGS = $(touch pwned3)")); + free(mk); + + munit_assert_int(run_configure_full(NULL, "'CFLAGS=`touch pwned4`'"), + ==, 0); + munit_assert_int(access(pwned4, F_OK), ==, -1); + mk = makefile_text(); + munit_assert_not_null(mk); + munit_assert_not_null(strstr(mk, "FLAGS = `touch pwned4`")); + free(mk); + + /* a non-identifier VAR name is a clean error */ + munit_assert_int(run_configure_full(NULL, "B-AD=1"), !=, 0); + err = stderr_text(); + munit_assert_not_null(err); + munit_assert_not_null(strstr(err, "invalid variable assignment")); + free(err); + return MUNIT_OK; +} + +/* ---- a hostile option NAME errors at GENERATION time ------------------ */ + +static MunitResult +test_args_option_name(const MunitParameter params[], void *data) +{ + (void)params; + (void)data; + static const char *const docsrc = + "project \"x\" version \"1.0\"\n" + "option \"x; rm -rf /\" default=#false\n" + "feature \"f\" { header \"unistd.h\" }\n"; + struct st_ext_ctx *ctx; + struct st_kdl_document *doc = NULL; + struct st_error *err = NULL; + char conf[600]; + + 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)); /* schema-valid, hostile name */ + munit_assert_int(mkpath(conf, sizeof conf, "configure"), >, 0); + err = st_gen_configure_emit_path(conf, doc, ctx); + munit_assert_not_null(err); + munit_assert_not_null(strstr(st_error_message(err), + "not a valid shell identifier")); + st_error_free(err); + err = NULL; + /* nothing was emitted: the truncated output is an empty file */ + { + char *bytes = slurp(conf); + + munit_assert_not_null(bytes); + munit_assert_string_equal(bytes, ""); + free(bytes); + } + st_kdl_document_free(doc); + st_ext_ctx_free(ctx); + return MUNIT_OK; +} + static MunitTest tests[] = { { "/gen/syntax", test_syntax, setup, teardown, MUNIT_TEST_OPTION_NONE, NULL }, @@ -1003,6 +1441,28 @@ static MunitTest tests[] = { teardown, MUNIT_TEST_OPTION_NONE, NULL }, { "/gen/config-status-help", test_config_status_help, setup, teardown, MUNIT_TEST_OPTION_NONE, NULL }, + { "/gen/args/syntax", test_args_syntax, setup, teardown, + MUNIT_TEST_OPTION_NONE, NULL }, + { "/gen/args/help", test_args_help, setup, teardown, + MUNIT_TEST_OPTION_NONE, NULL }, + { "/gen/args/version", test_args_version, setup, teardown, + MUNIT_TEST_OPTION_NONE, NULL }, + { "/gen/args/cc-override", test_args_cc_override, setup, teardown, + MUNIT_TEST_OPTION_NONE, NULL }, + { "/gen/args/enable-debug", test_args_enable_debug, setup, teardown, + MUNIT_TEST_OPTION_NONE, NULL }, + { "/gen/args/cross-compiling", test_args_cross_compiling, setup, teardown, + MUNIT_TEST_OPTION_NONE, NULL }, + { "/gen/args/bogus", test_args_bogus, setup, teardown, + MUNIT_TEST_OPTION_NONE, NULL }, + { "/gen/args/malformed", test_args_malformed, setup, teardown, + MUNIT_TEST_OPTION_NONE, NULL }, + { "/gen/args/prefix-space", test_args_prefix_space, setup, teardown, + MUNIT_TEST_OPTION_NONE, NULL }, + { "/gen/args/injection", test_args_injection, setup, teardown, + MUNIT_TEST_OPTION_NONE, NULL }, + { "/gen/args/option-name", test_args_option_name, setup, teardown, + MUNIT_TEST_OPTION_NONE, NULL }, { NULL, NULL, NULL, NULL, MUNIT_TEST_OPTION_NONE, NULL }, };