Template
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.
812 lines
26 KiB
C
812 lines
26 KiB
C
/*
|
|
* configure.c - the ./configure generator (todo 16).
|
|
*
|
|
* Assembles the complete POSIX-sh ./configure from a schema-validated DSL
|
|
* document and an extension ctx. See gen/configure.h for the full emitted
|
|
* layout contract, the substitution approach (split-on-@, documented), the
|
|
* LIBS accumulation rules, and the error categories.
|
|
*
|
|
* Two load-bearing safety decisions carry through from the detect chain:
|
|
* - Every user-supplied byte (feature names, check targets, registered
|
|
* var values, package names) reaches the emitted script either through
|
|
* st_sh_quote (single-quoted literal, inert) or through a
|
|
* validated-identifier position (have_<name>, `VAR="${VAR:-...}"`).
|
|
* - The substitution runs AT CONFIGURE TIME in shell, so the generated
|
|
* script errors on an unknown @VAR@ it encounters (the generator
|
|
* cannot know the user's Makefile.in contents); the value is carried
|
|
* verbatim through `printf '%s'` so nothing is ever re-interpreted.
|
|
*
|
|
* Copyright (c) 2026 huntedbytheirs
|
|
* SPDX-License-Identifier: BSD-3-Clause
|
|
*/
|
|
|
|
#include "gen/args.h"
|
|
#include "gen/config.h"
|
|
#include "gen/configure.h"
|
|
|
|
#include "cli.h" /* STUPIDTOOLS_VERSION (the generated-by version string) */
|
|
|
|
#include "detect/check_registry.h"
|
|
#include "detect/checks.h"
|
|
#include "detect/probe.h"
|
|
#include "detect/resolve.h"
|
|
#include "error.h"
|
|
#include "ext/abi.h"
|
|
#include "gen/sh_emit.h"
|
|
#include "kdl/ast.h"
|
|
#include "kdl/value.h"
|
|
|
|
#include <stdarg.h>
|
|
#include <stdbool.h>
|
|
#include <stddef.h>
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
|
|
/* ---- small helpers ----------------------------------------------------- */
|
|
|
|
/* True when `s` is a valid POSIX shell identifier: letter/underscore, then
|
|
* letters/digits/underscores, non-empty. Feature names and registered var
|
|
* names must pass this (they land in variable-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. Mirrors the value-model re-owning in
|
|
* checks.c (value failures become schema errors). */
|
|
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;
|
|
}
|
|
|
|
/* vsnprintf-based string builder (no reliance on POSIX strdup). */
|
|
static char *
|
|
strbuild(const char *fmt, ...)
|
|
{
|
|
va_list ap;
|
|
va_list ap2;
|
|
int n;
|
|
char *out;
|
|
|
|
va_start(ap, fmt);
|
|
va_copy(ap2, ap);
|
|
n = vsnprintf(NULL, 0, fmt, ap);
|
|
va_end(ap);
|
|
if (n < 0) {
|
|
va_end(ap2);
|
|
return NULL;
|
|
}
|
|
out = malloc((size_t)n + 1);
|
|
if (out == NULL) {
|
|
va_end(ap2);
|
|
return NULL;
|
|
}
|
|
(void)vsnprintf(out, (size_t)n + 1, fmt, ap2);
|
|
va_end(ap2);
|
|
return out;
|
|
}
|
|
|
|
/* Compare a node name against a literal, accepting any string form
|
|
* (identifiers directly; other forms via the value model). */
|
|
static struct st_error *
|
|
name_equals(const struct st_kdl_token_ref *name, const char *s, bool *out)
|
|
{
|
|
if (name->kind == ST_TOK_IDENT) {
|
|
*out = name->len == strlen(s) && memcmp(name->text, s, name->len) == 0;
|
|
return NULL;
|
|
}
|
|
{
|
|
struct st_kdl_value v;
|
|
struct st_error *e = st_kdl_value_from_token(name, &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;
|
|
}
|
|
}
|
|
|
|
/* ---- fixed shell text (emitted verbatim via st_sh_emit_str) ------------ */
|
|
|
|
/* The `%` in `printf '... %s ...'` is why these are emitted with
|
|
* st_sh_emit_str (verbatim), not fprintf (which would eat `%s`). */
|
|
|
|
static const char HOST_DETECT_PRE[] =
|
|
"\n"
|
|
"# --- host detection ---\n";
|
|
|
|
static const char HOST_DETECT_POST[] =
|
|
"st_os=$(uname -s)\n"
|
|
"st_os_norm\n"
|
|
"st_cc_id=unknown\n";
|
|
|
|
static const char TOOLCHAIN_DEFAULTS_PRE[] =
|
|
"\n"
|
|
"# --- toolchain defaults (registered variables) ---\n"
|
|
"# Each registered variable defaults to its generation-time value, and\n"
|
|
"# may be overridden by an environment variable of the same name (the\n"
|
|
"# `${VAR:-...}` form). Emitted BEFORE the cache boilerplate so the\n"
|
|
"# boilerplate's `:=` fallbacks never clobber these.\n";
|
|
|
|
static const char TOOLCHAIN_LIBS[] =
|
|
"# built-in link flags, accumulated by library/pkg_config probes\n"
|
|
"LIBS=\"\"\n";
|
|
|
|
static const char SUBST_PRE[] =
|
|
"\n"
|
|
"# --- substitution: Makefile.in -> Makefile ---\n"
|
|
"# The @VAR@ token set is the registered variables plus the built-ins\n"
|
|
"# LIBS, prefix, srcdir. Substitution runs at CONFIGURE time (the\n"
|
|
"# generator cannot know the user's Makefile.in), so an unknown @VAR@\n"
|
|
"# is a configure-time error naming the variable.\n"
|
|
"\n"
|
|
"# st_subst_lookup NAME: print the value of substitution variable NAME\n"
|
|
"# verbatim via printf '%s' (no metacharacter is re-interpreted), or\n"
|
|
"# return 1 when NAME is unknown.\n"
|
|
"st_subst_lookup() {\n"
|
|
" case \"$1\" in\n";
|
|
|
|
static const char SUBST_LOOKUP_TAIL[] =
|
|
" *) return 1 ;;\n"
|
|
" esac\n"
|
|
"}\n"
|
|
"\n"
|
|
"# st_subst_line LINE: echo LINE with every @VAR@ replaced by $VAR.\n"
|
|
"# Why not sed: its 's' replacement re-interprets '&', '\\' and the\n"
|
|
"# delimiter, so a value with those bytes is corrupted unless escaped -\n"
|
|
"# fragile. Why not ${var//...}: a bash/zsh/ksh extension, not POSIX.\n"
|
|
"# Instead the line is split on '@' with pure POSIX parameter expansion\n"
|
|
"# (${v%%'@'*}, ${v#*'@'}): a '@' only starts a substitution when a\n"
|
|
"# second '@' follows on the same line, so a lone '@' is literal; the\n"
|
|
"# name between the two must be known, else configure errors naming it.\n"
|
|
"# Values are carried verbatim through printf '%s', so '/', spaces,\n"
|
|
"# '&', '$' and backticks all survive byte-exact and never execute.\n"
|
|
"# (v1 limitation: a value may not END in a newline - command\n"
|
|
"# substitution strips trailing newlines.)\n"
|
|
"st_subst_line() {\n"
|
|
" st_rest=$1\n"
|
|
" st_out=\n"
|
|
" while :; do\n"
|
|
" case \"$st_rest\" in\n"
|
|
" *'@'*'@'*) : ;;\n"
|
|
" *)\n"
|
|
" st_out=\"$st_out$st_rest\"\n"
|
|
" break\n"
|
|
" ;;\n"
|
|
" esac\n"
|
|
" st_out=\"$st_out${st_rest%%'@'*}\"\n"
|
|
" st_after=${st_rest#*'@'}\n"
|
|
" st_name=${st_after%%'@'*}\n"
|
|
" st_rest=${st_after#*'@'}\n"
|
|
" if st_val=$(st_subst_lookup \"$st_name\"); then\n"
|
|
" st_out=\"$st_out$st_val\"\n"
|
|
" else\n"
|
|
" printf 'configure: error: unknown variable @%s@ in Makefile.in\\n' \"$st_name\" >&2\n"
|
|
" exit 1\n"
|
|
" fi\n"
|
|
" done\n"
|
|
" printf '%s\\n' \"$st_out\"\n"
|
|
"}\n"
|
|
"\n"
|
|
"# st_subst: read ./Makefile.in (the source directory; v1 cwd contract),\n"
|
|
"# substitute every @VAR@, write ./Makefile.\n"
|
|
"st_subst() {\n"
|
|
" if [ ! -f ./Makefile.in ]; then\n"
|
|
" printf '%s\\n' 'configure: error: Makefile.in not found' >&2\n"
|
|
" exit 1\n"
|
|
" fi\n"
|
|
" : > ./Makefile\n"
|
|
" while IFS= read -r st_line || [ -n \"$st_line\" ]; do\n"
|
|
" st_subst_line \"$st_line\" >> ./Makefile\n"
|
|
" done < ./Makefile.in\n"
|
|
"}\n"
|
|
"\n"
|
|
"st_subst\n";
|
|
|
|
/* ---- section emitters -------------------------------------------------- */
|
|
|
|
/* One library/pkg_config check's link-flags contribution. */
|
|
struct st_lib_entry {
|
|
enum st_check_kind kind; /* ST_CHECK_LIBRARY or ST_CHECK_PKG_CONFIG */
|
|
char *target; /* owned library/package name */
|
|
};
|
|
|
|
/* Emit the substitution lookup function's case arms. `names` are already
|
|
* validated identifiers, safe in variable-NAME positions. */
|
|
static struct st_error *
|
|
emit_subst_lookup(FILE *out, const char *const *names, size_t count)
|
|
{
|
|
size_t i;
|
|
|
|
if (st_sh_emit_str(out, SUBST_PRE) < 0) {
|
|
return st_error_io("I/O error emitting substitution section");
|
|
}
|
|
for (i = 0; i < count; i++) {
|
|
if (fprintf(out, " %s) printf '%%s' \"$%s\" ;;\n",
|
|
names[i], names[i]) < 0) {
|
|
return st_error_io("I/O error emitting substitution section");
|
|
}
|
|
}
|
|
if (st_sh_emit_str(out, SUBST_LOOKUP_TAIL) < 0) {
|
|
return st_error_io("I/O error emitting substitution section");
|
|
}
|
|
return NULL;
|
|
}
|
|
|
|
/* Emit the LIBS accumulation for one feature: for each library/pkg_config
|
|
* check, append the link flags when the feature resolved yes. Generic over
|
|
* the check kind; the pkg_config form is guarded by `command -v pkg-config`
|
|
* (v1). */
|
|
static struct st_error *
|
|
emit_libs_accumulation(FILE *out, const char *feature,
|
|
const struct st_lib_entry *libs, size_t count)
|
|
{
|
|
size_t i;
|
|
|
|
for (i = 0; i < count; i++) {
|
|
char *q;
|
|
|
|
if (libs[i].kind == ST_CHECK_LIBRARY) {
|
|
q = st_sh_quote(libs[i].target);
|
|
if (q == NULL) {
|
|
return st_error_internal("out of memory quoting library name");
|
|
}
|
|
/* `-l` inside the double quotes, the st_sh_quote'd name OUTSIDE
|
|
* them (single quotes are literal inside double quotes, which
|
|
* would turn -l'pthread' into a literal-quoted flag). */
|
|
if (fprintf(out,
|
|
"if [ \"$have_%s\" = \"yes\" ]; then "
|
|
"LIBS=\"$LIBS -l\"%s; fi\n",
|
|
feature, q) < 0) {
|
|
free(q);
|
|
return st_error_io("I/O error emitting LIBS accumulation");
|
|
}
|
|
free(q);
|
|
} else { /* ST_CHECK_PKG_CONFIG */
|
|
q = st_sh_quote(libs[i].target);
|
|
if (q == NULL) {
|
|
return st_error_internal("out of memory quoting package name");
|
|
}
|
|
/* v1 pkg-config guard: only accumulate when pkg-config exists. */
|
|
if (fprintf(out,
|
|
"if command -v pkg-config >/dev/null 2>&1; then\n"
|
|
" if [ \"$have_%s\" = \"yes\" ]; then "
|
|
"LIBS=\"$LIBS $(pkg-config --libs %s)\"; fi\n"
|
|
"fi\n",
|
|
feature, q) < 0) {
|
|
free(q);
|
|
return st_error_io("I/O error emitting LIBS accumulation");
|
|
}
|
|
free(q);
|
|
}
|
|
}
|
|
if (ferror(out)) {
|
|
return st_error_io("I/O error emitting LIBS accumulation");
|
|
}
|
|
return NULL;
|
|
}
|
|
|
|
/* Emit ONE feature node: its probe snippets, its resolution (aggregation +
|
|
* when-gate), then its LIBS accumulation. */
|
|
static struct st_error *
|
|
emit_feature(FILE *out, const struct st_kdl_node *node)
|
|
{
|
|
struct st_error *err = NULL;
|
|
struct st_when_ast *guard = NULL;
|
|
char *fname = NULL;
|
|
const struct st_kdl_node *child;
|
|
const struct st_kdl_prop *prop;
|
|
size_t n_checks = 0;
|
|
size_t i = 0;
|
|
char **check_names = NULL;
|
|
struct st_check_probe *probes = NULL;
|
|
struct st_lib_entry *libs = NULL;
|
|
size_t n_libs = 0;
|
|
|
|
/* the feature name (must be a valid shell identifier) */
|
|
if (node->args == NULL) {
|
|
return err_at_owned(node->name.span, "feature requires a name");
|
|
}
|
|
err = extract_string(&node->args->value, "feature name", &fname);
|
|
if (err != NULL) {
|
|
return err;
|
|
}
|
|
if (!valid_ident(fname)) {
|
|
char msg[192];
|
|
|
|
snprintf(msg, sizeof msg,
|
|
"feature name '%s' is not a valid shell identifier "
|
|
"(have_<name> requires [A-Za-z_][A-Za-z0-9_]*)", fname);
|
|
err = err_at_owned(node->args->value.span, msg);
|
|
goto done;
|
|
}
|
|
|
|
/* count checks */
|
|
for (child = node->children; child != NULL; child = child->next) {
|
|
n_checks++;
|
|
}
|
|
|
|
if (n_checks > 0) {
|
|
check_names = calloc(n_checks, sizeof(*check_names));
|
|
probes = calloc(n_checks, sizeof(*probes));
|
|
libs = calloc(n_checks, sizeof(*libs));
|
|
if (check_names == NULL || probes == NULL || libs == NULL) {
|
|
err = st_error_internal("out of memory building feature");
|
|
goto done;
|
|
}
|
|
}
|
|
|
|
/* build every check's probe + checkname, collecting lib/pkg_config */
|
|
for (child = node->children; child != NULL; child = child->next) {
|
|
enum st_check_kind kind;
|
|
char namebuf[128];
|
|
|
|
kind = st_check_kind_from_node(child, &err);
|
|
if (err != NULL) {
|
|
goto done;
|
|
}
|
|
if (st_resolve_check_name(fname, i, n_checks, namebuf,
|
|
sizeof namebuf) != 0) {
|
|
err = st_error_internal("failed to derive check name");
|
|
goto done;
|
|
}
|
|
check_names[i] = strbuild("%s", namebuf);
|
|
if (check_names[i] == NULL) {
|
|
err = st_error_internal("out of memory deriving check name");
|
|
goto done;
|
|
}
|
|
err = st_check_probe_build(kind, child, &probes[i]);
|
|
if (err != NULL) {
|
|
goto done;
|
|
}
|
|
if (kind == ST_CHECK_LIBRARY || kind == ST_CHECK_PKG_CONFIG) {
|
|
libs[n_libs].kind = kind;
|
|
err = extract_string(&child->args->value,
|
|
kind == ST_CHECK_LIBRARY
|
|
? "library check argument"
|
|
: "pkg_config check argument",
|
|
&libs[n_libs].target);
|
|
if (err != NULL) {
|
|
goto done;
|
|
}
|
|
n_libs++;
|
|
}
|
|
i++;
|
|
}
|
|
|
|
/* emit the probe snippets (todo 12), in document order */
|
|
for (i = 0; i < n_checks; i++) {
|
|
err = st_probe_emit_snippet(check_names[i], &probes[i], out);
|
|
if (err != NULL) {
|
|
goto done;
|
|
}
|
|
}
|
|
|
|
/* the `when` guard (optional feature property) */
|
|
for (prop = node->props; prop != NULL; prop = prop->next) {
|
|
bool is_when = false;
|
|
|
|
err = name_equals(&prop->key, "when", &is_when);
|
|
if (err != NULL) {
|
|
goto done;
|
|
}
|
|
if (is_when) {
|
|
char *guard_str = NULL;
|
|
|
|
err = extract_string(&prop->value, "feature 'when'", &guard_str);
|
|
if (err != NULL) {
|
|
goto done;
|
|
}
|
|
err = st_when_parse_at(guard_str, &node->name.span, &guard);
|
|
free(guard_str);
|
|
if (err != NULL) {
|
|
goto done;
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
|
|
/* feature resolution: aggregation + when-gate (todo 14) */
|
|
{
|
|
struct st_resolve_feature f;
|
|
|
|
f.name = fname;
|
|
f.check_names = (const char *const *)check_names;
|
|
f.check_count = n_checks;
|
|
f.guard = guard;
|
|
err = st_resolve_emit_feature(out, &f);
|
|
if (err != NULL) {
|
|
goto done;
|
|
}
|
|
}
|
|
|
|
/* LIBS accumulation (library/pkg_config checks), gated on have_<name> */
|
|
err = emit_libs_accumulation(out, fname, libs, n_libs);
|
|
if (err != NULL) {
|
|
goto done;
|
|
}
|
|
|
|
done:
|
|
for (i = 0; i < n_checks; i++) {
|
|
st_check_probe_free(&probes[i]);
|
|
free(check_names != NULL ? check_names[i] : NULL);
|
|
}
|
|
for (i = 0; i < n_libs; i++) {
|
|
free(libs[i].target);
|
|
}
|
|
free(check_names);
|
|
free(probes);
|
|
free(libs);
|
|
st_when_free(guard);
|
|
free(fname);
|
|
return err;
|
|
}
|
|
|
|
/* Append `name` (owned) to the feature-name list, growing it as needed.
|
|
* Used by the probe-section loop to collect the names the todo-17 results
|
|
* summary + config.h defines are emitted from. */
|
|
static struct st_error *
|
|
collect_feature_name(char ***names, size_t *count, size_t *cap, char *name)
|
|
{
|
|
if (*count == *cap) {
|
|
size_t ncap = *cap == 0 ? 8 : *cap * 2;
|
|
char **nn = realloc(*names, ncap * sizeof(*nn));
|
|
|
|
if (nn == NULL) {
|
|
return st_error_internal("out of memory collecting feature "
|
|
"names");
|
|
}
|
|
*names = nn;
|
|
*cap = ncap;
|
|
}
|
|
(*names)[(*count)++] = name;
|
|
return NULL;
|
|
}
|
|
|
|
/* ---- the public API ---------------------------------------------------- */
|
|
|
|
struct st_error *
|
|
st_gen_configure_emit(FILE *out, const struct st_kdl_document *doc,
|
|
struct st_ext_ctx *ctx)
|
|
{
|
|
struct st_registry *reg;
|
|
size_t n_reg;
|
|
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;
|
|
size_t n_feat = 0;
|
|
size_t feat_cap = 0;
|
|
|
|
if (out == NULL || doc == NULL || ctx == NULL) {
|
|
return st_error_usage("st_gen_configure_emit: NULL argument");
|
|
}
|
|
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) {
|
|
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/exec_prefix/srcdir/host/build/cross_compiling
|
|
* + one enable_<name> 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) {
|
|
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);
|
|
|
|
if (!valid_ident(name)) {
|
|
char msg[192];
|
|
|
|
snprintf(msg, sizeof msg,
|
|
"registered variable name '%s' is not a valid shell "
|
|
"identifier", name != NULL ? name : "?");
|
|
err = st_error_usage(msg);
|
|
goto done;
|
|
}
|
|
subst_names[i] = (char *)name;
|
|
}
|
|
subst_names[n_reg + 0] = "LIBS";
|
|
subst_names[n_reg + 1] = "prefix";
|
|
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 ||
|
|
st_sh_emit_comment(out,
|
|
"Generated by stupidtools " STUPIDTOOLS_VERSION
|
|
" - DO NOT EDIT") < 0) {
|
|
err = st_error_io("I/O error emitting configure header");
|
|
goto done;
|
|
}
|
|
|
|
/* 2. preamble: full argument parsing (todo 18) */
|
|
err = st_gen_args_emit(out, &args);
|
|
if (err != NULL) {
|
|
goto done;
|
|
}
|
|
|
|
/* 3. config.log init + original-args capture (todo 17) */
|
|
err = st_gen_config_log_init_emit(out);
|
|
if (err != NULL) {
|
|
goto done;
|
|
}
|
|
|
|
/* 4. host detection */
|
|
if (st_sh_emit_str(out, HOST_DETECT_PRE) < 0) {
|
|
err = st_error_io("I/O error emitting host detection");
|
|
goto done;
|
|
}
|
|
err = st_resolve_emit_os_norm(out);
|
|
if (err != NULL) {
|
|
goto done;
|
|
}
|
|
if (st_sh_emit_str(out, HOST_DETECT_POST) < 0) {
|
|
err = st_error_io("I/O error emitting host detection");
|
|
goto done;
|
|
}
|
|
|
|
/* 5. toolchain defaults (before the cache boilerplate; see the header) */
|
|
if (st_sh_emit_str(out, TOOLCHAIN_DEFAULTS_PRE) < 0) {
|
|
err = st_error_io("I/O error emitting toolchain defaults");
|
|
goto done;
|
|
}
|
|
for (i = 0; i < n_reg; i++) {
|
|
const char *name = st_registry_var_name(reg, i);
|
|
const char *value = st_registry_var_value(reg, i);
|
|
char *q = st_sh_quote(value != NULL ? value : "");
|
|
|
|
if (q == NULL) {
|
|
err = st_error_internal("out of memory quoting registered "
|
|
"variable");
|
|
goto done;
|
|
}
|
|
/* No outer double quotes: inside "${VAR:-...}" the single quotes
|
|
* of a st_sh_quote'd default are LITERAL, so an empty default would
|
|
* become the two-character string "''". The unquoted ${VAR:-'...'}
|
|
* form is safe because assignment RHS is not field-split or globbed
|
|
* and quote-removal strips the single quotes, yielding the exact
|
|
* registry value (spaces/&/$ preserved). */
|
|
if (fprintf(out, "%s=${%s:-%s}\n", name, name, q) < 0) {
|
|
free(q);
|
|
err = st_error_io("I/O error emitting toolchain defaults");
|
|
goto done;
|
|
}
|
|
free(q);
|
|
}
|
|
if (st_sh_emit_str(out, TOOLCHAIN_LIBS) < 0) {
|
|
err = st_error_io("I/O error emitting toolchain defaults");
|
|
goto done;
|
|
}
|
|
|
|
/* 6. cache boilerplate (todo 12) */
|
|
err = st_probe_emit_cache_functions(out);
|
|
if (err != NULL) {
|
|
goto done;
|
|
}
|
|
|
|
/* 7. probe section: each top-level `feature` node in document order.
|
|
* The resolved feature names are collected here for the todo-17
|
|
* results summary + config.h defines, emitted right after the loop. */
|
|
for (node = doc->nodes; node != NULL; node = node->next) {
|
|
bool is_feature = false;
|
|
char *fname = NULL;
|
|
|
|
err = name_equals(&node->name, "feature", &is_feature);
|
|
if (err != NULL) {
|
|
goto done;
|
|
}
|
|
if (!is_feature) {
|
|
continue;
|
|
}
|
|
err = emit_feature(out, node);
|
|
if (err != NULL) {
|
|
goto done;
|
|
}
|
|
/* a successful emit_feature guarantees the name is a valid shell
|
|
* identifier (it errors otherwise), so the have_<name> /
|
|
* HAVE_<NAME> interpolation below is safe */
|
|
err = extract_string(&node->args->value, "feature name", &fname);
|
|
if (err != NULL) {
|
|
goto done;
|
|
}
|
|
err = collect_feature_name(&feat_names, &n_feat, &feat_cap, fname);
|
|
if (err != NULL) {
|
|
free(fname);
|
|
goto done;
|
|
}
|
|
}
|
|
|
|
/* 8. probe-results summary + config.h (todo 17) */
|
|
err = st_gen_config_results_emit(out, (const char *const *)feat_names,
|
|
n_feat);
|
|
if (err != NULL) {
|
|
goto done;
|
|
}
|
|
err = st_gen_config_h_emit(out, (const char *const *)feat_names, n_feat);
|
|
if (err != NULL) {
|
|
goto done;
|
|
}
|
|
|
|
/* 9. substitution section */
|
|
err = emit_subst_lookup(out, (const char *const *)subst_names, n_subst);
|
|
if (err != NULL) {
|
|
goto done;
|
|
}
|
|
|
|
/* 10. config.status (todo 17): written after substitution, before the
|
|
* final exit 0 */
|
|
err = st_gen_config_status_emit(out);
|
|
if (err != NULL) {
|
|
goto done;
|
|
}
|
|
if (st_sh_emit_str(out, "\nexit 0\n") < 0) {
|
|
err = st_error_io("I/O error emitting configure");
|
|
goto done;
|
|
}
|
|
|
|
/* 11. final stream check */
|
|
if (ferror(out)) {
|
|
err = st_error_io("I/O error emitting configure");
|
|
goto done;
|
|
}
|
|
|
|
done:
|
|
for (i = 0; i < n_feat; i++) {
|
|
free(feat_names[i]);
|
|
}
|
|
free(feat_names);
|
|
/* 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);
|
|
st_gen_args_free(&args);
|
|
return err;
|
|
}
|
|
|
|
struct st_error *
|
|
st_gen_configure_emit_path(const char *path, const struct st_kdl_document *doc,
|
|
struct st_ext_ctx *ctx)
|
|
{
|
|
FILE *f;
|
|
struct st_error *err;
|
|
|
|
if (path == NULL) {
|
|
return st_error_usage("st_gen_configure_emit_path: NULL path");
|
|
}
|
|
f = fopen(path, "w");
|
|
if (f == NULL) {
|
|
return st_error_io("cannot open output file for writing");
|
|
}
|
|
err = st_gen_configure_emit(f, doc, ctx);
|
|
if (fclose(f) != 0 && err == NULL) {
|
|
err = st_error_io("I/O error closing output file");
|
|
}
|
|
return err;
|
|
}
|