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;
|
||||||
|
}
|
||||||
@@ -0,0 +1,116 @@
|
|||||||
|
/*
|
||||||
|
* configure.h - the ./configure generator (todo 16).
|
||||||
|
*
|
||||||
|
* THE INTEGRATION TODO: assembles everything the detect chain (todos
|
||||||
|
* 9/10/11/12/14) and the sh-emitter (15) built into a WORKING generated
|
||||||
|
* ./configure. Given a schema-validated DSL document (src/kdl/schema.h)
|
||||||
|
* plus an extension context (src/ext/abi.h) whose variable registry the
|
||||||
|
* C/C++ language modules have already populated, it EMITS a complete
|
||||||
|
* POSIX-sh ./configure to a FILE.
|
||||||
|
*
|
||||||
|
* GENERATED SCRIPT LAYOUT (the contract todos 17/18/23 consume)
|
||||||
|
* -------------------------------------------------------------
|
||||||
|
* 1. `#!/bin/sh` + a header comment (generated-by, version).
|
||||||
|
* 2. PREAMBLE — v1 MINIMAL argument handling, structurally marked with a
|
||||||
|
* `# === preamble ===` comment so todo 18 replaces it whole:
|
||||||
|
* - `--prefix=<dir>` stores $prefix (default /usr/local);
|
||||||
|
* - `--host=<triplet>` / `--build=<triplet>` store $host / $build;
|
||||||
|
* $cross_compiling=yes when --host differs from --build (a simple
|
||||||
|
* string comparison; both must be set), else no;
|
||||||
|
* - `VAR=VALUE` overrides are documented as already being environment
|
||||||
|
* variables when configure runs (the invoking shell exported them);
|
||||||
|
* the positional form is accepted-but-ignored until todo 18;
|
||||||
|
* - an unknown `--*` prints "unrecognized option" to stderr and
|
||||||
|
* exits 1.
|
||||||
|
* 3. HOST DETECTION: `st_os=$(uname -s)`, then the st_os_norm() emitted by
|
||||||
|
* resolve.h's st_resolve_emit_os_norm() rewrites it in place; `st_cc_id
|
||||||
|
* =unknown` (v1, no compiler-id sniffing at configure time).
|
||||||
|
* 4. TOOLCHAIN DEFAULTS: for each registered var name (CC/CFLAGS/...) emit
|
||||||
|
* `VAR="${VAR:-<default>}"` where <default> is the registry VALUE at
|
||||||
|
* generation time (st_sh_quote'd; typically empty for CFLAGS), then
|
||||||
|
* `LIBS=""`. Emitted BEFORE the probe cache boilerplate because that
|
||||||
|
* boilerplate uses `:=` fallbacks that must not clobber these values.
|
||||||
|
* 5. CACHE BOILERPLATE: probe.h's st_probe_emit_cache_functions (todo 12).
|
||||||
|
* 6. PROBE SECTION: for each top-level `feature` node in document order,
|
||||||
|
* for each check child, derive the checkname via resolve.h's
|
||||||
|
* st_resolve_check_name, build the probe via checks.h's
|
||||||
|
* st_check_probe_build, and emit the snippet via probe.h's
|
||||||
|
* st_probe_emit_snippet; then the feature aggregation + when-gate via
|
||||||
|
* st_resolve_emit_feature (todo 14); then the LIBS accumulation: for a
|
||||||
|
* `library "X"` check, `if [ "$have_<name>" = "yes" ]; then
|
||||||
|
* LIBS="$LIBS -l'X'"; fi`; for `pkg_config "X"`, the same wrapped in a
|
||||||
|
* v1 pkg-config guard `if command -v pkg-config >/dev/null 2>&1; then
|
||||||
|
* ... LIBS="$LIBS $(pkg-config --libs 'X')" ...; fi`. Generic over the
|
||||||
|
* check kinds, never hardcoded per feature name.
|
||||||
|
* 7. SUBSTITUTION: reads ./Makefile.in (cwd, the v1 source-dir contract)
|
||||||
|
* and replaces every `@VAR@` (VAR = registered vars ∪ {LIBS, prefix,
|
||||||
|
* srcdir}) with the shell variable's value, writing ./Makefile. An
|
||||||
|
* UNRESOLVED `@SOMETHING@` is a CONFIGURE-TIME error naming the
|
||||||
|
* variable (the generator cannot know the user's Makefile.in), reported
|
||||||
|
* after substitution via the per-line lookup; a missing Makefile.in is
|
||||||
|
* a clean error. See the SUBSTITUTION approach note below.
|
||||||
|
* 8. `exit 0`.
|
||||||
|
*
|
||||||
|
* SUBSTITUTION APPROACH (documented because the plan flagged it)
|
||||||
|
* --------------------------------------------------------------
|
||||||
|
* `${var//...}` is NOT POSIX, and sed's `s` replacement re-interprets `&`,
|
||||||
|
* `\` and the delimiter (values with `/`, spaces, `&` would be corrupted or
|
||||||
|
* need fragile escaping). The generated script therefore splits each line
|
||||||
|
* on `@` with pure POSIX parameter expansion (${v%%'@'*}, ${v#*'@'}) and
|
||||||
|
* looks each token up in a generated `case` (one arm per known variable,
|
||||||
|
* `printf '%s' "$VAR"` — the value is carried verbatim and never
|
||||||
|
* re-interpreted, so `$`, backticks, `/`, spaces, `&` all survive
|
||||||
|
* byte-exact and never execute). A `@` starts a substitution only when a
|
||||||
|
* second `@` follows on the same line, so a lone `@` stays literal. Known
|
||||||
|
* v1 limitation: a value may not END in a newline (command substitution
|
||||||
|
* strips trailing newlines); documented in the emitted script.
|
||||||
|
*
|
||||||
|
* POSIX-SH ONLY (AGENTS.md §4): the emitted script uses no `[[ ]]`, no
|
||||||
|
* arrays, no `local`, no `==`, no `<<<`, no `&>`, no `set -e`; `$(...)`
|
||||||
|
* is used. `sh -n` / `bash -n` / `zsh -n` must all pass (dash is not
|
||||||
|
* installed on this host — see .omo/notepads/stupidtools/issues.md).
|
||||||
|
*
|
||||||
|
* ERRORS
|
||||||
|
* ------
|
||||||
|
* All functions return NULL on success or an owned st_error. Failures are
|
||||||
|
* ST_ERR_USAGE for NULL arguments; ST_ERR_KDL_SCHEMA (span heap-allocated
|
||||||
|
* in the same block as the error, the parser.c pattern) for a feature name
|
||||||
|
* that is not a valid POSIX shell identifier; ST_ERR_INTERNAL for quoting
|
||||||
|
* allocation failures; ST_ERR_IO for stream write failures or (emit_path)
|
||||||
|
* an unopenable path. The document is only read, never modified or freed;
|
||||||
|
* the ctx is only read (its registry values are borrowed).
|
||||||
|
*
|
||||||
|
* Copyright (c) 2026 huntedbytheirs
|
||||||
|
* SPDX-License-Identifier: BSD-3-Clause
|
||||||
|
*/
|
||||||
|
|
||||||
|
#ifndef ST_GEN_CONFIGURE_H
|
||||||
|
#define ST_GEN_CONFIGURE_H
|
||||||
|
|
||||||
|
#include <stdio.h>
|
||||||
|
|
||||||
|
struct st_error;
|
||||||
|
struct st_ext_ctx;
|
||||||
|
struct st_kdl_document;
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Emit the complete POSIX-sh ./configure for `doc` (schema-validated) into
|
||||||
|
* `out`. Reads the registered variables from `ctx`'s variable registry and
|
||||||
|
* substitutes them by name; never hardcodes CC/CFLAGS/... in core. Returns
|
||||||
|
* NULL on success, or an owned st_error (see the header's ERRORS section).
|
||||||
|
*/
|
||||||
|
struct st_error *st_gen_configure_emit(FILE *out,
|
||||||
|
const struct st_kdl_document *doc,
|
||||||
|
struct st_ext_ctx *ctx);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Convenience wrapper: open `path` for writing ("w", text) and emit into
|
||||||
|
* it. On success the file is fully written and closed; on failure it is
|
||||||
|
* closed (best-effort) and an owned st_error is returned (ST_ERR_IO when
|
||||||
|
* the file cannot be created, otherwise the emit error).
|
||||||
|
*/
|
||||||
|
struct st_error *st_gen_configure_emit_path(const char *path,
|
||||||
|
const struct st_kdl_document *doc,
|
||||||
|
struct st_ext_ctx *ctx);
|
||||||
|
|
||||||
|
#endif /* ST_GEN_CONFIGURE_H */
|
||||||
@@ -0,0 +1,653 @@
|
|||||||
|
/* LINK: ../../src/gen/configure.c ../../src/gen/sh_emit.c ../../src/detect/probe.c ../../src/detect/checks.c ../../src/detect/check_registry.c ../../src/detect/resolve.c ../../src/kdl/schema.c ../../src/kdl/parser.c ../../src/kdl/lexer.c ../../src/kdl/value.c ../../src/ext/abi.c ../../src/ext/lang_c.c ../../src/ext/lang_cpp.c ../../src/error.c ../../src/span.c */
|
||||||
|
#ifndef _POSIX_C_SOURCE
|
||||||
|
#define _POSIX_C_SOURCE 200809L /* mkdtemp/unsetenv/setenv (POSIX.1-2008) */
|
||||||
|
#endif
|
||||||
|
/* tests/unit/test_gen_configure.c
|
||||||
|
*
|
||||||
|
* Unit + integration tests for the ./configure generator (todo 16):
|
||||||
|
* src/gen/configure.c. THE MODEL: the generator EMITS a POSIX-sh script
|
||||||
|
* from a schema-validated DSL document + an extension ctx; the emitted
|
||||||
|
* script RUNS at configure time (probes + @VAR@ substitution). These tests
|
||||||
|
* prove the emitted script is syntactically valid (sh/bash/zsh -n, no
|
||||||
|
* banned constructs), REALLY runs (exit 0, Makefile written, CC/LIBS/
|
||||||
|
* prefix correct), honors --prefix, errors cleanly on an unknown @VAR@, a
|
||||||
|
* missing Makefile.in and an unrecognized option, carries values with
|
||||||
|
* '/', spaces and '&' byte-exact, keeps $(...) / backticks inert, and
|
||||||
|
* cross-compile-skips RUN probes.
|
||||||
|
*
|
||||||
|
* The magic LINK comment on line 1 is REQUIRED by tests/run.sh (extra .c
|
||||||
|
* sources, relative to tests/unit/). configure.c pulls in the whole detect
|
||||||
|
* chain (probe/checks/check_registry/resolve) + sh_emit + abi (the C/C++
|
||||||
|
* modules register CC/CFLAGS) + kdl (parse/lexer/value/schema) + error/span.
|
||||||
|
*/
|
||||||
|
#include "munit.h"
|
||||||
|
|
||||||
|
#include "detect/check_registry.h"
|
||||||
|
#include "error.h"
|
||||||
|
#include "ext/abi.h"
|
||||||
|
#include "gen/configure.h"
|
||||||
|
#include "kdl/ast.h"
|
||||||
|
#include "kdl/schema.h"
|
||||||
|
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <string.h>
|
||||||
|
#include <sys/stat.h>
|
||||||
|
#include <sys/wait.h>
|
||||||
|
#include <unistd.h>
|
||||||
|
|
||||||
|
/* ---- per-test temp dir ------------------------------------------------ */
|
||||||
|
|
||||||
|
static char temp_dir[128];
|
||||||
|
|
||||||
|
static void *
|
||||||
|
setup(const MunitParameter params[], void *user_data)
|
||||||
|
{
|
||||||
|
(void)params;
|
||||||
|
(void)user_data;
|
||||||
|
if (snprintf(temp_dir, sizeof temp_dir, "/tmp/st_gen_XXXXXX") < 0) {
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
if (mkdtemp(temp_dir) == NULL) {
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
return (void *)1;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void
|
||||||
|
teardown(void *fixture)
|
||||||
|
{
|
||||||
|
char cmd[160];
|
||||||
|
|
||||||
|
if (fixture == NULL || temp_dir[0] == '\0') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (snprintf(cmd, sizeof cmd, "rm -rf -- '%s'", temp_dir) < 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
(void)system(cmd); /* best-effort; mkdtemp names are [A-Za-z0-9_]* */
|
||||||
|
temp_dir[0] = '\0';
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---- small file/process helpers --------------------------------------- */
|
||||||
|
|
||||||
|
/* Join temp_dir/`name` into `buf`; returns the snprintf result. */
|
||||||
|
static int
|
||||||
|
mkpath(char *buf, size_t sz, const char *name)
|
||||||
|
{
|
||||||
|
return snprintf(buf, sz, "%s/%s", temp_dir, name);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Slurp a file into a NUL-terminated heap buffer, or NULL. Caller frees. */
|
||||||
|
static char *
|
||||||
|
slurp(const char *path)
|
||||||
|
{
|
||||||
|
FILE *f;
|
||||||
|
long n;
|
||||||
|
char *buf;
|
||||||
|
|
||||||
|
f = fopen(path, "rb");
|
||||||
|
if (f == NULL) {
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
if (fseek(f, 0, SEEK_END) != 0) {
|
||||||
|
fclose(f);
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
n = ftell(f);
|
||||||
|
if (n < 0 || fseek(f, 0, SEEK_SET) != 0) {
|
||||||
|
fclose(f);
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
buf = malloc((size_t)n + 1);
|
||||||
|
if (buf == NULL) {
|
||||||
|
fclose(f);
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
if (fread(buf, 1, (size_t)n, f) != (size_t)n) {
|
||||||
|
free(buf);
|
||||||
|
fclose(f);
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
fclose(f);
|
||||||
|
buf[n] = '\0';
|
||||||
|
return buf;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Slurp temp_dir/Makefile, or NULL. Caller frees. */
|
||||||
|
static char *
|
||||||
|
makefile_text(void)
|
||||||
|
{
|
||||||
|
char path[600];
|
||||||
|
|
||||||
|
if (mkpath(path, sizeof path, "Makefile") < 0) {
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
return slurp(path);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Slurp temp_dir/run.err, or NULL. Caller frees. */
|
||||||
|
static char *
|
||||||
|
stderr_text(void)
|
||||||
|
{
|
||||||
|
char path[600];
|
||||||
|
|
||||||
|
if (mkpath(path, sizeof path, "run.err") < 0) {
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
return slurp(path);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Write `content` to temp_dir/Makefile.in. Returns 0 on success. */
|
||||||
|
static int
|
||||||
|
write_makefile_in(const char *content)
|
||||||
|
{
|
||||||
|
char path[600];
|
||||||
|
FILE *f;
|
||||||
|
|
||||||
|
if (mkpath(path, sizeof path, "Makefile.in") < 0) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
f = fopen(path, "w");
|
||||||
|
if (f == NULL) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
if (fputs(content, f) == EOF) {
|
||||||
|
fclose(f);
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
fclose(f);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Run "sh ./configure <args>" with cwd = temp_dir, stderr to run.err.
|
||||||
|
* Returns the configure exit status, or -1 if it could not be spawned. */
|
||||||
|
static int
|
||||||
|
run_configure(const char *args)
|
||||||
|
{
|
||||||
|
char cmd[2400];
|
||||||
|
int n;
|
||||||
|
int spawned;
|
||||||
|
|
||||||
|
n = snprintf(cmd, sizeof cmd, "cd '%s' && sh ./configure %s 2>'%s/run.err'",
|
||||||
|
temp_dir, args != NULL ? args : "", temp_dir);
|
||||||
|
if (n < 0 || (size_t)n >= sizeof cmd) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
spawned = system(cmd);
|
||||||
|
if (spawned == -1) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
return WEXITSTATUS(spawned);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* "<shell> -n <path>": the shell's exit status or -1. */
|
||||||
|
static int
|
||||||
|
syntax_check(const char *shell, const char *path)
|
||||||
|
{
|
||||||
|
char cmd[700];
|
||||||
|
int n;
|
||||||
|
int spawned;
|
||||||
|
|
||||||
|
n = snprintf(cmd, sizeof cmd, "%s -n '%s'", shell, path);
|
||||||
|
if (n < 0 || (size_t)n >= sizeof cmd) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
spawned = system(cmd);
|
||||||
|
if (spawned == -1) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
return WEXITSTATUS(spawned);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---- fixture loading (same as test_schema.c) -------------------------- */
|
||||||
|
|
||||||
|
static const char *
|
||||||
|
fixture_path(void)
|
||||||
|
{
|
||||||
|
static const char *const candidates[] = {
|
||||||
|
"tests/fixtures/stupid.kdl",
|
||||||
|
"../fixtures/stupid.kdl",
|
||||||
|
};
|
||||||
|
size_t i;
|
||||||
|
|
||||||
|
for (i = 0; i < sizeof(candidates) / sizeof(candidates[0]); i++) {
|
||||||
|
FILE *f = fopen(candidates[i], "rb");
|
||||||
|
|
||||||
|
if (f != NULL) {
|
||||||
|
fclose(f);
|
||||||
|
return candidates[i];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Build a ctx with the builtins + C detected (env CC/CFLAGS/etc. reset).
|
||||||
|
* Returns NULL on any failure. */
|
||||||
|
static struct st_ext_ctx *
|
||||||
|
build_ctx(void)
|
||||||
|
{
|
||||||
|
struct st_ext_ctx *ctx = st_ext_ctx_new();
|
||||||
|
struct st_error *err;
|
||||||
|
|
||||||
|
if (ctx == NULL) {
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
unsetenv("CC");
|
||||||
|
unsetenv("CFLAGS");
|
||||||
|
unsetenv("CXX");
|
||||||
|
unsetenv("CXXFLAGS");
|
||||||
|
err = st_ext_init_builtins(ctx);
|
||||||
|
if (err != NULL) {
|
||||||
|
st_error_free(err);
|
||||||
|
st_ext_ctx_free(ctx);
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
err = st_ext_detect_language(ctx, "c");
|
||||||
|
if (err != NULL) {
|
||||||
|
st_error_free(err);
|
||||||
|
st_ext_ctx_free(ctx);
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
return ctx;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Parse + validate + emit the fixture into temp_dir/configure, using
|
||||||
|
* `ctx`. Returns 0 on success, -1 on failure. */
|
||||||
|
static int
|
||||||
|
emit_fixture_configure(struct st_ext_ctx *ctx)
|
||||||
|
{
|
||||||
|
const char *path = fixture_path();
|
||||||
|
char conf[600];
|
||||||
|
char *src;
|
||||||
|
struct st_kdl_document *doc = NULL;
|
||||||
|
struct st_error *err = NULL;
|
||||||
|
|
||||||
|
if (path == NULL || ctx == NULL) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
src = slurp(path);
|
||||||
|
if (src == NULL) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
doc = st_kdl_parse(src, path, &err);
|
||||||
|
if (err != NULL) {
|
||||||
|
st_error_free(err);
|
||||||
|
free(src);
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
if (doc == NULL) {
|
||||||
|
free(src);
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
err = st_kdl_validate(doc);
|
||||||
|
if (err != NULL) {
|
||||||
|
st_error_free(err);
|
||||||
|
st_kdl_document_free(doc);
|
||||||
|
free(src);
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
if (mkpath(conf, sizeof conf, "configure") < 0) {
|
||||||
|
st_kdl_document_free(doc);
|
||||||
|
free(src);
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
err = st_gen_configure_emit_path(conf, doc, ctx);
|
||||||
|
/* the document borrows `src`'s token slices, so free src only after
|
||||||
|
* the document is released */
|
||||||
|
st_kdl_document_free(doc);
|
||||||
|
free(src);
|
||||||
|
if (err != NULL) {
|
||||||
|
st_error_free(err);
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* The standard Makefile.in every end-to-end case uses. */
|
||||||
|
#define STD_MAKEFILE_IN \
|
||||||
|
"CC = @CC@\n" \
|
||||||
|
"LIBS = @LIBS@\n" \
|
||||||
|
"prefix = @prefix@\n" \
|
||||||
|
"FLAGS = @CFLAGS@\n"
|
||||||
|
|
||||||
|
/* ---- (a) the emitted configure passes the three-shell syntax oracle --- */
|
||||||
|
|
||||||
|
static MunitResult
|
||||||
|
test_syntax(const MunitParameter params[], void *data)
|
||||||
|
{
|
||||||
|
(void)params;
|
||||||
|
(void)data;
|
||||||
|
static const char *const banned[] = {
|
||||||
|
"[[ ", "]]", "local ", "==", "<<<", "&>", "set -e",
|
||||||
|
};
|
||||||
|
struct st_ext_ctx *ctx;
|
||||||
|
char conf[600];
|
||||||
|
char *bytes;
|
||||||
|
size_t i;
|
||||||
|
|
||||||
|
ctx = build_ctx();
|
||||||
|
munit_assert_not_null(ctx);
|
||||||
|
munit_assert_int(emit_fixture_configure(ctx), ==, 0);
|
||||||
|
st_ext_ctx_free(ctx);
|
||||||
|
|
||||||
|
munit_assert_int(mkpath(conf, sizeof conf, "configure"), >, 0);
|
||||||
|
munit_assert_int(syntax_check("sh", conf), ==, 0);
|
||||||
|
munit_assert_int(syntax_check("bash", conf), ==, 0);
|
||||||
|
munit_assert_int(syntax_check("zsh", conf), ==, 0);
|
||||||
|
|
||||||
|
bytes = slurp(conf);
|
||||||
|
munit_assert_not_null(bytes);
|
||||||
|
for (i = 0; i < sizeof(banned) / sizeof(banned[0]); i++) {
|
||||||
|
munit_assert_null(strstr(bytes, banned[i]));
|
||||||
|
}
|
||||||
|
/* the preamble is structurally marked for todo 18 */
|
||||||
|
munit_assert_not_null(strstr(bytes, "# --- PREAMBLE ---"));
|
||||||
|
free(bytes);
|
||||||
|
return MUNIT_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---- (b) real end-to-end run ------------------------------------------ */
|
||||||
|
|
||||||
|
static MunitResult
|
||||||
|
test_end_to_end(const MunitParameter params[], void *data)
|
||||||
|
{
|
||||||
|
(void)params;
|
||||||
|
(void)data;
|
||||||
|
struct st_ext_ctx *ctx;
|
||||||
|
const char *cc;
|
||||||
|
char cc_buf[64];
|
||||||
|
char want[128];
|
||||||
|
char *mk;
|
||||||
|
int cc_len;
|
||||||
|
|
||||||
|
ctx = build_ctx();
|
||||||
|
munit_assert_not_null(ctx);
|
||||||
|
cc = st_registry_get_var(st_ext_var_registry(ctx), "CC");
|
||||||
|
munit_assert_not_null(cc);
|
||||||
|
/* copy before st_ext_ctx_free() — the registry value is borrowed */
|
||||||
|
cc_len = snprintf(cc_buf, sizeof cc_buf, "%s", cc);
|
||||||
|
munit_assert_int(cc_len, >, 0);
|
||||||
|
munit_assert_int(emit_fixture_configure(ctx), ==, 0);
|
||||||
|
st_ext_ctx_free(ctx);
|
||||||
|
|
||||||
|
munit_assert_int(write_makefile_in(STD_MAKEFILE_IN), ==, 0);
|
||||||
|
munit_assert_int(run_configure(NULL), ==, 0);
|
||||||
|
|
||||||
|
mk = makefile_text();
|
||||||
|
munit_assert_not_null(mk);
|
||||||
|
/* CC substituted from the registry value */
|
||||||
|
{
|
||||||
|
int n = snprintf(want, sizeof want, "CC = %s", cc_buf);
|
||||||
|
|
||||||
|
munit_assert_int(n, >, 0);
|
||||||
|
munit_assert_not_null(strstr(mk, want));
|
||||||
|
}
|
||||||
|
/* LIBS accumulated from library checks (pthread + m) */
|
||||||
|
munit_assert_not_null(strstr(mk, "-lpthread"));
|
||||||
|
munit_assert_not_null(strstr(mk, "-lm"));
|
||||||
|
/* prefix defaults to /usr/local */
|
||||||
|
munit_assert_not_null(strstr(mk, "prefix = /usr/local"));
|
||||||
|
free(mk);
|
||||||
|
return MUNIT_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---- (c) --prefix is honored ------------------------------------------ */
|
||||||
|
|
||||||
|
static MunitResult
|
||||||
|
test_prefix(const MunitParameter params[], void *data)
|
||||||
|
{
|
||||||
|
(void)params;
|
||||||
|
(void)data;
|
||||||
|
struct st_ext_ctx *ctx;
|
||||||
|
char *mk;
|
||||||
|
|
||||||
|
ctx = build_ctx();
|
||||||
|
munit_assert_not_null(ctx);
|
||||||
|
munit_assert_int(emit_fixture_configure(ctx), ==, 0);
|
||||||
|
st_ext_ctx_free(ctx);
|
||||||
|
|
||||||
|
munit_assert_int(write_makefile_in(STD_MAKEFILE_IN), ==, 0);
|
||||||
|
munit_assert_int(run_configure("--prefix=/opt/x"), ==, 0);
|
||||||
|
|
||||||
|
mk = makefile_text();
|
||||||
|
munit_assert_not_null(mk);
|
||||||
|
munit_assert_not_null(strstr(mk, "prefix = /opt/x"));
|
||||||
|
free(mk);
|
||||||
|
return MUNIT_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---- (d) an unknown @VAR@ errors naming the variable ------------------ */
|
||||||
|
|
||||||
|
static MunitResult
|
||||||
|
test_unknown_var(const MunitParameter params[], void *data)
|
||||||
|
{
|
||||||
|
(void)params;
|
||||||
|
(void)data;
|
||||||
|
struct st_ext_ctx *ctx;
|
||||||
|
char *err;
|
||||||
|
|
||||||
|
ctx = build_ctx();
|
||||||
|
munit_assert_not_null(ctx);
|
||||||
|
munit_assert_int(emit_fixture_configure(ctx), ==, 0);
|
||||||
|
st_ext_ctx_free(ctx);
|
||||||
|
|
||||||
|
munit_assert_int(write_makefile_in("CC = @NOPE@\n"), ==, 0);
|
||||||
|
munit_assert_int(run_configure(NULL), !=, 0);
|
||||||
|
|
||||||
|
err = stderr_text();
|
||||||
|
munit_assert_not_null(err);
|
||||||
|
munit_assert_not_null(strstr(err, "NOPE"));
|
||||||
|
free(err);
|
||||||
|
return MUNIT_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---- (e) a missing Makefile.in is a clean error ----------------------- */
|
||||||
|
|
||||||
|
static MunitResult
|
||||||
|
test_missing_makefile(const MunitParameter params[], void *data)
|
||||||
|
{
|
||||||
|
(void)params;
|
||||||
|
(void)data;
|
||||||
|
struct st_ext_ctx *ctx;
|
||||||
|
char *err;
|
||||||
|
|
||||||
|
ctx = build_ctx();
|
||||||
|
munit_assert_not_null(ctx);
|
||||||
|
munit_assert_int(emit_fixture_configure(ctx), ==, 0);
|
||||||
|
st_ext_ctx_free(ctx);
|
||||||
|
|
||||||
|
/* no Makefile.in written at all */
|
||||||
|
munit_assert_int(run_configure(NULL), !=, 0);
|
||||||
|
|
||||||
|
err = stderr_text();
|
||||||
|
munit_assert_not_null(err);
|
||||||
|
munit_assert_not_null(strstr(err, "Makefile.in"));
|
||||||
|
free(err);
|
||||||
|
return MUNIT_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---- (f) '/', spaces and '&' survive byte-exact ----------------------- */
|
||||||
|
|
||||||
|
static MunitResult
|
||||||
|
test_bytes_exact(const MunitParameter params[], void *data)
|
||||||
|
{
|
||||||
|
(void)params;
|
||||||
|
(void)data;
|
||||||
|
struct st_ext_ctx *ctx;
|
||||||
|
char *mk;
|
||||||
|
|
||||||
|
ctx = build_ctx();
|
||||||
|
munit_assert_not_null(ctx);
|
||||||
|
munit_assert_int(emit_fixture_configure(ctx), ==, 0);
|
||||||
|
st_ext_ctx_free(ctx);
|
||||||
|
|
||||||
|
/* spaces via CFLAGS; '&' and '/' via --prefix */
|
||||||
|
munit_assert_int(write_makefile_in(STD_MAKEFILE_IN), ==, 0);
|
||||||
|
munit_assert_int(setenv("CFLAGS", "a b c", 1), ==, 0);
|
||||||
|
munit_assert_int(run_configure("--prefix='/opt/x & y/z'"), ==, 0);
|
||||||
|
unsetenv("CFLAGS");
|
||||||
|
|
||||||
|
mk = makefile_text();
|
||||||
|
munit_assert_not_null(mk);
|
||||||
|
munit_assert_not_null(strstr(mk, "FLAGS = a b c"));
|
||||||
|
munit_assert_not_null(strstr(mk, "prefix = /opt/x & y/z"));
|
||||||
|
free(mk);
|
||||||
|
return MUNIT_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---- unrecognized --flag is a clean error ----------------------------- */
|
||||||
|
|
||||||
|
static MunitResult
|
||||||
|
test_unrecognized_option(const MunitParameter params[], void *data)
|
||||||
|
{
|
||||||
|
(void)params;
|
||||||
|
(void)data;
|
||||||
|
struct st_ext_ctx *ctx;
|
||||||
|
char *err;
|
||||||
|
|
||||||
|
ctx = build_ctx();
|
||||||
|
munit_assert_not_null(ctx);
|
||||||
|
munit_assert_int(emit_fixture_configure(ctx), ==, 0);
|
||||||
|
st_ext_ctx_free(ctx);
|
||||||
|
|
||||||
|
munit_assert_int(run_configure("--bogus"), !=, 0);
|
||||||
|
|
||||||
|
err = stderr_text();
|
||||||
|
munit_assert_not_null(err);
|
||||||
|
munit_assert_not_null(strstr(err, "unrecognized option"));
|
||||||
|
free(err);
|
||||||
|
return MUNIT_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---- injection: a $(...) VALUE stays inert (not executed) ------------- */
|
||||||
|
|
||||||
|
static MunitResult
|
||||||
|
test_injection_value(const MunitParameter params[], void *data)
|
||||||
|
{
|
||||||
|
(void)params;
|
||||||
|
(void)data;
|
||||||
|
struct st_ext_ctx *ctx;
|
||||||
|
char marker[600];
|
||||||
|
char *mk;
|
||||||
|
|
||||||
|
ctx = build_ctx();
|
||||||
|
munit_assert_not_null(ctx);
|
||||||
|
munit_assert_int(emit_fixture_configure(ctx), ==, 0);
|
||||||
|
st_ext_ctx_free(ctx);
|
||||||
|
|
||||||
|
munit_assert_int(mkpath(marker, sizeof marker, "pwned1"), >, 0);
|
||||||
|
|
||||||
|
/* prefix gets a command-substitution payload; it must be a literal. */
|
||||||
|
munit_assert_int(write_makefile_in("prefix = @prefix@\n"), ==, 0);
|
||||||
|
munit_assert_int(run_configure("--prefix='$(touch pwned1)'"), ==, 0);
|
||||||
|
|
||||||
|
munit_assert_int(access(marker, F_OK), ==, -1);
|
||||||
|
mk = makefile_text();
|
||||||
|
munit_assert_not_null(mk);
|
||||||
|
munit_assert_not_null(strstr(mk, "prefix = $(touch pwned1)"));
|
||||||
|
free(mk);
|
||||||
|
return MUNIT_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---- injection: an unknown @$(...)@ name stays inert (not executed) --- */
|
||||||
|
|
||||||
|
static MunitResult
|
||||||
|
test_injection_unknown_name(const MunitParameter params[], void *data)
|
||||||
|
{
|
||||||
|
(void)params;
|
||||||
|
(void)data;
|
||||||
|
struct st_ext_ctx *ctx;
|
||||||
|
char marker[600];
|
||||||
|
char *err;
|
||||||
|
|
||||||
|
ctx = build_ctx();
|
||||||
|
munit_assert_not_null(ctx);
|
||||||
|
munit_assert_int(emit_fixture_configure(ctx), ==, 0);
|
||||||
|
st_ext_ctx_free(ctx);
|
||||||
|
|
||||||
|
munit_assert_int(mkpath(marker, sizeof marker, "pwned2"), >, 0);
|
||||||
|
|
||||||
|
munit_assert_int(write_makefile_in("X = @$(touch pwned2)@\n"), ==, 0);
|
||||||
|
munit_assert_int(run_configure(NULL), !=, 0);
|
||||||
|
|
||||||
|
munit_assert_int(access(marker, F_OK), ==, -1);
|
||||||
|
err = stderr_text();
|
||||||
|
munit_assert_not_null(err);
|
||||||
|
munit_assert_not_null(strstr(err, "unknown variable"));
|
||||||
|
free(err);
|
||||||
|
return MUNIT_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---- cross-compile: RUN probes skip with a warning -------------------- */
|
||||||
|
|
||||||
|
static MunitResult
|
||||||
|
test_cross_compile(const MunitParameter params[], void *data)
|
||||||
|
{
|
||||||
|
(void)params;
|
||||||
|
(void)data;
|
||||||
|
static const char *const docsrc =
|
||||||
|
"project \"x\" version \"1.0\"\n"
|
||||||
|
"feature \"sz\" { sizeof \"long\" }\n";
|
||||||
|
struct st_ext_ctx *ctx;
|
||||||
|
struct st_kdl_document *doc = NULL;
|
||||||
|
struct st_error *err = NULL;
|
||||||
|
char conf[600];
|
||||||
|
char *e;
|
||||||
|
|
||||||
|
ctx = build_ctx();
|
||||||
|
munit_assert_not_null(ctx);
|
||||||
|
|
||||||
|
doc = st_kdl_parse(docsrc, "inline.kdl", &err);
|
||||||
|
munit_assert_null(err);
|
||||||
|
munit_assert_not_null(doc);
|
||||||
|
munit_assert_null(st_kdl_validate(doc));
|
||||||
|
munit_assert_int(mkpath(conf, sizeof conf, "configure"), >, 0);
|
||||||
|
munit_assert_null(st_gen_configure_emit_path(conf, doc, ctx));
|
||||||
|
st_kdl_document_free(doc);
|
||||||
|
st_ext_ctx_free(ctx);
|
||||||
|
|
||||||
|
munit_assert_int(write_makefile_in("CC = @CC@\n"), ==, 0);
|
||||||
|
munit_assert_int(run_configure("--host=aarch64-linux --build=x86_64-linux"),
|
||||||
|
==, 0);
|
||||||
|
|
||||||
|
e = stderr_text();
|
||||||
|
munit_assert_not_null(e);
|
||||||
|
munit_assert_not_null(strstr(e, "cross-compiling"));
|
||||||
|
free(e);
|
||||||
|
return MUNIT_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
static MunitTest tests[] = {
|
||||||
|
{ "/gen/syntax", test_syntax, setup, teardown, MUNIT_TEST_OPTION_NONE,
|
||||||
|
NULL },
|
||||||
|
{ "/gen/end-to-end", test_end_to_end, setup, teardown,
|
||||||
|
MUNIT_TEST_OPTION_NONE, NULL },
|
||||||
|
{ "/gen/prefix", test_prefix, setup, teardown, MUNIT_TEST_OPTION_NONE,
|
||||||
|
NULL },
|
||||||
|
{ "/gen/unknown-var", test_unknown_var, setup, teardown,
|
||||||
|
MUNIT_TEST_OPTION_NONE, NULL },
|
||||||
|
{ "/gen/missing-makefile", test_missing_makefile, setup, teardown,
|
||||||
|
MUNIT_TEST_OPTION_NONE, NULL },
|
||||||
|
{ "/gen/bytes-exact", test_bytes_exact, setup, teardown,
|
||||||
|
MUNIT_TEST_OPTION_NONE, NULL },
|
||||||
|
{ "/gen/unrecognized-option", test_unrecognized_option, setup, teardown,
|
||||||
|
MUNIT_TEST_OPTION_NONE, NULL },
|
||||||
|
{ "/gen/injection-value", test_injection_value, setup, teardown,
|
||||||
|
MUNIT_TEST_OPTION_NONE, NULL },
|
||||||
|
{ "/gen/injection-unknown-name", test_injection_unknown_name, setup,
|
||||||
|
teardown, MUNIT_TEST_OPTION_NONE, NULL },
|
||||||
|
{ "/gen/cross-compile", test_cross_compile, setup, teardown,
|
||||||
|
MUNIT_TEST_OPTION_NONE, NULL },
|
||||||
|
{ NULL, NULL, NULL, NULL, MUNIT_TEST_OPTION_NONE, NULL },
|
||||||
|
};
|
||||||
|
|
||||||
|
static const MunitSuite suite = {
|
||||||
|
"/gen", tests, NULL, 1, MUNIT_SUITE_OPTION_NONE,
|
||||||
|
};
|
||||||
|
|
||||||
|
int
|
||||||
|
main(int argc, char *argv[MUNIT_ARRAY_PARAM(argc + 1)])
|
||||||
|
{
|
||||||
|
return munit_suite_main(&suite, NULL, argc, argv);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user