Template
feat(gen): assemble configure script with substitution
This commit is contained in:
@@ -0,0 +1,743 @@
|
||||
/*
|
||||
* 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/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 PREAMBLE[] =
|
||||
"\n"
|
||||
"# --- PREAMBLE ---\n"
|
||||
"# v1 MINIMAL argument handling. todo 18 replaces this whole section\n"
|
||||
"# with the full --help / --enable-*/--with-*/VAR=VALUE parser.\n"
|
||||
"prefix=/usr/local\n"
|
||||
"srcdir=.\n"
|
||||
"build=\n"
|
||||
"host=\n"
|
||||
"cross_compiling=no\n"
|
||||
"\n"
|
||||
"# VAR=VALUE overrides reach configure as environment variables (the\n"
|
||||
"# invoking shell exported them, e.g. `CC=clang ./configure`), so v1\n"
|
||||
"# needs no handling here. The positional form (`./configure CC=clang`)\n"
|
||||
"# is accepted but ignored until todo 18 exports it.\n"
|
||||
"for st_arg in \"$@\"; do\n"
|
||||
" case \"$st_arg\" in\n"
|
||||
" --prefix=*) prefix=${st_arg#--prefix=} ;;\n"
|
||||
" --build=*) build=${st_arg#--build=} ;;\n"
|
||||
" --host=*) host=${st_arg#--host=} ;;\n"
|
||||
" *=*) : ;;\n"
|
||||
" *)\n"
|
||||
" printf 'configure: error: unrecognized option %s\\n' \"$st_arg\" >&2\n"
|
||||
" exit 1\n"
|
||||
" ;;\n"
|
||||
" esac\n"
|
||||
"done\n"
|
||||
"\n"
|
||||
"# cross-compiling when --host differs from --build (both set)\n"
|
||||
"if [ -n \"$host\" ] && [ \"$host\" != \"$build\" ]; then\n"
|
||||
" cross_compiling=yes\n"
|
||||
"fi\n";
|
||||
|
||||
static const char HOST_DETECT_PRE[] =
|
||||
"\n"
|
||||
"# --- host detection ---\n";
|
||||
|
||||
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"
|
||||
"\n"
|
||||
"exit 0\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;
|
||||
}
|
||||
|
||||
/* ---- 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;
|
||||
char **subst_names = NULL;
|
||||
size_t n_subst = 0;
|
||||
|
||||
if (out == NULL || doc == NULL || ctx == NULL) {
|
||||
return st_error_usage("st_gen_configure_emit: NULL argument");
|
||||
}
|
||||
reg = st_ext_var_registry(ctx);
|
||||
if (reg == NULL) {
|
||||
return st_error_usage("st_gen_configure_emit: ctx has no variable "
|
||||
"registry");
|
||||
}
|
||||
n_reg = st_registry_var_count(reg);
|
||||
|
||||
/* collect the full @VAR@ substitution set: registered vars + the
|
||||
* built-ins LIBS/prefix/srcdir */
|
||||
n_subst = n_reg + 3;
|
||||
subst_names = calloc(n_subst, sizeof(*subst_names));
|
||||
if (subst_names == NULL) {
|
||||
return st_error_internal("out of memory building substitution set");
|
||||
}
|
||||
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] = "srcdir";
|
||||
|
||||
/* 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 (minimal args; todo 18 replaces it) */
|
||||
if (st_sh_emit_str(out, PREAMBLE) < 0) {
|
||||
err = st_error_io("I/O error emitting preamble");
|
||||
goto done;
|
||||
}
|
||||
|
||||
/* 3. 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;
|
||||
}
|
||||
|
||||
/* 4. 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;
|
||||
}
|
||||
|
||||
/* 5. cache boilerplate (todo 12) */
|
||||
err = st_probe_emit_cache_functions(out);
|
||||
if (err != NULL) {
|
||||
goto done;
|
||||
}
|
||||
|
||||
/* 6. probe section: each top-level `feature` node in document order */
|
||||
for (node = doc->nodes; node != NULL; node = node->next) {
|
||||
bool is_feature = false;
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
/* 7. substitution section */
|
||||
err = emit_subst_lookup(out, (const char *const *)subst_names, n_subst);
|
||||
if (err != NULL) {
|
||||
goto done;
|
||||
}
|
||||
|
||||
/* 8. exit 0 is already the last line of the substitution tail */
|
||||
if (ferror(out)) {
|
||||
err = st_error_io("I/O error emitting configure");
|
||||
goto done;
|
||||
}
|
||||
|
||||
done:
|
||||
free(subst_names);
|
||||
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;
|
||||
}
|
||||
Reference in New Issue
Block a user