Template
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-<name>/--disable-<name> (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_<name> 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.
This commit is contained in:
+655
@@ -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_<name>, --enable-<name>);
|
||||||
|
* - 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 <stdbool.h>
|
||||||
|
#include <stddef.h>
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <string.h>
|
||||||
|
|
||||||
|
/* ---- 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_<name> / --enable-<name> 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 "<name>" version "<semver>"` 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-<name> 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-<name>/--disable-<name> 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_<name>=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_<name> 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;
|
||||||
|
}
|
||||||
+142
@@ -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=<dir>, --exec-prefix=<dir>, --host=<triplet>,
|
||||||
|
* --build=<triplet>), the generic VAR=VALUE override, the per-option
|
||||||
|
* enable/disable flags (--enable-<name> and --disable-<name>), and
|
||||||
|
* --help/--version - then exit 0.
|
||||||
|
* - `--version`: the DSL project's name + version (the project node),
|
||||||
|
* exit 0.
|
||||||
|
* - `--prefix=<dir>` / `--prefix <dir>` (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-<name>` / `--disable-<name>` setting
|
||||||
|
* `enable_<name>=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-<name>` / `--disable-<name>` and the
|
||||||
|
* shell variable `enable_<name>` (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-<x>` / `--without-<x>` 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_<name>=<default> 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 <stdbool.h>
|
||||||
|
#include <stddef.h>
|
||||||
|
#include <stdio.h>
|
||||||
|
|
||||||
|
struct st_error;
|
||||||
|
struct st_kdl_document;
|
||||||
|
|
||||||
|
/* One collected DSL option: the --enable-<name>/--disable-<name> 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 "<name>" */
|
||||||
|
char *project_version; /* project ... version "<semver>" */
|
||||||
|
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 */
|
||||||
+40
-42
@@ -20,6 +20,7 @@
|
|||||||
* SPDX-License-Identifier: BSD-3-Clause
|
* SPDX-License-Identifier: BSD-3-Clause
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
#include "gen/args.h"
|
||||||
#include "gen/config.h"
|
#include "gen/config.h"
|
||||||
#include "gen/configure.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
|
/* The `%` in `printf '... %s ...'` is why these are emitted with
|
||||||
* st_sh_emit_str (verbatim), not fprintf (which would eat `%s`). */
|
* 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[] =
|
static const char HOST_DETECT_PRE[] =
|
||||||
"\n"
|
"\n"
|
||||||
"# --- host detection ---\n";
|
"# --- host detection ---\n";
|
||||||
@@ -601,6 +569,7 @@ st_gen_configure_emit(FILE *out, const struct st_kdl_document *doc,
|
|||||||
size_t i;
|
size_t i;
|
||||||
struct st_error *err = NULL;
|
struct st_error *err = NULL;
|
||||||
const struct st_kdl_node *node;
|
const struct st_kdl_node *node;
|
||||||
|
struct st_gen_args args;
|
||||||
char **subst_names = NULL;
|
char **subst_names = NULL;
|
||||||
size_t n_subst = 0;
|
size_t n_subst = 0;
|
||||||
char **feat_names = NULL;
|
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) {
|
if (out == NULL || doc == NULL || ctx == NULL) {
|
||||||
return st_error_usage("st_gen_configure_emit: NULL argument");
|
return st_error_usage("st_gen_configure_emit: NULL argument");
|
||||||
}
|
}
|
||||||
|
memset(&args, 0, sizeof args);
|
||||||
|
err = st_gen_args_collect(doc, &args);
|
||||||
|
if (err != NULL) {
|
||||||
|
goto done;
|
||||||
|
}
|
||||||
reg = st_ext_var_registry(ctx);
|
reg = st_ext_var_registry(ctx);
|
||||||
if (reg == NULL) {
|
if (reg == NULL) {
|
||||||
return st_error_usage("st_gen_configure_emit: ctx has no variable "
|
err = st_error_usage("st_gen_configure_emit: ctx has no variable "
|
||||||
"registry");
|
"registry");
|
||||||
|
goto done;
|
||||||
}
|
}
|
||||||
n_reg = st_registry_var_count(reg);
|
n_reg = st_registry_var_count(reg);
|
||||||
|
|
||||||
/* collect the full @VAR@ substitution set: registered vars + the
|
/* collect the full @VAR@ substitution set: registered vars + the
|
||||||
* built-ins LIBS/prefix/srcdir */
|
* built-ins LIBS/prefix/exec_prefix/srcdir/host/build/cross_compiling
|
||||||
n_subst = n_reg + 3;
|
* + one enable_<name> per DSL option (todo 18) */
|
||||||
|
n_subst = n_reg + 7 + args.option_count;
|
||||||
subst_names = calloc(n_subst, sizeof(*subst_names));
|
subst_names = calloc(n_subst, sizeof(*subst_names));
|
||||||
if (subst_names == NULL) {
|
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++) {
|
for (i = 0; i < n_reg; i++) {
|
||||||
const char *name = st_registry_var_name(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 + 0] = "LIBS";
|
||||||
subst_names[n_reg + 1] = "prefix";
|
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 */
|
/* 1. shebang + header comment */
|
||||||
if (st_sh_emit_str(out, "#!/bin/sh\n") < 0 ||
|
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;
|
goto done;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 2. preamble (minimal args; todo 18 replaces it) */
|
/* 2. preamble: full argument parsing (todo 18) */
|
||||||
if (st_sh_emit_str(out, PREAMBLE) < 0) {
|
err = st_gen_args_emit(out, &args);
|
||||||
err = st_error_io("I/O error emitting preamble");
|
if (err != NULL) {
|
||||||
goto done;
|
goto done;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -787,7 +777,15 @@ done:
|
|||||||
free(feat_names[i]);
|
free(feat_names[i]);
|
||||||
}
|
}
|
||||||
free(feat_names);
|
free(feat_names);
|
||||||
|
/* the enable_<name> 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);
|
free(subst_names);
|
||||||
|
st_gen_args_free(&args);
|
||||||
return err;
|
return err;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+35
-18
@@ -11,17 +11,31 @@
|
|||||||
* GENERATED SCRIPT LAYOUT (the contract todos 17/18/23 consume)
|
* GENERATED SCRIPT LAYOUT (the contract todos 17/18/23 consume)
|
||||||
* -------------------------------------------------------------
|
* -------------------------------------------------------------
|
||||||
* 1. `#!/bin/sh` + a header comment (generated-by, version).
|
* 1. `#!/bin/sh` + a header comment (generated-by, version).
|
||||||
* 2. PREAMBLE — v1 MINIMAL argument handling, structurally marked with a
|
* 2. PREAMBLE — FULL argument handling (todo 18, emitted by
|
||||||
* `# --- PREAMBLE ---` comment so todo 18 replaces it whole:
|
* src/gen/args.h's st_gen_args_emit from the collected DSL project +
|
||||||
* - `--prefix=<dir>` stores $prefix (default /usr/local);
|
* option nodes): the `# --- PREAMBLE ---` section sets
|
||||||
* - `--host=<triplet>` / `--build=<triplet>` store $host / $build;
|
* prefix=/usr/local, exec_prefix=, srcdir=., build=, host=,
|
||||||
* $cross_compiling=yes when --host differs from --build (a simple
|
* cross_compiling=no and enable_<name>=<default> per DSL option,
|
||||||
* string comparison; both must be set), else no;
|
* defines st_usage(), then parses "$@" (READ-ONLY — the loop never
|
||||||
* - `VAR=VALUE` overrides are documented as already being environment
|
* shifts, so st_orig_args stays complete for section 3):
|
||||||
* variables when configure runs (the invoking shell exported them);
|
* - `-h`/`--help`: usage on stdout, exit 0;
|
||||||
* the positional form is accepted-but-ignored until todo 18;
|
* - `--version`: project name + version, exit 0;
|
||||||
* - an unknown `--*` prints "unrecognized option" to stderr and
|
* - `--prefix` / `--exec-prefix` / `--host` / `--build` in both
|
||||||
* exits 1.
|
* `--opt=value` and `--opt value` forms (a missing, empty or
|
||||||
|
* option-looking value errors with usage on stderr, exit 1);
|
||||||
|
* - per-option `--enable-<name>` / `--disable-<name>` setting
|
||||||
|
* enable_<name>=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
|
* 3. CONFIG.LOG INIT (todo 17) — capture the original invocation
|
||||||
* (st_orig_args="$@", st_configure_path=$0, plus single-quote-escaped
|
* (st_orig_args="$@", st_configure_path=$0, plus single-quote-escaped
|
||||||
* copies built with sed for the config.status bake-in) and
|
* copies built with sed for the config.status bake-in) and
|
||||||
@@ -59,8 +73,9 @@
|
|||||||
* safe.
|
* safe.
|
||||||
* 9. SUBSTITUTION: reads ./Makefile.in (cwd, the v1 source-dir contract)
|
* 9. SUBSTITUTION: reads ./Makefile.in (cwd, the v1 source-dir contract)
|
||||||
* and replaces every `@VAR@` (VAR = registered vars ∪ {LIBS, prefix,
|
* and replaces every `@VAR@` (VAR = registered vars ∪ {LIBS, prefix,
|
||||||
* srcdir}) with the shell variable's value, writing ./Makefile. An
|
* exec_prefix, srcdir, host, build, cross_compiling} ∪ {enable_<name>
|
||||||
* UNRESOLVED `@SOMETHING@` is a CONFIGURE-TIME error naming the
|
* 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
|
* variable (the generator cannot know the user's Makefile.in), reported
|
||||||
* after substitution via the per-line lookup; a missing Makefile.in is
|
* after substitution via the per-line lookup; a missing Makefile.in is
|
||||||
* a clean error. See the SUBSTITUTION approach note below.
|
* a clean error. See the SUBSTITUTION approach note below.
|
||||||
@@ -98,11 +113,13 @@
|
|||||||
* ------
|
* ------
|
||||||
* All functions return NULL on success or an owned st_error. Failures are
|
* 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
|
* 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
|
* in the same block as the error, the parser.c pattern) for a feature or
|
||||||
* that is not a valid POSIX shell identifier; ST_ERR_INTERNAL for quoting
|
* option name that is not a valid POSIX shell identifier (option errors
|
||||||
* allocation failures; ST_ERR_IO for stream write failures or (emit_path)
|
* come from st_gen_args_collect, src/gen/args.h); ST_ERR_INTERNAL for
|
||||||
* an unopenable path. The document is only read, never modified or freed;
|
* quoting allocation failures; ST_ERR_IO for stream write failures or
|
||||||
* the ctx is only read (its registry values are borrowed).
|
* (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
|
* Copyright (c) 2026 huntedbytheirs
|
||||||
* SPDX-License-Identifier: BSD-3-Clause
|
* SPDX-License-Identifier: BSD-3-Clause
|
||||||
|
|||||||
@@ -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
|
#ifndef _POSIX_C_SOURCE
|
||||||
#define _POSIX_C_SOURCE 200809L /* mkdtemp/unsetenv/setenv (POSIX.1-2008) */
|
#define _POSIX_C_SOURCE 200809L /* mkdtemp/unsetenv/setenv (POSIX.1-2008) */
|
||||||
#endif
|
#endif
|
||||||
@@ -181,6 +181,31 @@ run_configure(const char *args)
|
|||||||
return WEXITSTATUS(spawned);
|
return WEXITSTATUS(spawned);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Run "cd temp_dir && <env>sh ./configure <args>" 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);
|
||||||
|
}
|
||||||
|
|
||||||
/* "<shell> -n <path>": the shell's exit status or -1. */
|
/* "<shell> -n <path>": the shell's exit status or -1. */
|
||||||
static int
|
static int
|
||||||
syntax_check(const char *shell, const char *path)
|
syntax_check(const char *shell, const char *path)
|
||||||
@@ -235,6 +260,14 @@ slurp_named(const char *name)
|
|||||||
return slurp(path);
|
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/<name> (a missing file is not an error: the stale_state
|
/* 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. */
|
* checks remove outputs that may or may not exist). Returns 0 on success. */
|
||||||
static int
|
static int
|
||||||
@@ -968,6 +1001,411 @@ test_config_status_help(const MunitParameter params[], void *data)
|
|||||||
return MUNIT_OK;
|
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 <dir> (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[] = {
|
static MunitTest tests[] = {
|
||||||
{ "/gen/syntax", test_syntax, setup, teardown, MUNIT_TEST_OPTION_NONE,
|
{ "/gen/syntax", test_syntax, setup, teardown, MUNIT_TEST_OPTION_NONE,
|
||||||
NULL },
|
NULL },
|
||||||
@@ -1003,6 +1441,28 @@ static MunitTest tests[] = {
|
|||||||
teardown, MUNIT_TEST_OPTION_NONE, NULL },
|
teardown, MUNIT_TEST_OPTION_NONE, NULL },
|
||||||
{ "/gen/config-status-help", test_config_status_help, setup, teardown,
|
{ "/gen/config-status-help", test_config_status_help, setup, teardown,
|
||||||
MUNIT_TEST_OPTION_NONE, NULL },
|
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 },
|
{ NULL, NULL, NULL, NULL, MUNIT_TEST_OPTION_NONE, NULL },
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user