Files
ww/cmd/ww/main.c

6432 lines
194 KiB
C

/*
* ww — the user-facing driver. Plan 9 cc(1) / Hare hare(1) analogue.
*
* Tool paths default to siblings of $0 (so a fresh build runs out of
* out/bin/), and can be overridden with WW_W6C / WW_W6A / WW_W6L.
*/
#define _XOPEN_SOURCE 700
#include "ww.h"
#include <errno.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>
#include <sys/stat.h>
#include <dirent.h>
#include <libgen.h>
#include <limits.h>
#include <stdarg.h>
#ifndef PATH_MAX
#define PATH_MAX 4096
#endif
static const char *usage =
"usage: ww [-V] <subcommand> [args...]\n"
" -V print version and exit\n"
" build [-S] [-w DIR] [-I DIR] [-o FILE] [path ...] build local package graphs\n"
" run [path] ... build then exec, passing extra args to the program\n"
" test [-S -o STEM] [-w DIR] [options] [path ...] build/run tests; -S emits package asm\n"
" version print version and exit\n"
"\n"
" path forms:\n"
" foo.ww literal file\n"
" foo search cwd, -I dirs, then the source library for foo.ww or foo/\n"
" lib/foo directory: build its package sources\n"
" -o publishes a non-main archive FILE + FILE.wwi\n"
" lib/... every eligible package under lib, recursively\n"
" . build the cwd's <basename>.ww\n";
static char *self_dir;
static const char *self_path;
static char *sep_sprintf(const char *, ...);
static int sep_reserve(void **, int *, int, size_t);
static int sep_fail_size(void);
static const char *
envpath(const char *name)
{
const char *p = getenv(name);
return p && p[0] ? p : NULL;
}
static const char *
toolpath(const char *envvar, const char *name)
{
const char *p = envpath(envvar);
if (p) return p;
static char buf[PATH_MAX];
int n = snprintf(buf, sizeof buf, "%s/%s", self_dir, name);
if (n < 0 || (size_t)n >= sizeof buf) return NULL;
return strdup(buf);
}
static int
run_argv(const char *prog, char *const argv[])
{
pid_t pid = fork();
if (pid < 0) { perror("ww: fork"); return -1; }
if (pid == 0) {
execv(prog, argv);
static const char msg[] = "ww: execve failed\n";
(void)write(2, msg, sizeof msg - 1);
_exit(127);
}
int status = 0;
pid_t got;
do { got = waitpid(pid, &status, 0); } while (got < 0 && errno == EINTR);
if (got < 0) { perror("ww: waitpid"); return -1; }
if (WIFEXITED(status)) return WEXITSTATUS(status);
return 1;
}
/* run_test_bin — exec the built test binary with an optional name-filter
* pattern as argv[1] (lib/test run() reads it via os.args). fork+execv
* (not system()) so glob metacharacters in the pattern reach the binary
* verbatim instead of being expanded by the shell. Mirrors the wwstage
* twin (selfhost/cmd/ww/main.ww runsingletest, which passes the same
* argv to os.exec.runstdio). #17 fnmatch filter. */
static int
run_test_bin(const char *bin, const char *pattern)
{
pid_t pid = fork();
if (pid < 0) { perror("ww: fork"); return -1; }
if (pid == 0) {
char *xargv[3];
xargv[0] = (char *)bin;
if (pattern) { xargv[1] = (char *)pattern; xargv[2] = NULL; }
else { xargv[1] = NULL; }
execv(bin, xargv);
perror("ww: exec");
_exit(127);
}
int status = 0;
/* the do_run twin's EINTR discipline: an interrupted wait left
* status==0, so WIFEXITED(0)/WEXITSTATUS(0) reported a false
* test PASS. */
pid_t got;
do { got = waitpid(pid, &status, 0); } while (got < 0 && errno == EINTR);
if (got < 0) { perror("ww: waitpid"); return -1; }
if (WIFEXITED(status)) return WEXITSTATUS(status);
return 1;
}
/* Delegate package/directory testing to the native WW coordinator. Keep the
* old single-file path in this driver: wwtest itself builds each generated
* package root through `ww test -c ... package.ww`, so that file boundary also
* prevents delegation recursion. */
static int
exec_package_command(int argc, char **argv, const char *target,
const char *resolved, const char *root_identity, int add_dot,
int build_only)
{
const char *override = getenv("WW_WWTEST");
char *fallback = NULL;
const char *prog = override && override[0] ? override : NULL;
if (prog == NULL) {
fallback = sep_sprintf("%s/wwtest", self_dir);
if (fallback == NULL) return 1;
prog = fallback;
}
if (argc < 0 || argc > INT_MAX - 10) {
free(fallback);
sep_fail_size();
return 1;
}
char **xargv = calloc((size_t)argc + 10, sizeof *xargv);
if (xargv == NULL) {
fprintf(stderr,
"ww %s: cannot allocate package coordinator arguments\n",
build_only ? "build" : "test");
free(fallback);
return 1;
}
int n = 0, dotted = 0;
xargv[n++] = (char *)prog;
xargv[n++] = "package";
if (build_only) {
xargv[n++] = "--ww-operation";
xargv[n++] = "build";
}
xargv[n++] = "--ww-driver";
xargv[n++] = (char *)self_path;
if (root_identity != NULL) {
xargv[n++] = "--ww-root-identity";
xargv[n++] = (char *)root_identity;
}
for (int i = 0; i < argc; i++) {
if (add_dot && !dotted && strcmp(argv[i], "--") == 0) {
xargv[n++] = ".";
dotted = 1;
}
xargv[n++] = (resolved && argv[i] == target)
? (char *)resolved : argv[i];
}
if (add_dot && !dotted) xargv[n++] = ".";
xargv[n] = NULL;
execv(prog, xargv); /* inherit the caller's environment */
fprintf(stderr, "ww %s: cannot exec package coordinator\n",
build_only ? "build" : "test");
free(xargv);
free(fallback);
return 1;
}
/* Breaks cycles in `use` resolution. Linear because typical imports are
* a handful per build. */
struct ImportSet {
char **paths;
int n, cap;
};
static int sep_fatal_allocation;
static int sep_fail_size(void);
static int sep_fail_nomem(void);
#define SEP_LOCAL_IMPORT_PREFIX "__wwlocal"
static int
import_seen(struct ImportSet *s, const char *path)
{
for (int i = 0; i < s->n; i++)
if (strcmp(s->paths[i], path) == 0) return 1;
return 0;
}
static int
import_add(struct ImportSet *s, const char *path)
{
if (s->n == INT_MAX) return sep_fail_size();
if (sep_reserve((void **)&s->paths, &s->cap, s->n + 1,
sizeof *s->paths) < 0)
return -1;
char *copy = strdup(path);
if (copy == NULL) return sep_fail_nomem();
s->paths[s->n++] = copy;
return 0;
}
/* `encoding.utf8` → `encoding/utf8`. Mirrors Hare hare(1)'s
* use-path → fs-path mapping (ref/hare/hare/module/srcs.ha:78
* builds the same shape via path::push per ident part). */
static int
import_path_form(const char *name, char *out, size_t outsz)
{
size_t n = strlen(name);
if (n + 1 > outsz) return -1;
for (size_t i = 0; i < n; i++)
out[i] = (name[i] == '.') ? '/' : name[i];
out[n] = '\0';
return 0;
}
static int
reserved_import_path(const char *name)
{
size_t n = strlen(SEP_LOCAL_IMPORT_PREFIX);
return strncmp(name, SEP_LOCAL_IMPORT_PREFIX, n) == 0
&& (name[n] == '\0' || name[n] == '.');
}
/* An import path names one directory package. There is deliberately no
* <dir>/<path>.ww branch here: literal or searched single-file roots are a
* CLI compatibility concern handled by locate_module, never an import edge. */
static int
locate_import_in(const char *dir, const char *path_form, char *out,
size_t outsz)
{
struct stat st;
int n = snprintf(out, outsz, "%s/%s", dir, path_form);
if (n < 0 || (size_t)n >= outsz) return 0;
if (stat(out, &st) == 0 && S_ISDIR(st.st_mode))
return 1;
return 0;
}
/* Walk every ordered root for <root>/<path>/ only. A decoy
* <earlier-root>/<path>.ww is neither a match nor a shadow: source imports
* always create canonical directory-package nodes. */
static int
locate_import_root(const char *dirs, const char *path_form, char *out,
size_t outsz, char **root_out)
{
const char *p = dirs;
while (*p) {
const char *e = strchr(p, ':');
size_t n = e ? (size_t)(e - p) : strlen(p);
if (n > 0) {
char *dir = malloc(n + 1);
if (dir == NULL) return sep_fail_nomem();
memcpy(dir, p, n);
dir[n] = '\0';
int found = locate_import_in(dir, path_form, out, outsz);
if (found) {
if (root_out != NULL) *root_out = dir;
else free(dir);
return 1;
}
free(dir);
}
if (!e) break;
p = e + 1;
}
return 0;
}
static int
locate_import(const char *dirs, const char *path_form, char *out,
size_t outsz)
{
return locate_import_root(dirs, path_form, out, outsz, NULL) > 0;
}
/* Allocation-sized source-import lookup. Package identity and search depth
* are not bounded by PATH_MAX; callers own both returned strings. */
static int
locate_import_alloc(const char *dirs, const char *path_form,
char **entry_out, char **root_out)
{
const char *p = dirs;
while (*p != '\0') {
const char *e = strchr(p, ':');
size_t n = e != NULL ? (size_t)(e - p) : strlen(p);
if (n > 0) {
char *root = strndup(p, n);
if (root == NULL) return sep_fail_nomem();
size_t pn = strlen(path_form);
if (n > (size_t)-1 - pn - 2) {
free(root);
return sep_fail_size();
}
char *entry = malloc(n + pn + 2);
if (entry == NULL) {
free(root);
return sep_fail_nomem();
}
memcpy(entry, root, n);
size_t off = n;
if (off == 0 || entry[off - 1] != '/') entry[off++] = '/';
memcpy(entry + off, path_form, pn + 1);
struct stat st;
if (stat(entry, &st) == 0 && S_ISDIR(st.st_mode)) {
*entry_out = entry;
*root_out = root;
return 1;
}
free(entry);
free(root);
}
if (e == NULL) break;
p = e + 1;
}
return 0;
}
/* CLI target compatibility: directory packages still win globally, then a
* bare target may resolve to <root>/<path>.ww. This function is never used
* while loading a source import. */
static int
locate_module(const char *dirs, const char *path_form, char *out,
size_t outsz, int *is_dir)
{
if (locate_import(dirs, path_form, out, outsz)) {
*is_dir = 1;
return 1;
}
const char *p = dirs;
while (*p) {
const char *e = strchr(p, ':');
size_t n = e ? (size_t)(e - p) : strlen(p);
if (n > 0) {
char *dir = malloc(n + 1);
if (dir == NULL) return 0;
memcpy(dir, p, n);
dir[n] = '\0';
int pn = snprintf(out, outsz, "%s/%s.ww", dir, path_form);
free(dir);
if (pn < 0 || (size_t)pn >= outsz) {
if (!e) break;
p = e + 1;
continue;
}
if (access(out, 0) == 0) {
*is_dir = 0;
return 1;
}
}
if (!e) break;
p = e + 1;
}
return 0;
}
/* Byte-wise total order is locale-independent; rule-10 byte-id requires
* the two stages sort the same way. strcmp diverges from Hare's memcmp
* (ref/hare/sort/cmp/cmp.ha:9); the order is identical for NUL-free
* filenames. */
static int
strs_cmp(const void *a, const void *b)
{
const char *sa = *(const char *const *)a;
const char *sb = *(const char *const *)b;
return strcmp(sa, sb);
}
static int sep_slurp(const char*, char**, u64*);
/* Go's contract: only *_test.ww is a test source. Ask the compiler parser,
* rather than a textual attribute scan, whether a production source contains
* @test; otherwise valid whitespace/comments could silently drop a test. */
static int
source_has_test_decl(const char *path)
{
char *buf;
u64 len;
if (sep_slurp(path, &buf, &len) < 0) return -1;
/* Keep directory-loader package-clause diagnostics stable. The full
* parser reports its language-level "missing package clause" first;
* the imports-only pass owns the package-loader wording and also avoids
* stage-specific recovery diagnostics for a malformed clause. */
Arena *ia = newarena();
Lex il;
Parser ip;
lexinit(&il, ia, path, buf, len);
parserinit(&ip, ia, &il);
Node *imports = parseimports(&ip);
if (il.errs || ip.errs) {
freearena(ia);
free(buf);
return -1;
}
if (imports->module == NULL) {
Pos pp = { path, 1, 1 };
errorf(pp, "invalid or missing package clause");
freearena(ia);
free(buf);
return -1;
}
freearena(ia);
Arena *a = newarena();
Lex l;
Parser p;
lexinit(&l, a, path, buf, len);
parserinit(&p, a, &l);
Node *file = parsefile(&p);
if (l.errs || p.errs) {
freearena(a);
free(buf);
return -1;
}
int found = 0;
for (Node *d = file->list; d != NULL && !found; d = d->next)
if (d->kind == N_FNDECL)
for (Node *at = d->attr; at != NULL; at = at->next)
if (at->str != NULL && strcmp(at->str, "test") == 0) {
found = 1;
break;
}
freearena(a);
free(buf);
return found;
}
#define SEP_VARIANT_PRODUCTION 0
#define SEP_VARIANT_SAME_TEST 1
#define SEP_VARIANT_EXTERNAL 2
#define SEP_VARIANT_TEST_MAIN 3
#define SEP_ROLE_NORMAL 0
#define SEP_ROLE_TEST_SUPPORT 1
#define SEP_ROLE_GENERATED_MAIN 2
#define SEP_TEST_SUPPORT_MODULE "__wwtest"
#define SEP_LOAD_INTERNAL -3
#define SEP_LOAD_VENDOR -4
/* Package-graph storage grows geometrically. Counts remain signed ints
* because they are stable action/context indices throughout the existing
* command model; checked reserve rejects an unrepresentable count or byte
* size before publishing a partial vector or invoking a tool. */
#define SEP_INITIAL_CAP 8
/* Package discovery is deliberately single-threaded. Remember allocation
* and representability failures across its helper stack so one failed root
* cannot be mistaken for an ordinary package diagnostic while a sibling
* proceeds to compiler or linker invocation. */
static int
sep_fail_size(void)
{
sep_fatal_allocation = 1;
fprintf(stderr, "ww: package graph is too large\n");
return -1;
}
static int
sep_fail_nomem(void)
{
sep_fatal_allocation = 1;
fprintf(stderr, "ww: out of memory\n");
return -1;
}
static int
sep_reserve(void **buf, int *cap, int need, size_t elemsz)
{
if (need < 0 || elemsz == 0) return sep_fail_size();
if (need <= *cap) return 0;
int ncap = *cap > 0 ? *cap : SEP_INITIAL_CAP;
while (ncap < need) {
if (ncap > INT_MAX / 2) {
ncap = INT_MAX;
break;
}
ncap *= 2;
}
if (ncap < need || (size_t)ncap > (size_t)-1 / elemsz)
return sep_fail_size();
int oldcap = *cap;
void *next = realloc(*buf, (size_t)ncap * elemsz);
if (next == NULL) return sep_fail_nomem();
memset((char *)next + (size_t)oldcap * elemsz, 0,
(size_t)(ncap - oldcap) * elemsz);
*buf = next;
*cap = ncap;
return 0;
}
/* Test-file package classification uses the compiler's imports-only parser.
* The coordinator chooses variants, but the command owns which real source
* paths enter a package compilation. */
static int
source_package_name(const char *path, char **out)
{
char *buf;
u64 len;
if (sep_slurp(path, &buf, &len) < 0) {
fprintf(stderr, "ww: cannot read %s\n", path);
return -1;
}
Arena *a = newarena();
Lex l;
Parser p;
lexinit(&l, a, path, buf, len);
parserinit(&p, a, &l);
Node *imports = parseimports(&p);
if (l.errs || p.errs) {
freearena(a);
free(buf);
return -1;
}
if (imports->module == NULL) {
Pos pp = { path, 1, 1 };
errorf(pp, "invalid or missing package clause");
freearena(a);
free(buf);
return -1;
}
*out = strdup(imports->module);
if (*out == NULL) {
sep_fail_nomem();
freearena(a);
free(buf);
return -1;
}
freearena(a);
free(buf);
return 0;
}
static int
source_list_add(char ***list, int *n, int *cap, const char *path)
{
if (*n == INT_MAX) return sep_fail_size();
if (sep_reserve((void **)list, cap, *n + 1, sizeof **list) < 0)
return -1;
char *copy = strdup(path);
if (copy == NULL) return sep_fail_nomem();
(*list)[(*n)++] = copy;
return 0;
}
static void
source_list_free(char **list, int n)
{
for (int i = 0; i < n; i++) free(list[i]);
free(list);
}
/* Production packages select production files only. A same-package test root
* selects production files followed by matching same-package test files; an
* external root selects only matching external-test files. Each partition is
* byte-sorted so compiler test discovery is deterministic without generated
* package amalgamation. */
static int
enumerate_dir_ww(const char *dirpath, int variant, const char *test_package,
char ***out_files)
{
DIR *d = opendir(dirpath);
if (d == NULL) { *out_files = NULL; return -1; }
char **prod = NULL, **tests = NULL;
int nprod = 0, capprod = 0, ntests = 0, captests = 0;
struct dirent *ent;
while ((ent = readdir(d)) != NULL) {
const char *nm = ent->d_name;
size_t nl = strlen(nm);
if (nl <= 3) continue;
if (nm[0] == '.' || nm[0] == '_') continue;
if (strcmp(nm + nl - 3, ".ww") != 0) continue;
int is_test = nl >= 8 && strcmp(nm + nl - 8, "_test.ww") == 0;
if (variant == SEP_VARIANT_PRODUCTION && is_test) continue;
if (variant == SEP_VARIANT_EXTERNAL && !is_test) continue;
char path[PATH_MAX];
int pn = snprintf(path, sizeof path, "%s/%s", dirpath, nm);
if (pn < 0 || (size_t)pn >= sizeof path) {
fprintf(stderr, "ww: package source path is too long\n");
source_list_free(prod, nprod);
source_list_free(tests, ntests);
closedir(d);
*out_files = NULL;
return -2;
}
struct stat st;
if (lstat(path, &st) != 0) {
fprintf(stderr,
"ww: %s: package source is not a regular file\n", path);
source_list_free(prod, nprod);
source_list_free(tests, ntests);
closedir(d);
*out_files = NULL;
return -2;
}
if (S_ISLNK(st.st_mode)) {
if (stat(path, &st) != 0) {
fprintf(stderr,
"ww: %s: package source is not a regular file\n",
path);
source_list_free(prod, nprod);
source_list_free(tests, ntests);
closedir(d);
*out_files = NULL;
return -2;
}
/* go/build ignores a source-shaped symlink to a directory. */
if (S_ISDIR(st.st_mode)) continue;
}
if (!S_ISREG(st.st_mode)) {
fprintf(stderr,
"ww: %s: package source is not a regular file\n", path);
source_list_free(prod, nprod);
source_list_free(tests, ntests);
closedir(d);
*out_files = NULL;
return -2;
}
int has_test = !is_test ? source_has_test_decl(path) : 0;
if (has_test < 0) {
source_list_free(prod, nprod);
source_list_free(tests, ntests);
closedir(d);
*out_files = NULL;
return -2;
}
if (has_test > 0) {
fprintf(stderr,
"ww: %s: @test declaration outside *_test.ww\n",
path);
source_list_free(prod, nprod);
source_list_free(tests, ntests);
closedir(d);
*out_files = NULL;
return -2;
}
if (is_test) {
char *package = NULL;
if (source_package_name(path, &package) < 0) {
source_list_free(prod, nprod);
source_list_free(tests, ntests);
closedir(d);
*out_files = NULL;
return -2;
}
if (test_package == NULL || strcmp(package, test_package) != 0) {
free(package);
continue;
}
free(package);
}
char ***list = is_test ? &tests : &prod;
int *n = is_test ? &ntests : &nprod;
int *cap = is_test ? &captests : &capprod;
if (source_list_add(list, n, cap, path) < 0) {
source_list_free(prod, nprod);
source_list_free(tests, ntests);
closedir(d);
*out_files = NULL;
return -2;
}
}
closedir(d);
if (nprod > 1) qsort(prod, (size_t)nprod, sizeof *prod, strs_cmp);
if (ntests > 1) qsort(tests, (size_t)ntests, sizeof *tests, strs_cmp);
if (nprod > INT_MAX - ntests) {
sep_fail_size();
source_list_free(prod, nprod);
source_list_free(tests, ntests);
*out_files = NULL;
return -2;
}
int total = nprod + ntests;
if ((size_t)total > (size_t)-1 / sizeof *prod) {
sep_fail_size();
source_list_free(prod, nprod);
source_list_free(tests, ntests);
*out_files = NULL;
return -2;
}
char **all = total ? malloc((size_t)total * sizeof *all) : NULL;
if (total && all == NULL) {
sep_fail_nomem();
source_list_free(prod, nprod);
source_list_free(tests, ntests);
*out_files = NULL;
return -2;
}
for (int i = 0; i < nprod; i++) all[i] = prod[i];
for (int i = 0; i < ntests; i++) all[nprod + i] = tests[i];
free(prod);
free(tests);
*out_files = all;
return total;
}
/* ww build — separate-compilation driver (task #46/c3).
*
* This is the SOLE build path (E3-C1 flip, task #87): the legacy
* single-file amalgamator is gone. Each imported
* package's `.wwi` interface is materialized and every package is
* compiled on its own (`w6c -c`), then the `.o` set is flat-linked.
*
* Each w6c pass is BOTH consumer (reads each direct dep `.wwi` through a
* separate --import argument) AND producer (writes this package's `.wwi`
* for its importers via -I). Reverse-topo order guarantees a package's
* deps' `.wwi` exist before it compiles.
*
* Only DIRECT dependency artifacts enter a compile action. The canonical
* import path paired with each `.wwi` preserves qualified symbol identity;
* a `.wwi` relocates the public foreign type/const facts required by its own
* API. Full transitive reachability remains a linker concern.
*/
struct sepbind {
char kind;
char *name;
char *source;
int line;
int col;
int dep; /* stable canonical action index; -1 for inline */
};
struct sepbindset {
struct sepbind *v;
int n, cap;
};
struct sepchild {
int pkg;
int context;
};
struct sepchildren {
struct sepchild *v;
int n, cap;
};
struct seppkg {
char *path; /* complete compiler/import identity */
char *import_base; /* complete canonical ordinary import identity */
char *entry; /* resolved package dir (or file, for a file root) */
char *canon; /* canonical location; never compiler identity */
char *artifact; /* legacy short artifact key, when one exists */
char *storage; /* internal storage basename; never package identity */
char *init_symbol; /* canonical package/variant-owned hidden task */
int storage_hashed; /* storage is the bounded complete-action locator */
char *name; /* validated declared name; directory packages only */
char *test_package; /* selected test package; root variants only */
char **sources; /* owned, byte-sorted selected paths; dirs only */
int nsources;
int is_dir;
int variant; /* SEP_VARIANT_*; dependencies are production */
int role; /* normal, reserved test support, or generated main */
int root; /* requested usage; never package-action identity */
int link_entry; /* package supplies the executable's bare main */
int generated_main; /* compiler-owned generated test-main package */
int generated_target; /* target variant used by generated test main */
int failed; /* discovery/compile failure reaches this action */
int test_support; /* compiler-generated -T support package */
int loaded; /* directory membership/name loaded exactly once */
int export_changed; /* staged export differs from committed export */
int source_staged; /* warm request owns complete source `.new` set */
int init_staged; /* warm request owns complete dispatcher `.new` set */
int archive_staged; /* warm request owns complete archive `.new` */
int emit_context; /* first verified resolution context */
unsigned char *context_state; /* 0 new, 1 active, 2 checked */
int context_cap;
struct sepbindset bindings; /* first context's source-to-action bindings */
int *deps; /* stable direct-dep indices into sepgraph.pkg */
int ndeps;
int depcap;
int color; /* tri-color DFS: 0 white, 1 gray, 2 black */
};
struct sepcontext {
char *root; /* selected entry directory; diagnostic identity */
char *searchpath; /* root : explicit -I roots : toolchain source */
char *route; /* this package's resolved lexical directory route */
char *source_root; /* applicable lexical vendor-walk boundary */
};
struct sepgraph {
struct seppkg *pkg;
int n;
int pkgcap;
struct sepcontext *context;
int ncontext;
int contextcap;
int support_context;
int identity_failed; /* command-global canonical identity collision */
};
struct sepproduct {
const char *dir;
const char *out;
const char *identity; /* explicit canonical lookup identity, if any */
const char *test_package;
const char *status;
const char *artifact;
int variant;
int context;
int root;
int variant_root; /* production-plus-test or external test package */
int support; /* direct generated-main support action, or -1 */
char *stage_out; /* request-private linked/published output */
char *stage_iface; /* request-private published package interface */
char *stage_status; /* request-private completion marker */
};
static void sep_pkg_free_fields(struct seppkg *);
static int
sep_reserve_packages(struct sepgraph *g, int need)
{
return sep_reserve((void **)&g->pkg, &g->pkgcap, need,
sizeof *g->pkg);
}
static int
sep_reserve_contexts(struct sepgraph *g, int need)
{
return sep_reserve((void **)&g->context, &g->contextcap, need,
sizeof *g->context);
}
static unsigned char
sep_context_state(const struct seppkg *p, int context)
{
if (context < 0 || context >= p->context_cap) return 0;
return p->context_state[context];
}
static int
sep_set_context_state(struct seppkg *p, int context, unsigned char state)
{
if (context < 0 || context == INT_MAX) return sep_fail_size();
if (sep_reserve((void **)&p->context_state, &p->context_cap,
context + 1, sizeof *p->context_state) < 0)
return -1;
p->context_state[context] = state;
return 0;
}
static int
sep_add_dep(struct sepgraph *g, int pi, int dep)
{
struct seppkg *p = &g->pkg[pi];
for (int i = 0; i < p->ndeps; i++)
if (p->deps[i] == dep) return 0;
if (p->ndeps == INT_MAX) return sep_fail_size();
if (sep_reserve((void **)&p->deps, &p->depcap, p->ndeps + 1,
sizeof *p->deps) < 0)
return -1;
p->deps[p->ndeps++] = dep;
return 0;
}
#define SEP_MAXLFLAGS 32
#define SEP_ARTIFACT_MAX PATH_MAX
struct seplinkflags {
const char *libdirs[SEP_MAXLFLAGS];
int nlibdirs;
const char *libs[SEP_MAXLFLAGS];
int nlibs;
};
static char *
sep_sprintf(const char *fmt, ...)
{
va_list ap, cp;
va_start(ap, fmt);
va_copy(cp, ap);
int n = vsnprintf(NULL, 0, fmt, cp);
va_end(cp);
if (n < 0) {
va_end(ap);
sep_fail_size();
return NULL;
}
char *s = malloc((size_t)n + 1);
if (s == NULL) sep_fail_nomem();
if (s != NULL && vsnprintf(s, (size_t)n + 1, fmt, ap) != n) {
free(s);
s = NULL;
sep_fail_size();
}
va_end(ap);
return s;
}
struct sepsha256 {
u32 h[8];
u8 block[64];
u64 bytes;
size_t nblock;
};
static u32
sep_rotr32(u32 x, int n)
{
return (x >> n) | (x << (32 - n));
}
static void
sep_sha256_block(struct sepsha256 *s, const u8 *p)
{
static const u32 k[64] = {
0x428a2f98U, 0x71374491U, 0xb5c0fbcfU, 0xe9b5dba5U,
0x3956c25bU, 0x59f111f1U, 0x923f82a4U, 0xab1c5ed5U,
0xd807aa98U, 0x12835b01U, 0x243185beU, 0x550c7dc3U,
0x72be5d74U, 0x80deb1feU, 0x9bdc06a7U, 0xc19bf174U,
0xe49b69c1U, 0xefbe4786U, 0x0fc19dc6U, 0x240ca1ccU,
0x2de92c6fU, 0x4a7484aaU, 0x5cb0a9dcU, 0x76f988daU,
0x983e5152U, 0xa831c66dU, 0xb00327c8U, 0xbf597fc7U,
0xc6e00bf3U, 0xd5a79147U, 0x06ca6351U, 0x14292967U,
0x27b70a85U, 0x2e1b2138U, 0x4d2c6dfcU, 0x53380d13U,
0x650a7354U, 0x766a0abbU, 0x81c2c92eU, 0x92722c85U,
0xa2bfe8a1U, 0xa81a664bU, 0xc24b8b70U, 0xc76c51a3U,
0xd192e819U, 0xd6990624U, 0xf40e3585U, 0x106aa070U,
0x19a4c116U, 0x1e376c08U, 0x2748774cU, 0x34b0bcb5U,
0x391c0cb3U, 0x4ed8aa4aU, 0x5b9cca4fU, 0x682e6ff3U,
0x748f82eeU, 0x78a5636fU, 0x84c87814U, 0x8cc70208U,
0x90befffaU, 0xa4506cebU, 0xbef9a3f7U, 0xc67178f2U,
};
u32 w[64];
for (int i = 0; i < 16; i++)
w[i] = (u32)p[4*i] << 24 | (u32)p[4*i+1] << 16
| (u32)p[4*i+2] << 8 | p[4*i+3];
for (int i = 16; i < 64; i++) {
u32 x = w[i-15], y = w[i-2];
u32 a = sep_rotr32(x, 7) ^ sep_rotr32(x, 18) ^ (x >> 3);
u32 b = sep_rotr32(y, 17) ^ sep_rotr32(y, 19) ^ (y >> 10);
w[i] = w[i-16] + a + w[i-7] + b;
}
u32 a = s->h[0], b = s->h[1], c = s->h[2], d = s->h[3];
u32 e = s->h[4], f = s->h[5], g = s->h[6], h = s->h[7];
for (int i = 0; i < 64; i++) {
u32 s1 = sep_rotr32(e, 6) ^ sep_rotr32(e, 11)
^ sep_rotr32(e, 25);
u32 ch = (e & f) ^ (~e & g);
u32 t1 = h + s1 + ch + k[i] + w[i];
u32 s0 = sep_rotr32(a, 2) ^ sep_rotr32(a, 13)
^ sep_rotr32(a, 22);
u32 maj = (a & b) ^ (a & c) ^ (b & c);
u32 t2 = s0 + maj;
h = g; g = f; f = e; e = d + t1;
d = c; c = b; b = a; a = t1 + t2;
}
s->h[0] += a; s->h[1] += b; s->h[2] += c; s->h[3] += d;
s->h[4] += e; s->h[5] += f; s->h[6] += g; s->h[7] += h;
}
static void
sep_sha256_init(struct sepsha256 *s)
{
static const u32 init[8] = {
0x6a09e667U, 0xbb67ae85U, 0x3c6ef372U, 0xa54ff53aU,
0x510e527fU, 0x9b05688cU, 0x1f83d9abU, 0x5be0cd19U,
};
memset(s, 0, sizeof *s);
memcpy(s->h, init, sizeof init);
}
static void
sep_sha256_write(struct sepsha256 *s, const void *vp, size_t n)
{
const u8 *p = vp;
s->bytes += n;
while (n > 0) {
size_t take = sizeof s->block - s->nblock;
if (take > n) take = n;
memcpy(s->block + s->nblock, p, take);
s->nblock += take;
p += take;
n -= take;
if (s->nblock == sizeof s->block) {
sep_sha256_block(s, s->block);
s->nblock = 0;
}
}
}
static void
sep_sha256_sum(struct sepsha256 *s, u8 out[32])
{
u64 bits = s->bytes << 3;
u8 pad[128] = {0x80};
size_t n = s->nblock < 56 ? 56 - s->nblock : 120 - s->nblock;
sep_sha256_write(s, pad, n);
u8 len[8];
for (int i = 0; i < 8; i++) len[7-i] = (u8)(bits >> (8*i));
sep_sha256_write(s, len, sizeof len);
for (int i = 0; i < 8; i++) {
out[4*i] = (u8)(s->h[i] >> 24);
out[4*i+1] = (u8)(s->h[i] >> 16);
out[4*i+2] = (u8)(s->h[i] >> 8);
out[4*i+3] = (u8)s->h[i];
}
}
static char *
sep_storage_digest(const struct seppkg *p)
{
static const char prefix[] = "ww-package-storage-v2:";
static const char hex[] = "0123456789abcdef";
struct sepsha256 s;
u8 sum[32];
char tag[5] = { (char)('0' + p->variant), ':',
(char)('0' + p->role), ':', 0 };
static const u8 zero;
sep_sha256_init(&s);
sep_sha256_write(&s, prefix, sizeof prefix - 1);
sep_sha256_write(&s, tag, 4);
sep_sha256_write(&s, p->path, strlen(p->path));
sep_sha256_write(&s, &zero, 1);
sep_sha256_write(&s, p->canon, strlen(p->canon));
sep_sha256_sum(&s, sum);
char *out = malloc(80);
if (out == NULL) {
sep_fail_nomem();
return NULL;
}
int n = snprintf(out, 16, "__wwpkg.v%d.r%d.h", p->variant, p->role);
if (n < 0 || n >= 16) { free(out); return NULL; }
for (int i = 0; i < 32; i++) {
out[n + 2*i] = hex[sum[i] >> 4];
out[n + 2*i + 1] = hex[sum[i] & 15];
}
out[n + 64] = '\0';
return out;
}
static int
sep_directory_variant(int variant)
{
return variant == SEP_VARIANT_PRODUCTION
|| variant == SEP_VARIANT_SAME_TEST
|| variant == SEP_VARIANT_EXTERNAL;
}
static char *
sep_variant_path(int variant, const char *base)
{
if (variant == SEP_VARIANT_EXTERNAL)
return sep_sprintf("%s_test", base);
char *path = strdup(base);
if (path == NULL) sep_fail_nomem();
return path;
}
static void
sep_diag_path_locations(const char *path, const char *a, const char *b)
{
if (strcmp(a, b) > 0) { const char *t = a; a = b; b = t; }
fprintf(stderr, "ww: package %s resolves to directories %s and %s\n",
path, a, b);
}
static void
sep_diag_directory_identities(const char *entry, const char *a, const char *b)
{
if (strcmp(a, b) > 0) { const char *t = a; a = b; b = t; }
fprintf(stderr, "ww: package directory %s has import identities %s and %s\n",
entry, a, b);
}
static int sep_import_component(const char *, size_t);
static int
sep_import_base_valid(const char *path)
{
const char *p = path;
while (*p != '\0') {
const char *dot = strchr(p, '.');
size_t n = dot != NULL ? (size_t)(dot - p) : strlen(p);
if (!sep_import_component(p, n)) return 0;
if (dot == NULL) return 1;
p = dot + 1;
}
return 0;
}
/* Go command packages retain their canonical import identity while declaring
* package main. An external command-test variant declares main_test. These
* declarations classify package kind but never participate in interning. */
static int
sep_command_declared_name(const struct seppkg *p)
{
if (p->name == NULL) return 0;
return p->variant == SEP_VARIANT_EXTERNAL
? strcmp(p->name, "main_test") == 0
: strcmp(p->name, "main") == 0;
}
/* Directory-package action kind comes only from the loaded declaration.
* An explicit package-less file is the retained raw-unit compatibility path;
* it has no directory package declaration and remains a command unit. */
static int
sep_root_is_command(const struct seppkg *p)
{
if (p->name == NULL) return !p->is_dir;
return strcmp(p->name, "main") == 0;
}
static int sep_external_production_edge(const struct sepgraph *, int, int);
static int sep_external_name_matches_production(const struct sepgraph *,
int, int);
static int
sep_forbidden_command_import(const struct sepgraph *g, int importer, int dep)
{
const struct seppkg *to = &g->pkg[dep];
if (strcmp(to->name, "main") != 0 || to->role != SEP_ROLE_NORMAL)
return 0;
/* An external test's exact import of its colocated command production is
* test-variant wiring, not a general source-importable command edge. */
return !(sep_external_production_edge(g, importer, dep)
&& sep_external_name_matches_production(g, importer, dep));
}
static int
sep_internal_parent_count(const char *path, size_t *parents)
{
const char *p = path;
size_t components = 0;
size_t final = 0;
int found = 0;
while (*p != '\0') {
while (*p == '.') p++;
if (*p == '\0') break;
const char *end = strchr(p, '.');
size_t n = end != NULL ? (size_t)(end - p) : strlen(p);
if (n == sizeof "internal" - 1
&& memcmp(p, "internal", n) == 0) {
final = components;
found = 1;
}
components++;
if (end == NULL) break;
p = end + 1;
}
if (!found) return 0;
*parents = components - final;
return 1;
}
static int
sep_importer_within_owner(const struct seppkg *from,
const char *target_entry, size_t parents)
{
const char *importer = from->canon;
char *owned = NULL;
if (!from->is_dir) {
const char *slash = strrchr(from->canon, '/');
if (slash == NULL) return -1;
size_t n = slash == from->canon ? 1 : (size_t)(slash - from->canon);
owned = strndup(from->canon, n);
if (owned == NULL) return sep_fail_nomem();
importer = owned;
}
size_t boundary = strlen(target_entry);
while (boundary > 1 && target_entry[boundary - 1] == '/') boundary--;
for (size_t i = 0; i < parents; i++) {
while (boundary > 0 && target_entry[boundary - 1] != '/') boundary--;
while (boundary > 1 && target_entry[boundary - 1] == '/') boundary--;
}
char *lexical = boundary == 0
? strdup(".") : strndup(target_entry, boundary);
if (lexical == NULL) {
free(owned);
return sep_fail_nomem();
}
errno = 0;
char *owner = realpath(lexical, NULL);
free(lexical);
if (owner == NULL) {
if (errno == ENOMEM)
sep_fail_nomem();
else
fprintf(stderr, "ww: cannot canonicalize package %s\n",
target_entry);
free(owned);
return -1;
}
size_t n = strlen(importer);
boundary = strlen(owner);
int allowed = (n == boundary
&& memcmp(importer, owner, boundary) == 0)
|| (boundary == 1 && owner[0] == '/' && importer[0] == '/')
|| (n > boundary && memcmp(importer, owner, boundary) == 0
&& importer[boundary] == '/');
free(owner);
free(owned);
return allowed;
}
static int
sep_internal_import_allowed(const struct seppkg *from,
const char *target_path, const char *target_entry)
{
size_t parents;
if (!sep_internal_parent_count(target_path, &parents)) return 1;
return sep_importer_within_owner(from, target_entry, parents);
}
/* Locate the final exact non-terminal dotted component named vendor. The
* returned suffix points into path; parents is the number of lexical target
* directory components to remove in order to obtain the owning tree. */
static int
sep_vendor_suffix(const char *path, const char **suffix, size_t *parents)
{
const char *p = path;
const char *final_suffix = NULL;
size_t components = 0, final_component = 0;
while (*p != '\0') {
const char *dot = strchr(p, '.');
size_t n = dot != NULL ? (size_t)(dot - p) : strlen(p);
if (dot != NULL && dot[1] != '\0'
&& n == sizeof "vendor" - 1
&& memcmp(p, "vendor", n) == 0) {
final_suffix = dot + 1;
final_component = components;
}
components++;
if (dot == NULL) break;
p = dot + 1;
}
if (final_suffix == NULL) return 0;
*suffix = final_suffix;
*parents = components - final_component;
return 1;
}
/* Filesystem twin of sep_vendor_suffix for a directly selected literal test
* root whose ordinary import identity is not bound until after source scan. */
static const char *
sep_vendor_route_suffix(const char *path)
{
const char *p = path;
const char *final = NULL;
while (*p != '\0') {
while (*p == '/') p++;
if (*p == '\0') break;
const char *slash = strchr(p, '/');
size_t n = slash != NULL ? (size_t)(slash - p) : strlen(p);
if (slash != NULL && slash[1] != '\0'
&& n == sizeof "vendor" - 1
&& memcmp(p, "vendor", n) == 0)
final = slash + 1;
if (slash == NULL) break;
p = slash + 1;
}
return final;
}
static int
sep_vendor_import_allowed(const struct seppkg *from,
const char *target_path, const char *target_entry)
{
const char *suffix;
size_t parents;
if (!sep_vendor_suffix(target_path, &suffix, &parents)) return 1;
(void)suffix;
return sep_importer_within_owner(from, target_entry, parents);
}
static int
sep_path_is_vendored(const char *path)
{
const char *suffix;
size_t parents;
return path != NULL && sep_vendor_suffix(path, &suffix, &parents);
}
static int
sep_command_compiler_marker(const struct sepgraph *g, int pi)
{
return sep_command_declared_name(&g->pkg[pi])
&& !g->pkg[pi].link_entry;
}
/* Bind the canonical ordinary import identity of one provisional directory
* action. The action's compiler path is derived from that base and its semantic
* variant; neither requested-root state nor artifact naming participates. */
static int
sep_bind_import_base(struct sepgraph *g, int pi, const char *base)
{
struct seppkg *p = &g->pkg[pi];
if (base == NULL || base[0] == '\0') return -1;
if (!reserved_import_path(base) && !sep_import_base_valid(base)) {
fprintf(stderr, "ww: invalid package path %s\n", base);
return -1;
}
if (p->import_base != NULL) {
if (strcmp(p->import_base, base) == 0) return 0;
g->identity_failed = 1;
sep_diag_directory_identities(p->entry, p->import_base, base);
return -1;
}
char *path = sep_variant_path(p->variant, base);
if (path == NULL) return -1;
for (int i = 0; i < g->n; i++) {
if (i == pi || !g->pkg[i].is_dir || g->pkg[i].generated_main)
continue;
int same_location = strcmp(g->pkg[i].canon, p->canon) == 0;
int support_alias = p->role == SEP_ROLE_TEST_SUPPORT
|| g->pkg[i].role == SEP_ROLE_TEST_SUPPORT;
if (!support_alias && !same_location
&& g->pkg[i].import_base != NULL
&& strcmp(g->pkg[i].import_base, base) == 0) {
g->identity_failed = 1;
sep_diag_path_locations(base, g->pkg[i].entry, p->entry);
free(path);
return -1;
}
if (same_location && !support_alias
&& g->pkg[i].import_base != NULL
&& strcmp(g->pkg[i].import_base, base) != 0) {
/* Expanded vendor identity is part of the canonical package key.
* Different vendor routes may intentionally reach one physical
* directory and still denote distinct Go-like packages. */
if (!sep_path_is_vendored(g->pkg[i].import_base)
&& !sep_path_is_vendored(base)) {
g->identity_failed = 1;
sep_diag_directory_identities(p->entry,
g->pkg[i].import_base, base);
free(path);
return -1;
}
}
if (g->pkg[i].path != NULL && g->pkg[i].path[0] != '\0'
&& strcmp(g->pkg[i].path, path) == 0) {
if (!same_location) {
g->identity_failed = 1;
sep_diag_path_locations(path, g->pkg[i].entry, p->entry);
free(path);
return -1;
}
if (support_alias
|| g->pkg[i].variant == p->variant) {
g->identity_failed = 1;
fprintf(stderr,
"ww: package action identity collision for %s in %s\n",
path, p->entry);
free(path);
return -1;
}
}
}
p->import_base = strdup(base);
if (p->import_base == NULL) {
sep_fail_nomem();
free(path);
return -1;
}
free(p->path);
p->path = path;
return 0;
}
static int
sep_find_or_add_variant(struct sepgraph *g, const char *path,
const char *entry, int is_dir, int variant, const char *test_package,
int role, const char *artifact, int root)
{
char *canon = realpath(entry, NULL);
if (canon == NULL) {
if (errno == ENOMEM) return sep_fail_nomem();
fprintf(stderr, "ww: cannot canonicalize package %s\n", entry);
return -1;
}
const char *base = path ? path : "";
char *incoming_path = NULL;
if (is_dir && base[0] != '\0') {
incoming_path = sep_variant_path(variant, base);
if (incoming_path == NULL) { free(canon); return -1; }
}
for (int i = 0; i < g->n; i++) {
struct seppkg *q = &g->pkg[i];
int same_location = strcmp(q->canon, canon) == 0;
if (is_dir && q->is_dir && !q->generated_main) {
int support_alias = role == SEP_ROLE_TEST_SUPPORT
|| q->role == SEP_ROLE_TEST_SUPPORT;
if (!support_alias && !same_location && base[0] != '\0'
&& q->import_base != NULL
&& strcmp(base, q->import_base) == 0) {
g->identity_failed = 1;
sep_diag_path_locations(base, q->entry, entry);
free(incoming_path);
free(canon);
return -1;
}
if (incoming_path != NULL && q->path[0] != '\0'
&& strcmp(incoming_path, q->path) == 0
&& !same_location) {
g->identity_failed = 1;
sep_diag_path_locations(incoming_path, q->entry, entry);
free(incoming_path);
free(canon);
return -1;
}
if (same_location && q->variant == variant && q->role == role) {
if (base[0] != '\0' && sep_path_is_vendored(base)
&& q->root && q->import_base == NULL)
continue;
if (root && base[0] == '\0' && q->import_base != NULL
&& sep_path_is_vendored(q->import_base))
continue;
if (base[0] != '\0' && q->import_base != NULL
&& strcmp(base, q->import_base) != 0
&& (sep_path_is_vendored(base)
|| sep_path_is_vendored(q->import_base)))
continue;
const char *selected = test_package ? test_package : "";
const char *existing = q->test_package ? q->test_package : "";
if (variant != SEP_VARIANT_PRODUCTION
&& strcmp(existing, selected) != 0) {
fprintf(stderr,
"ww: incompatible package-test roots %s\n", entry);
free(incoming_path);
free(canon);
return -1;
}
if (base[0] != '\0'
&& sep_bind_import_base(g, i, base) < 0) {
free(incoming_path);
free(canon);
return -1;
}
q->root = q->root || root;
free(incoming_path);
free(canon);
return i;
}
if (same_location) {
if (support_alias) continue;
if (q->import_base != NULL && base[0] != '\0'
&& strcmp(q->import_base, base) != 0) {
if (sep_path_is_vendored(q->import_base)
|| sep_path_is_vendored(base))
continue;
g->identity_failed = 1;
sep_diag_directory_identities(entry,
q->import_base, base);
free(incoming_path);
free(canon);
return -1;
}
if (sep_directory_variant(q->variant)
&& sep_directory_variant(variant))
continue;
fprintf(stderr,
"ww: package directory %s has incompatible variants\n",
entry);
free(incoming_path);
free(canon);
return -1;
}
continue;
}
if (same_location && strcmp(q->path, base) == 0
&& q->variant == variant && q->role == role) {
q->root = q->root || root;
free(incoming_path);
free(canon);
return i;
}
}
if (g->n == INT_MAX) {
sep_fail_size();
free(incoming_path);
free(canon);
return -1;
}
if (sep_reserve_packages(g, g->n + 1) < 0) {
free(incoming_path);
free(canon);
return -1;
}
int ni = g->n++;
struct seppkg *p = &g->pkg[ni];
memset(p, 0, sizeof *p);
p->path = strdup(base);
p->entry = strdup(entry);
p->canon = canon;
free(incoming_path);
if (p->path == NULL || p->entry == NULL) {
sep_fail_nomem();
free(p->path); free(p->entry); free(p->canon);
g->n--;
return -1;
}
p->is_dir = is_dir;
p->variant = variant;
p->role = role;
p->root = root;
p->emit_context = -1;
if (test_package != NULL) {
p->test_package = strdup(test_package);
if (p->test_package == NULL) {
sep_fail_nomem();
sep_pkg_free_fields(p);
g->n--;
return -1;
}
}
if (is_dir) {
const char *inherited = base;
if (inherited[0] == '\0')
for (int i = 0; i < ni; i++)
if (g->pkg[i].is_dir && !g->pkg[i].generated_main
&& g->pkg[i].role != SEP_ROLE_TEST_SUPPORT
&& role != SEP_ROLE_TEST_SUPPORT
&& strcmp(g->pkg[i].canon, p->canon) == 0
&& g->pkg[i].import_base != NULL) {
if (root && sep_path_is_vendored(
g->pkg[i].import_base))
continue;
inherited = g->pkg[i].import_base;
break;
}
if (inherited[0] != '\0'
&& sep_bind_import_base(g, ni, inherited) < 0) {
sep_pkg_free_fields(p);
g->n--;
return -1;
}
} else {
if (artifact != NULL) {
p->artifact = strdup(artifact);
if (p->artifact == NULL) {
sep_fail_nomem();
sep_pkg_free_fields(p);
g->n--;
return -1;
}
}
}
return ni;
}
static int
sep_find_or_add(struct sepgraph *g, const char *path, const char *entry,
int is_dir)
{
return sep_find_or_add_variant(g, path, entry, is_dir,
SEP_VARIANT_PRODUCTION, NULL, SEP_ROLE_NORMAL, NULL, 0);
}
static int
sep_find_or_add_role(struct sepgraph *g, const char *path, const char *entry,
int is_dir, int role, const char *artifact)
{
return sep_find_or_add_variant(g, path, entry, is_dir,
SEP_VARIANT_PRODUCTION, NULL, role, artifact, 0);
}
static void
sep_pkg_free_fields(struct seppkg *p)
{
for (int j = 0; j < p->nsources; j++) free(p->sources[j]);
free(p->sources);
for (int j = 0; j < p->bindings.n; j++) {
free(p->bindings.v[j].name);
free(p->bindings.v[j].source);
}
free(p->bindings.v);
free(p->context_state);
free(p->deps);
free(p->path);
free(p->import_base);
free(p->entry);
free(p->canon);
free(p->artifact);
free(p->storage);
free(p->init_symbol);
free(p->name);
free(p->test_package);
memset(p, 0, sizeof *p);
}
/* Release the one package-owned directory-membership list. Every graph exit
* funnels through this function; regular-file nodes own no source list. */
static void
sep_graph_free(struct sepgraph *g)
{
if (g == NULL) return;
for (int i = 0; i < g->n; i++) sep_pkg_free_fields(&g->pkg[i]);
for (int i = 0; i < g->ncontext; i++) {
free(g->context[i].root);
free(g->context[i].searchpath);
free(g->context[i].route);
free(g->context[i].source_root);
}
free(g->context);
free(g->pkg);
free(g);
}
static int sep_import_path_from_relative(const char *, char *, size_t);
static char *
sep_trimmed_path(const char *path)
{
size_t n = strlen(path);
while (n > 1 && path[n - 1] == '/') n--;
if (n == 0) return strdup(".");
char *out = strndup(path, n);
if (out == NULL) sep_fail_nomem();
return out;
}
static char *
sep_parent_path(const char *path)
{
size_t n = strlen(path);
while (n > 1 && path[n - 1] == '/') n--;
size_t slash = n;
while (slash > 0 && path[slash - 1] != '/') slash--;
if (slash == 0) return strdup(".");
size_t parent = slash - 1;
while (parent > 1 && path[parent - 1] == '/') parent--;
if (parent == 0) parent = 1;
char *out = strndup(path, parent);
if (out == NULL) sep_fail_nomem();
return out;
}
static char *
sep_join_route(const char *root, const char *rel)
{
size_t n = strlen(root);
return sep_sprintf("%s%s%s", root,
n > 0 && root[n - 1] == '/' ? "" : "/", rel);
}
static int
sep_lexical_relative(const char *root, const char *route, const char **rel)
{
size_t rn = strlen(root), pn = strlen(route);
while (rn > 1 && root[rn - 1] == '/') rn--;
while (pn > 1 && route[pn - 1] == '/') pn--;
if (rn == 1 && root[0] == '/') {
if (pn > 1 && route[0] == '/') { *rel = route + 1; return 1; }
return 0;
}
if (rn == 1 && root[0] == '.' && pn > 2
&& route[0] == '.' && route[1] == '/') {
*rel = route + 2;
return 1;
}
if (pn > rn && strncmp(route, root, rn) == 0
&& route[rn] == '/') {
*rel = route + rn + 1;
return 1;
}
return 0;
}
static int
sep_same_canonical_directory(const char *a, const char *b)
{
errno = 0;
char *ac = realpath(a, NULL);
int ae = errno;
errno = 0;
char *bc = realpath(b, NULL);
int be = errno;
if ((ac == NULL && ae == ENOMEM) || (bc == NULL && be == ENOMEM)) {
free(ac); free(bc);
return sep_fail_nomem();
}
int same = ac != NULL && bc != NULL && strcmp(ac, bc) == 0;
free(ac); free(bc);
return same;
}
/* Determine the active source root before source scanning. An explicit
* logical identity reconstructs the exact root that selected it. A literal
* root uses the first precedence-valid strict ancestor from its own ordered
* search context; otherwise the selected directory itself is the boundary. */
static int
sep_initial_route_root(const char *entry, const char *identity,
const char *searchpath, char **route_out, char **source_root_out)
{
char *entry_trim = sep_trimmed_path(entry);
if (entry_trim == NULL) return -1;
if (identity != NULL && identity[0] != '\0') {
char *root = strdup(entry_trim);
if (root == NULL) {
free(entry_trim);
return sep_fail_nomem();
}
size_t components = 1;
for (const char *p = identity; *p != '\0'; p++)
if (*p == '.') components++;
for (size_t i = 0; i < components; i++) {
char *parent = sep_parent_path(root);
free(root);
root = parent;
if (root == NULL) { free(entry_trim); return -1; }
}
char *pathform = malloc(strlen(identity) + 1);
if (pathform == NULL) {
free(root); free(entry_trim);
return sep_fail_nomem();
}
if (import_path_form(identity, pathform, strlen(identity) + 1) < 0) {
free(pathform); free(root); free(entry_trim);
return -1;
}
char *route = sep_join_route(root, pathform);
free(pathform);
if (route == NULL) { free(root); free(entry_trim); return -1; }
int same = sep_same_canonical_directory(route, entry_trim);
if (same <= 0) {
if (same == 0)
fprintf(stderr,
"ww: package %s does not match resolved directory %s\n",
identity, entry);
free(route); free(root); free(entry_trim);
return -1;
}
free(entry_trim);
*route_out = route;
*source_root_out = root;
return 0;
}
errno = 0;
char *entry_canon = realpath(entry_trim, NULL);
if (entry_canon == NULL && errno == ENOMEM) {
free(entry_trim);
return sep_fail_nomem();
}
const char *p = searchpath;
while (*p != '\0') {
const char *e = strchr(p, ':');
size_t n = e != NULL ? (size_t)(e - p) : strlen(p);
if (n > 0) {
char *candidate_root = strndup(p, n);
if (candidate_root == NULL) {
free(entry_canon); free(entry_trim);
return sep_fail_nomem();
}
const char *rel = NULL;
char *physical_root = NULL;
if (!sep_lexical_relative(candidate_root, entry_trim, &rel)
&& entry_canon != NULL) {
errno = 0;
physical_root = realpath(candidate_root, NULL);
if (physical_root == NULL && errno == ENOMEM) {
free(candidate_root); free(entry_canon);
free(entry_trim);
return sep_fail_nomem();
}
if (physical_root != NULL) {
size_t rn = strlen(physical_root);
if (rn == 1 && physical_root[0] == '/'
&& entry_canon[0] == '/'
&& entry_canon[1] != '\0') {
rel = entry_canon + 1;
} else if (strncmp(entry_canon, physical_root, rn) == 0
&& entry_canon[rn] == '/'
&& entry_canon[rn + 1] != '\0') {
rel = entry_canon + rn + 1;
}
}
}
if (rel != NULL && rel[0] != '\0') {
char *identitybuf = malloc(strlen(rel) + 1);
if (identitybuf == NULL) {
free(physical_root); free(candidate_root);
free(entry_canon); free(entry_trim);
return sep_fail_nomem();
}
int valid = sep_import_path_from_relative(rel, identitybuf,
strlen(rel) + 1);
int reserved = valid > 0
&& reserved_import_path(identitybuf);
free(identitybuf);
if (valid > 0 && !reserved) {
char *located = NULL, *selected_root = NULL;
int found = locate_import_alloc(searchpath, rel,
&located, &selected_root);
if (found < 0) {
free(physical_root); free(candidate_root);
free(entry_canon); free(entry_trim);
return -1;
}
int same = found ? sep_same_canonical_directory(
located, entry_trim) : 0;
free(selected_root);
free(located);
if (same < 0) {
free(physical_root); free(candidate_root);
free(entry_canon); free(entry_trim);
return -1;
}
if (same) {
char *route = sep_join_route(candidate_root, rel);
free(physical_root); free(entry_canon);
free(entry_trim);
if (route == NULL) {
free(candidate_root);
return -1;
}
*route_out = route;
*source_root_out = candidate_root;
return 0;
}
}
}
free(physical_root);
free(candidate_root);
}
if (e == NULL) break;
p = e + 1;
}
free(entry_canon);
*route_out = entry_trim;
*source_root_out = strdup(entry_trim);
if (*source_root_out == NULL) {
free(entry_trim);
*route_out = NULL;
return sep_fail_nomem();
}
return 0;
}
static int
sep_context_add(struct sepgraph *g, const char *root,
const char *searchpath, const char *route, const char *source_root)
{
for (int i = 0; i < g->ncontext; i++)
if (strcmp(g->context[i].root, root) == 0
&& strcmp(g->context[i].searchpath, searchpath) == 0
&& strcmp(g->context[i].route, route) == 0
&& strcmp(g->context[i].source_root, source_root) == 0)
return i;
if (g->ncontext == INT_MAX) return sep_fail_size();
if (sep_reserve_contexts(g, g->ncontext + 1) < 0) return -1;
struct sepcontext *c = &g->context[g->ncontext];
memset(c, 0, sizeof *c);
c->root = strdup(root);
c->searchpath = strdup(searchpath);
c->route = sep_trimmed_path(route);
c->source_root = sep_trimmed_path(source_root);
if (c->root == NULL || c->searchpath == NULL || c->route == NULL
|| c->source_root == NULL) {
sep_fail_nomem();
free(c->root); free(c->searchpath);
free(c->route); free(c->source_root);
memset(c, 0, sizeof *c);
return -1;
}
return g->ncontext++;
}
/* One selected directory owns a base search order. Per-package child
* contexts retain their own lexical route and active source-root boundary;
* neither field participates in canonical package/action identity. */
static int
sep_context_for(struct sepgraph *g, const char *root,
const char *extra_includes, const char *toolsrcdir,
const char *identity)
{
char *searchpath;
if (extra_includes != NULL && extra_includes[0] != '\0')
searchpath = sep_sprintf("%s:%s:%s",
root, extra_includes, toolsrcdir);
else
searchpath = sep_sprintf("%s:%s", root, toolsrcdir);
if (searchpath == NULL) return -1;
char *route = NULL, *source_root = NULL;
if (sep_initial_route_root(root, identity, searchpath,
&route, &source_root) < 0) {
free(searchpath);
return -1;
}
int result = sep_context_add(g, root, searchpath, route, source_root);
free(source_root); free(route); free(searchpath);
return result;
}
static int
sep_child_context_for(struct sepgraph *g, int parent, const char *route,
const char *source_root)
{
if (parent < 0 || parent >= g->ncontext) return -1;
return sep_context_add(g, g->context[parent].root,
g->context[parent].searchpath, route, source_root);
}
/* Semantic package identity never enters a bounded filesystem component.
* Short actions retain their established basename. Overflow actions use a
* tagged SHA-256 locator; the complete owner remains in the unit/export and
* is checked before warm reuse. */
#define SEP_NAME_MAX 255
static const char *
sep_legacy_artifact(const struct seppkg *p)
{
if (p->artifact != NULL && p->artifact[0] != '\0') return p->artifact;
if (p->path != NULL && p->path[0] != '\0') return p->path;
return "__root";
}
static int
sep_validate_storage_path(const struct seppkg *p, const char *scratch)
{
static const char tail[] =
".init.unit.ww.wwtxn.9223372036854775807.old";
size_t need = strlen(scratch) + 1 + strlen(p->storage)
+ strlen(tail) + 1;
if (strlen(p->storage) + strlen(tail) > SEP_NAME_MAX
|| need > SEP_ARTIFACT_MAX) {
fprintf(stderr, "ww: package artifact path is too long\n");
return -1;
}
return 0;
}
static int
sep_assign_storage(struct seppkg *p, const char *scratch)
{
static const char tail[] =
".init.unit.ww.wwtxn.9223372036854775807.old";
const char *base = sep_legacy_artifact(p);
size_t need = strlen(scratch) + 1 + strlen(base)
+ strlen(tail) + 1;
if (strlen(base) + strlen(tail) <= SEP_NAME_MAX
&& need <= SEP_ARTIFACT_MAX) {
p->storage = strdup(base);
if (p->storage == NULL) sep_fail_nomem();
p->storage_hashed = 0;
} else {
p->storage = sep_storage_digest(p);
p->storage_hashed = 1;
}
if (p->storage == NULL) return -1;
return sep_validate_storage_path(p, scratch);
}
static int
sep_fname(const struct sepgraph *g, int pi, const char *scratch,
const char *suffix, char *out, size_t outsz)
{
int n = snprintf(out, outsz, "%s/%s%s", scratch,
g->pkg[pi].storage, suffix);
return n >= 0 && (size_t)n < outsz ? 0 : -1;
}
static int
sep_validate_artifact_paths(struct sepgraph *g, const char *scratch)
{
for (int i = 0; i < g->n; i++) {
if (g->pkg[i].failed || !g->pkg[i].loaded) continue;
if (g->pkg[i].storage == NULL
&& sep_assign_storage(&g->pkg[i], scratch) < 0) return -1;
}
int changed;
do {
changed = 0;
for (int i = 0; i < g->n && !changed; i++) {
if (g->pkg[i].failed || !g->pkg[i].loaded) continue;
for (int j = i + 1; j < g->n; j++) {
if (g->pkg[j].failed || !g->pkg[j].loaded
|| strcmp(g->pkg[i].storage,
g->pkg[j].storage) != 0)
continue;
if (!g->pkg[i].storage_hashed
|| !g->pkg[j].storage_hashed) {
int pair[2] = {i, j};
for (int k = 0; k < 2; k++) {
struct seppkg *p = &g->pkg[pair[k]];
if (p->storage_hashed) continue;
free(p->storage);
p->storage = sep_storage_digest(p);
if (p->storage == NULL) return -1;
p->storage_hashed = 1;
if (sep_validate_storage_path(p, scratch) < 0)
return -1;
}
changed = 1;
break;
}
fprintf(stderr,
"ww: package storage collision for %s in %s and %s in %s at %s\n",
g->pkg[i].path, g->pkg[i].canon,
g->pkg[j].path, g->pkg[j].canon,
g->pkg[i].storage);
return -1;
}
}
} while (changed);
return 0;
}
static int
sep_validate_unit_owner(const char *unit, const struct seppkg *p)
{
struct stat st;
if (lstat(unit, &st) != 0) {
if (errno == ENOENT) return 0;
fprintf(stderr, "ww: package storage owner mismatch at %s for %s\n",
p->storage, p->path[0] ? p->path : "(root)");
return -1;
}
if (!S_ISREG(st.st_mode)) {
fprintf(stderr, "ww: package storage owner mismatch at %s for %s\n",
p->storage, p->path[0] ? p->path : "(root)");
return -1;
}
FILE *f = fopen(unit, "rb");
if (f == NULL) {
fprintf(stderr, "ww: package storage owner mismatch at %s for %s\n",
p->storage, p->path[0] ? p->path : "(root)");
return -1;
}
char *line = NULL;
size_t cap = 0;
ssize_t n = getline(&line, &cap, f);
int bad = ferror(f) || fclose(f) != 0;
char *want = p->path[0] == '\0'
? strdup("//ww:module-reset\n")
: sep_sprintf("//ww:module-reset %s\n", p->path);
int match = !bad && n >= 0 && want != NULL && strcmp(line, want) == 0;
free(want);
free(line);
if (match) return 1;
fprintf(stderr,
"ww: package storage owner mismatch at %s for %s\n",
p->storage, p->path[0] ? p->path : "(root)");
return -1;
}
static int
sep_validate_workdir_owners(const struct sepgraph *g, const char *scratch)
{
char unit[SEP_ARTIFACT_MAX];
for (int i = 0; i < g->n; i++) {
if (g->pkg[i].failed || !g->pkg[i].loaded) continue;
if (sep_fname(g, i, scratch, ".unit.ww", unit, sizeof unit) < 0)
return -1;
if (sep_validate_unit_owner(unit, &g->pkg[i]) < 0) return -1;
}
return 0;
}
static int
sep_slurp(const char *path, char **out, u64 *len)
{
FILE *f = fopen(path, "rb");
if (f == NULL) return -1;
if (fseek(f, 0, SEEK_END) != 0) { fclose(f); return -1; }
long n = ftell(f);
if (n < 0 || fseek(f, 0, SEEK_SET) != 0) {
fclose(f);
return -1;
}
char *buf = malloc((size_t)n + 1);
if (buf == NULL) {
sep_fail_nomem();
fclose(f);
return -1;
}
if (fread(buf, 1, (size_t)n, f) != (size_t)n) {
free(buf);
fclose(f);
return -1;
}
buf[n] = '\0';
if (fclose(f) != 0) {
free(buf);
return -1;
}
*out = buf;
*len = (u64)n;
return 0;
}
static int
use_node_cmp(const void *a, const void *b)
{
const Node *x = *(Node *const *)a;
const Node *y = *(Node *const *)b;
const char *xp = x->usesource ? x->usesource
: x->usepath ? x->usepath : x->str;
const char *yp = y->usesource ? y->usesource
: y->usepath ? y->usepath : y->str;
int r = strcmp(xp, yp);
if (r != 0) return r;
r = strcmp(x->pos.file ? x->pos.file : "",
y->pos.file ? y->pos.file : "");
if (r != 0) return r;
if (x->pos.line != y->pos.line) return x->pos.line - y->pos.line;
return x->pos.col - y->pos.col;
}
static int
sep_external_production_name(const struct seppkg *pkg, const char *name)
{
if (pkg->variant != SEP_VARIANT_EXTERNAL
|| pkg->test_package == NULL || pkg->test_package[0] == '\0')
return 0;
size_t n = strlen(name);
size_t tn = strlen(pkg->test_package);
return tn == n + 5 && strncmp(pkg->test_package, name, n) == 0
&& strcmp(pkg->test_package + n, "_test") == 0;
}
static int
sep_external_production_edge(const struct sepgraph *g, int importer, int dep)
{
const struct seppkg *from = &g->pkg[importer];
const struct seppkg *to = &g->pkg[dep];
return from->variant == SEP_VARIANT_EXTERNAL
&& to->variant == SEP_VARIANT_PRODUCTION
&& to->role == SEP_ROLE_NORMAL
&& strcmp(from->canon, to->canon) == 0;
}
static int
sep_external_name_matches_production(const struct sepgraph *g, int importer,
int dep)
{
const struct seppkg *from = &g->pkg[importer];
const struct seppkg *to = &g->pkg[dep];
return from->name != NULL && to->name != NULL
&& sep_external_production_name(from, to->name);
}
static char *sep_local_import_base(const struct seppkg *);
/* Canonical source bindings make a shared action independent of the request
* that reaches it first. A direct binding retains the stable target action
* index, hence both expanded identity and canonical directory; contextual
* route/root legality deliberately does not enter action identity. */
static int
sep_binding_add(struct sepbindset *bindings, char kind, const char *name,
int dep, Pos pos)
{
if (bindings->n == INT_MAX) return sep_fail_size();
if (sep_reserve((void **)&bindings->v, &bindings->cap,
bindings->n + 1, sizeof *bindings->v) < 0)
return -1;
char *copy = strdup(name);
if (copy == NULL) return sep_fail_nomem();
char *source = strdup(pos.file ? pos.file : "");
if (source == NULL) {
free(copy);
return sep_fail_nomem();
}
bindings->v[bindings->n++] = (struct sepbind){
kind, copy, source, pos.line, pos.col, dep };
return 0;
}
static int
sep_binding_cmp(const void *a, const void *b)
{
const struct sepbind *x = a, *y = b;
int r = strcmp(x->name, y->name);
if (r != 0) return r;
if (x->kind != y->kind) return (unsigned char)x->kind
- (unsigned char)y->kind;
if (x->dep != y->dep) return x->dep < y->dep ? -1 : 1;
r = strcmp(x->source, y->source);
if (r != 0) return r;
if (x->line != y->line) return x->line - y->line;
return x->col - y->col;
}
static int
sep_binding_semantic_same(const struct sepbind *a, const struct sepbind *b)
{
return a->kind == b->kind && a->dep == b->dep
&& strcmp(a->name, b->name) == 0;
}
static int
sep_bindsets_semantically_same(const struct sepbindset *a,
const struct sepbindset *b)
{
int ai = 0, bi = 0;
while (ai < a->n && bi < b->n) {
if (!sep_binding_semantic_same(&a->v[ai], &b->v[bi])) return 0;
struct sepbind *av = &a->v[ai];
struct sepbind *bv = &b->v[bi];
do ai++; while (ai < a->n
&& sep_binding_semantic_same(av, &a->v[ai]));
do bi++; while (bi < b->n
&& sep_binding_semantic_same(bv, &b->v[bi]));
}
return ai == a->n && bi == b->n;
}
static int
sep_validate_bindings(const struct sepgraph *g,
const struct sepbindset *bindings)
{
const char *name = NULL;
int dep = -1;
for (int i = 0; i < bindings->n; i++) {
const struct sepbind *b = &bindings->v[i];
if (b->kind != 'D') continue;
if (name != NULL && strcmp(name, b->name) == 0 && dep != b->dep) {
Pos pos = { b->source, b->line, b->col };
errorf(pos, "package path %s resolves to both %s and %s",
b->name, g->pkg[dep].path, g->pkg[b->dep].path);
return -1;
}
name = b->name;
dep = b->dep;
}
return 0;
}
static int
sep_binding_needs_map(const struct sepgraph *g, const struct sepbind *b)
{
return b->kind == 'D' && b->dep >= 0 && b->dep < g->n
&& strcmp(b->name, g->pkg[b->dep].path) != 0;
}
static int
sep_binding_first_map(const struct sepgraph *g,
const struct sepbindset *bindings, int i)
{
if (!sep_binding_needs_map(g, &bindings->v[i])) return 0;
for (int j = i - 1; j >= 0
&& strcmp(bindings->v[j].name, bindings->v[i].name) == 0; j--)
if (sep_binding_needs_map(g, &bindings->v[j])) return 0;
return 1;
}
static void
sep_bindset_free(struct sepbindset *bindings)
{
for (int i = 0; i < bindings->n; i++) {
free(bindings->v[i].name);
free(bindings->v[i].source);
}
free(bindings->v);
bindings->v = NULL;
bindings->n = bindings->cap = 0;
}
static int
sep_children_add(struct sepchildren *children, int pkg, int context)
{
for (int i = 0; i < children->n; i++)
if (children->v[i].pkg == pkg
&& children->v[i].context == context)
return 0;
if (children->n == INT_MAX) return sep_fail_size();
if (sep_reserve((void **)&children->v, &children->cap,
children->n + 1, sizeof *children->v) < 0)
return -1;
children->v[children->n++] = (struct sepchild){ pkg, context };
return 0;
}
static void
sep_children_free(struct sepchildren *children)
{
free(children->v);
children->v = NULL;
children->n = children->cap = 0;
}
struct sepresolved {
char *identity; /* expanded canonical package identity */
char *entry; /* current edge's resolved lexical route */
char *source_root; /* child vendor-walk boundary */
int vendored;
};
static void
sep_resolved_free(struct sepresolved *r)
{
free(r->identity);
free(r->entry);
free(r->source_root);
memset(r, 0, sizeof *r);
}
/* A candidate shadows outer/ordinary lookup only when the directory contains
* at least one observed source filename. A source-named nonregular entry still
* selects the package so enumeration reports the real package error. Returns
* -1 only for deterministic loader-owned allocation failure. */
static int
sep_vendor_candidate_has_sources(const char *candidate)
{
struct stat st;
if (stat(candidate, &st) != 0) return 0;
if (!S_ISDIR(st.st_mode)) return 0;
DIR *d = opendir(candidate);
if (d == NULL) return 0;
int found = 0;
struct dirent *de;
while ((de = readdir(d)) != NULL) {
size_t n = strlen(de->d_name);
if (n < 3 || strcmp(de->d_name + n - 3, ".ww") != 0)
continue;
char *path = sep_join_route(candidate, de->d_name);
if (path == NULL) { (void)closedir(d); return -1; }
struct stat ent;
int directory = lstat(path, &ent) == 0 && S_ISDIR(ent.st_mode);
free(path);
if (directory) continue;
found = 1;
break;
}
(void)closedir(d);
return found;
}
/* Expand one source import under its own package context. The nearest
* source-bearing vendor candidate wins; ordinary lookup additionally records
* the exact ordered root that selected the child. */
static int
sep_resolve_source_import(struct sepgraph *g, int context,
const char *name, const char *path_form, struct sepresolved *out)
{
memset(out, 0, sizeof *out);
if (context < 0 || context >= g->ncontext) return -1;
const char *route = g->context[context].route;
const char *source_root = g->context[context].source_root;
const char *ignored;
if (strcmp(route, source_root) != 0
&& !sep_lexical_relative(source_root, route, &ignored)) {
fprintf(stderr, "ww: package route %s is outside source root %s\n",
route, source_root);
return -1;
}
const char *direct_suffix;
size_t direct_parents;
int direct_expanded = sep_vendor_suffix(name, &direct_suffix,
&direct_parents);
(void)direct_suffix;
(void)direct_parents;
char *ancestor = NULL;
if (!direct_expanded) ancestor = sep_trimmed_path(route);
if (!direct_expanded && ancestor == NULL) return -1;
while (!direct_expanded) {
char *vendordir = sep_join_route(ancestor, "vendor");
char *candidate = vendordir != NULL
? sep_join_route(vendordir, path_form) : NULL;
free(vendordir);
if (candidate == NULL) { free(ancestor); return -1; }
int source_candidate = sep_vendor_candidate_has_sources(candidate);
if (source_candidate < 0) {
free(candidate); free(ancestor);
return -1;
}
if (source_candidate > 0) {
const char *rel = NULL;
if (!sep_lexical_relative(source_root, candidate, &rel)) {
free(candidate); free(ancestor);
return -1;
}
size_t n = strlen(rel);
char *identity = malloc(n + 1);
if (identity == NULL) {
free(candidate); free(ancestor);
return sep_fail_nomem();
}
int valid = sep_import_path_from_relative(rel, identity, n + 1);
if (valid <= 0 || reserved_import_path(identity)) {
free(identity); free(candidate); free(ancestor);
if (valid < 0) sep_fail_size();
else fprintf(stderr,
"ww: invalid vendored package path %s\n", rel);
return -1;
}
out->source_root = strdup(source_root);
if (out->source_root == NULL) {
free(identity); free(candidate); free(ancestor);
return sep_fail_nomem();
}
out->identity = identity;
out->entry = candidate;
out->vendored = 1;
free(ancestor);
return 1;
}
free(candidate);
if (strcmp(ancestor, source_root) == 0) break;
char *parent = sep_parent_path(ancestor);
if (parent == NULL) { free(ancestor); return -1; }
if (strcmp(parent, ancestor) == 0) {
free(parent); free(ancestor);
return -1;
}
free(ancestor);
ancestor = parent;
}
free(ancestor);
int located = locate_import_alloc(g->context[context].searchpath,
path_form, &out->entry, &out->source_root);
if (located <= 0) return located;
out->identity = strdup(name);
if (out->identity == NULL) {
sep_resolved_free(out);
return sep_fail_nomem();
}
return 1;
}
/* A DIRECTORY import is a package boundary: add it as a direct dep of pkg
* `pi`. A FILE import is an intra-package split — fold its imports into
* `pi` (its bytes join pi's body at emit time). Collects package PATHS
* rather than concatenating bytes the way the legacy amalgamator did
* (§1.1). */
static int
sep_scan_file(struct sepgraph *g, int pi, const char *file,
int context, struct ImportSet *filevisit,
struct sepbindset *bindings, struct sepchildren *children,
int owned_source)
{
if (import_seen(filevisit, file)) return 0;
if (import_add(filevisit, file) < 0) return -1;
char *buf;
u64 len;
if (sep_slurp(file, &buf, &len) < 0) {
fprintf(stderr, "ww: cannot read %s\n", file);
return -1;
}
Arena *a = newarena();
Lex l;
Parser p;
lexinit(&l, a, file, buf, len);
parserinit(&p, a, &l);
Node *imports = parseimports(&p);
if (l.errs || p.errs) {
freearena(a);
free(buf);
return -1;
}
if (imports->module == NULL && owned_source) {
Pos pp = { file, 1, 1 };
errorf(pp, "invalid or missing package clause");
freearena(a);
free(buf);
return -1;
}
if (imports->module != NULL
&& (owned_source || g->pkg[pi].name == NULL)) {
const char *declared = imports->module;
struct seppkg *pkg = &g->pkg[pi];
if (pkg->name == NULL) {
pkg->name = strdup(declared);
if (pkg->name == NULL) {
sep_fail_nomem();
freearena(a);
free(buf);
return -1;
}
}
else if (strcmp(pkg->name, declared) != 0) {
errorf(imports->pos,
"conflicting package names %s and %s in %s",
pkg->name, declared, pkg->entry);
freearena(a);
free(buf);
return -1;
}
}
if (owned_source && g->pkg[pi].is_dir) {
for (Node *package = imports->body; package; package = package->next) {
if (strcmp(package->module, g->pkg[pi].name) != 0) {
errorf(package->pos,
"conflicting package names %s and %s in %s",
g->pkg[pi].name, package->module, g->pkg[pi].entry);
freearena(a);
free(buf);
return -1;
}
}
}
int nuse = 0;
for (Node *u = imports->list; u; u = u->next)
if (u->kind == N_USE) {
if (nuse == INT_MAX) {
sep_fail_size();
freearena(a);
free(buf);
return -1;
}
nuse++;
}
if ((size_t)nuse > (size_t)-1 / sizeof(Node *)) {
sep_fail_size();
freearena(a);
free(buf);
return -1;
}
Node **uses = nuse ? malloc((size_t)nuse * sizeof *uses) : NULL;
if (nuse && uses == NULL) {
sep_fail_nomem();
freearena(a);
free(buf);
return -1;
}
int ui = 0;
for (Node *u = imports->list; u; u = u->next)
if (u->kind == N_USE) uses[ui++] = u;
if (nuse > 1) qsort(uses, (size_t)nuse, sizeof *uses, use_node_cmp);
int rc = 0;
for (int i = 0; i < nuse && rc == 0; i++) {
Node *u = uses[i];
const char *name = u->usesource ? u->usesource
: u->usepath ? u->usepath : u->str;
if (reserved_import_path(name)) {
errorf(u->pos, "package path %s is reserved", name);
rc = -1;
break;
}
size_t pathlen = strlen(name);
char *path_form = malloc(pathlen + 1);
if (path_form == NULL) {
sep_fail_nomem();
rc = -1;
break;
}
if (import_path_form(name, path_form, pathlen + 1) < 0) {
free(path_form);
sep_fail_size();
rc = -1;
break;
}
struct sepresolved resolved = {0};
int external_production = 0;
/* External tests import their colocated production package through the
* same source spelling rules. Preserve the exact route/root of this edge;
* a direct expanded spelling is still rejected below. */
int located = sep_resolve_source_import(g, context, name,
path_form, &resolved);
/* A directly selected literal external-test root may not yet have
* an ordinary identity. Only after normal source resolution has found
* no candidate may its short self import bind colocated production. */
if (located == 0 && g->pkg[pi].variant == SEP_VARIANT_EXTERNAL) {
const char *route_suffix = sep_vendor_route_suffix(
g->context[context].route);
int literal_self = route_suffix != NULL
? strcmp(path_form, route_suffix) == 0
: strchr(name, '.') == NULL
&& sep_external_production_name(&g->pkg[pi], name);
if (literal_self) {
if (g->pkg[pi].import_base != NULL)
resolved.identity = strdup(g->pkg[pi].import_base);
else
resolved.identity = sep_local_import_base(&g->pkg[pi]);
resolved.entry = strdup(g->context[context].route);
resolved.source_root = strdup(
g->context[context].source_root);
if (resolved.identity == NULL || resolved.entry == NULL
|| resolved.source_root == NULL) {
if (!sep_fatal_allocation) sep_fail_nomem();
sep_resolved_free(&resolved);
free(path_form);
rc = -1;
break;
}
located = 1;
}
}
free(path_form);
if (located < 0) {
sep_resolved_free(&resolved);
rc = -1;
break;
}
if (!located) {
int inline_package = 0;
if (!g->pkg[pi].is_dir)
for (Node *package = imports->body; package;
package = package->next)
if (strcmp(package->module, name) == 0) {
inline_package = 1;
break;
}
if (inline_package) {
if (sep_binding_add(bindings, 'I', name, -1,
u->pos) < 0)
rc = -1;
continue;
}
errorf(u->pos, "cannot find package %s", name);
rc = -1;
break;
}
{
errno = 0;
char *canon = realpath(resolved.entry, NULL);
if (canon == NULL) {
if (errno == ENOMEM) {
sep_fail_nomem();
sep_resolved_free(&resolved);
rc = -1;
break;
}
errorf(u->pos, "cannot canonicalize package '%s'", name);
sep_resolved_free(&resolved);
rc = -1;
break;
}
int self = strcmp(canon, g->pkg[pi].canon) == 0;
if (self && g->pkg[pi].variant == SEP_VARIANT_EXTERNAL)
external_production = 1;
if (self && !external_production) {
const char *owner = g->pkg[pi].path[0]
? g->pkg[pi].path : g->pkg[pi].canon;
errorf(u->pos, "self-import: package '%s' cannot import itself",
owner[0] ? owner : "(root)");
free(canon);
sep_resolved_free(&resolved);
rc = -1;
break;
}
/* The external package's self-production import is the ordinary
* canonical production action for this directory. Discovery role and
* the external product's artifact name never create another action. */
int di = sep_find_or_add(g, resolved.identity,
resolved.entry, 1);
if (di < 0) {
free(canon);
sep_resolved_free(&resolved);
rc = -1;
break;
}
int allowed = sep_internal_import_allowed(&g->pkg[pi],
resolved.identity, resolved.entry);
if (allowed < 0) {
free(canon); sep_resolved_free(&resolved);
rc = -1; break;
}
if (!allowed) {
errorf(u->pos, "use of internal package %s not allowed",
resolved.identity);
free(canon);
sep_resolved_free(&resolved);
rc = SEP_LOAD_INTERNAL;
break;
}
allowed = sep_vendor_import_allowed(&g->pkg[pi],
resolved.identity, resolved.entry);
if (allowed < 0) {
free(canon); sep_resolved_free(&resolved);
rc = -1; break;
}
if (!allowed) {
errorf(u->pos, "use of vendored package not allowed");
free(canon);
sep_resolved_free(&resolved);
rc = SEP_LOAD_VENDOR;
break;
}
const char *suffix;
size_t parents;
if (sep_vendor_suffix(resolved.identity, &suffix, &parents)
&& strcmp(name, suffix) != 0) {
(void)parents;
errorf(u->pos, "%s must be imported as %s",
resolved.identity, suffix);
free(canon);
sep_resolved_free(&resolved);
rc = SEP_LOAD_VENDOR;
break;
}
int child_context = sep_child_context_for(g, context,
resolved.entry, resolved.source_root);
if (child_context < 0
|| sep_binding_add(bindings, 'D', name, di, u->pos) < 0
|| sep_add_dep(g, pi, di) < 0
|| sep_children_add(children, di, child_context) < 0) {
free(canon);
sep_resolved_free(&resolved);
rc = -1;
break;
}
free(canon);
sep_resolved_free(&resolved);
}
}
free(uses);
freearena(a);
free(buf);
return rc;
}
static void
sep_import_set_free(struct ImportSet *s)
{
for (int i = 0; i < s->n; i++) free(s->paths[i]);
free(s->paths);
s->paths = NULL;
s->n = s->cap = 0;
}
static int
sep_dep_cmp(const struct sepgraph *g, int a, int b)
{
int r = strcmp(g->pkg[a].path, g->pkg[b].path);
if (r != 0) return r;
if (g->pkg[a].variant != g->pkg[b].variant)
return g->pkg[a].variant - g->pkg[b].variant;
if (g->pkg[a].role != g->pkg[b].role)
return g->pkg[a].role - g->pkg[b].role;
return strcmp(g->pkg[a].canon, g->pkg[b].canon);
}
static char *
sep_package_init_symbol(const struct seppkg *p)
{
if (p->path[0] == '\0')
return sep_sprintf("__ww..pkg.e.v%d.r%d.init",
p->variant, p->role);
return sep_sprintf("__ww..pkg.p.%s.v%d.r%d.init",
p->path, p->variant, p->role);
}
/* Add the compiler-owned test main as a real package action. Its identity is a
* pure function of the selected variant (and its one command-global support
* edge), never a product ordinal. Equivalent products therefore reuse it. */
static int
sep_add_generated_main(struct sepgraph *g, struct sepproduct *product,
int ordinal, int support)
{
(void)ordinal;
int variant = product->variant_root;
if (variant < 0 || variant >= g->n) return -1;
const char *kind = "production";
if (g->pkg[variant].variant == SEP_VARIANT_SAME_TEST)
kind = "internal";
else if (g->pkg[variant].variant == SEP_VARIANT_EXTERNAL)
kind = "external";
char *path = sep_sprintf("__wwtestmain.%s.%s.main",
g->pkg[variant].path, kind);
const char *variant_artifact = g->pkg[variant].artifact != NULL
? g->pkg[variant].artifact : g->pkg[variant].path;
char *artifact = sep_sprintf("%s-main", variant_artifact);
char *canon = sep_sprintf("%s#%s-test-main",
g->pkg[variant].canon, kind);
char *entry = strdup(g->pkg[variant].entry);
if (entry == NULL) sep_fail_nomem();
if (path == NULL || artifact == NULL || canon == NULL || entry == NULL) {
free(path); free(artifact); free(canon); free(entry);
return -1;
}
for (int i = 0; i < g->n; i++) {
if (strcmp(g->pkg[i].path, path) != 0) continue;
if (g->pkg[i].generated_main) {
int wants_support = support >= 0 && support != variant;
int has_variant = 0, has_support = 0;
for (int k = 0; k < g->pkg[i].ndeps; k++) {
if (g->pkg[i].deps[k] == variant) has_variant = 1;
if (wants_support && g->pkg[i].deps[k] == support)
has_support = 1;
}
if (has_variant && has_support == wants_support
&& g->pkg[i].ndeps == 1 + wants_support) {
free(path); free(artifact); free(canon); free(entry);
return i;
}
}
fprintf(stderr,
"ww: generated test-main package identity collides with source import %s\n",
path);
free(path); free(artifact); free(canon); free(entry);
return -1;
}
if (g->n == INT_MAX) {
sep_fail_size();
free(path); free(artifact); free(canon); free(entry);
return -1;
}
if (sep_reserve_packages(g, g->n + 1) < 0) {
free(path); free(artifact); free(canon); free(entry);
return -1;
}
struct seppkg *p = &g->pkg[g->n];
memset(p, 0, sizeof *p);
p->path = path;
p->artifact = artifact;
p->canon = canon;
p->entry = entry;
p->name = strdup("main");
if (p->name == NULL) {
sep_fail_nomem();
goto fail;
}
p->variant = SEP_VARIANT_TEST_MAIN;
p->role = SEP_ROLE_GENERATED_MAIN;
p->root = 1;
p->link_entry = 1;
p->generated_main = 1;
p->generated_target = variant;
p->loaded = 1;
p->emit_context = product->context;
if (sep_set_context_state(p, product->context, 2) < 0
|| sep_add_dep(g, g->n, variant) < 0
|| (support >= 0 && support != variant
&& sep_add_dep(g, g->n, support) < 0))
goto fail;
for (int i = 1; i < p->ndeps; i++) {
int v = p->deps[i];
int j = i;
while (j > 0 && sep_dep_cmp(g, p->deps[j - 1], v) > 0) {
p->deps[j] = p->deps[j - 1];
j--;
}
p->deps[j] = v;
}
return g->n++;
fail:
free(p->path);
free(p->artifact);
free(p->canon);
free(p->entry);
free(p->name);
free(p->context_state);
free(p->deps);
memset(p, 0, sizeof *p);
return -1;
}
/* Load one action's owned sources and direct bindings under one context.
* Dependency descent is iterative below so a valid deep graph consumes the
* growable frame vector rather than the process call stack. */
static int
sep_prepare_pkg_context(struct sepgraph *g, int pi, int context,
struct sepchildren *children)
{
if (sep_set_context_state(&g->pkg[pi], context, 1) < 0)
return -1;
struct ImportSet fv = {0};
struct sepbindset bindings = {0};
int rc = 0;
if (!g->pkg[pi].loaded) {
g->pkg[pi].loaded = 1;
if (g->pkg[pi].is_dir) {
const char *test_package = g->pkg[pi].test_package;
g->pkg[pi].nsources = enumerate_dir_ww(g->pkg[pi].entry,
g->pkg[pi].variant, test_package,
&g->pkg[pi].sources);
if (g->pkg[pi].nsources == -2) {
rc = -1; /* diagnosed in enumerate_dir_ww */
} else if (g->pkg[pi].nsources < 0) {
fprintf(stderr, "ww: cannot read directory %s\n",
g->pkg[pi].entry);
rc = -1;
} else if (g->pkg[pi].nsources == 0) {
fprintf(stderr,
"ww: %s: directory contains no WW package sources\n",
g->pkg[pi].entry);
rc = -1;
}
}
}
if (g->pkg[pi].is_dir) {
for (int i = 0; i < g->pkg[pi].nsources && rc == 0; i++)
rc = sep_scan_file(g, pi, g->pkg[pi].sources[i],
context, &fv, &bindings, children, 1);
} else if (rc == 0) {
rc = sep_scan_file(g, pi, g->pkg[pi].entry, context,
&fv, &bindings, children, 0);
}
sep_import_set_free(&fv);
if (bindings.n > 1)
qsort(bindings.v, (size_t)bindings.n,
sizeof *bindings.v, sep_binding_cmp);
if (rc == 0 && sep_validate_bindings(g, &bindings) < 0) rc = -1;
if (rc == 0 && g->pkg[pi].emit_context < 0) {
g->pkg[pi].bindings = bindings;
bindings.v = NULL;
bindings.n = bindings.cap = 0;
g->pkg[pi].emit_context = context;
} else if (rc == 0) {
struct sepbindset *want = &g->pkg[pi].bindings;
if (!sep_bindsets_semantically_same(want, &bindings)) rc = -1;
if (rc < 0) {
const char *first =
g->context[g->pkg[pi].emit_context].root;
const char *second = g->context[context].root;
if (strcmp(first, second) > 0) {
const char *swap = first; first = second; second = swap;
}
fprintf(stderr,
"ww: package %s resolves imports differently in %s and %s\n",
g->pkg[pi].path[0] ? g->pkg[pi].path : g->pkg[pi].canon,
first, second);
}
}
sep_bindset_free(&bindings);
if (rc < 0) {
g->pkg[pi].context_state[context] = 2;
g->pkg[pi].failed = 1;
return rc;
}
for (int i = 1; i < g->pkg[pi].ndeps; i++) {
int v = g->pkg[pi].deps[i];
int j = i;
while (j > 0 && sep_dep_cmp(g, g->pkg[pi].deps[j - 1], v) > 0) {
g->pkg[pi].deps[j] = g->pkg[pi].deps[j - 1];
j--;
}
g->pkg[pi].deps[j] = v;
}
/* Mark before recursion so a source cycle terminates here; topo emits the
* stable cycle diagnostic after all direct bindings are known. */
g->pkg[pi].context_state[context] = 2;
return 0;
}
struct seploadframe {
int pkg;
int context;
int next_child;
int pending_dep;
struct sepchildren children;
};
/* Load one canonical package under one selected-root resolution context.
* Source membership is owned once, but a shared package's imports are checked
* under every context that reaches it. The first canonical binding set owns
* file-body composition; every later set must be identical. */
static int
sep_load_pkg(struct sepgraph *g, int pi, int context)
{
if (pi < 0 || pi >= g->n || context < 0 || context >= g->ncontext)
return -1;
if (g->pkg[pi].test_support && g->support_context >= 0)
context = g->support_context;
struct seploadframe *frames = NULL;
int nframe = 0, framecap = 0;
int result = -1;
if (sep_reserve((void **)&frames, &framecap, 1,
sizeof *frames) < 0)
return -2;
frames[nframe++] = (struct seploadframe){ pi, context, -1, -1, {0} };
while (nframe > 0) {
struct seploadframe *f = &frames[nframe - 1];
if (f->next_child < 0) {
unsigned char state = sep_context_state(&g->pkg[f->pkg],
f->context);
if (state == 2) {
if (g->pkg[f->pkg].failed) goto failed;
sep_children_free(&f->children);
nframe--;
continue;
}
if (state == 1) {
sep_children_free(&f->children);
nframe--;
continue;
}
int prepared = sep_prepare_pkg_context(g, f->pkg, f->context,
&f->children);
if (prepared < 0) {
result = prepared;
goto failed;
}
f->next_child = 0;
}
if (f->pending_dep >= 0) {
int dep = f->pending_dep;
f->pending_dep = -1;
if (dep != f->pkg
&& sep_external_production_edge(g, f->pkg, dep)
&& !sep_external_name_matches_production(g, f->pkg, dep)) {
fprintf(stderr,
"ww: external test package %s does not match production package %s\n",
g->pkg[f->pkg].name, g->pkg[dep].name);
goto failed;
}
if (dep != f->pkg
&& sep_forbidden_command_import(g, f->pkg, dep)) {
fprintf(stderr,
"ww: package %s is a program, not an importable package\n",
g->pkg[dep].path[0] ? g->pkg[dep].path
: g->pkg[dep].canon);
goto failed;
}
}
if (f->next_child >= f->children.n) {
sep_children_free(&f->children);
nframe--;
continue;
}
struct sepchild child = f->children.v[f->next_child++];
int dep = child.pkg;
f->pending_dep = dep;
int child_context = child.context;
if (g->pkg[dep].test_support && g->support_context >= 0)
child_context = g->support_context;
if (nframe == INT_MAX) {
sep_fail_size();
for (int i = 0; i < nframe; i++) {
g->pkg[frames[i].pkg].failed = 1;
sep_children_free(&frames[i].children);
}
free(frames);
return -2;
}
if (sep_reserve((void **)&frames, &framecap, nframe + 1,
sizeof *frames) < 0) {
for (int i = 0; i < nframe; i++) {
g->pkg[frames[i].pkg].failed = 1;
sep_children_free(&frames[i].children);
}
free(frames);
return -2;
}
frames[nframe++] = (struct seploadframe){
dep, child_context, -1, -1, {0} };
}
free(frames);
return 0;
failed:
for (int i = 0; i < nframe; i++) {
g->pkg[frames[i].pkg].failed = 1;
sep_children_free(&frames[i].children);
}
free(frames);
return sep_fatal_allocation ? -2 : result;
}
static int
sep_import_component(const char *s, size_t n)
{
if (n == 0 || !((s[0] >= 'a' && s[0] <= 'z')
|| (s[0] >= 'A' && s[0] <= 'Z') || s[0] == '_'))
return 0;
for (size_t i = 1; i < n; i++)
if (!((s[i] >= 'a' && s[i] <= 'z')
|| (s[i] >= 'A' && s[i] <= 'Z')
|| (s[i] >= '0' && s[i] <= '9') || s[i] == '_'))
return 0;
return kwlookup(s, (u64)n) == TK_NONE;
}
static int
sep_import_path_from_relative(const char *rel, char *out, size_t outsz)
{
size_t off = 0;
const char *p = rel;
while (*p != '\0') {
const char *slash = strchr(p, '/');
size_t n = slash ? (size_t)(slash - p) : strlen(p);
if (!sep_import_component(p, n)) return 0;
if (off + n + (slash != NULL) + 1 > outsz) return -1;
memcpy(out + off, p, n);
off += n;
if (slash == NULL) break;
out[off++] = '.';
p = slash + 1;
}
out[off] = '\0';
return off != 0;
}
/* A literal directory selected below an applicable source root already has a
* complete lexical identity. Bind it before any source edge can reach the
* same physical directory under a different vendored route. */
static int
sep_context_import_base(const struct sepgraph *g, int context, char **out)
{
*out = NULL;
if (context < 0 || context >= g->ncontext) return -1;
const struct sepcontext *c = &g->context[context];
if (strcmp(c->route, c->source_root) == 0) return 0;
const char *rel = NULL;
if (!sep_lexical_relative(c->source_root, c->route, &rel)
|| rel == NULL || rel[0] == '\0') {
fprintf(stderr, "ww: package route %s is outside source root %s\n",
c->route, c->source_root);
return -1;
}
size_t n = strlen(rel);
char *base = malloc(n + 1);
if (base == NULL) return sep_fail_nomem();
int converted = sep_import_path_from_relative(rel, base, n + 1);
if (converted <= 0 || reserved_import_path(base)) {
if (converted < 0) sep_fail_size();
else fprintf(stderr, "ww: invalid package path %s\n", rel);
free(base);
return -1;
}
*out = base;
return 1;
}
/* A reverse candidate is authoritative only when the normal ordered forward
* lookup selects this exact canonical directory. This prevents a later or
* nested source root from manufacturing an alias shadowed by an earlier root. */
static int
sep_reverse_import_base(const struct sepgraph *g, const struct seppkg *pkg,
int context, char *out, size_t outsz)
{
const char *searchpath = g->context[context].searchpath;
const char *p = searchpath;
while (*p != '\0') {
const char *e = strchr(p, ':');
size_t n = e ? (size_t)(e - p) : strlen(p);
char *root = malloc(n + 1);
if (root == NULL) return sep_fail_nomem();
memcpy(root, p, n);
root[n] = '\0';
errno = 0;
char *canon = n == 0 ? NULL : realpath(root, NULL);
int canon_errno = n == 0 ? 0 : errno;
free(root);
if (canon == NULL && canon_errno == ENOMEM)
return sep_fail_nomem();
if (canon != NULL) {
size_t rn = strlen(canon);
const char *rel = NULL;
if (rn == 1 && canon[0] == '/' && pkg->canon[0] == '/'
&& pkg->canon[1] != '\0')
rel = pkg->canon + 1;
else if (strncmp(pkg->canon, canon, rn) == 0
&& pkg->canon[rn] == '/' && pkg->canon[rn + 1] != '\0')
rel = pkg->canon + rn + 1;
if (rel != NULL) {
int ir = sep_import_path_from_relative(rel, out, outsz);
if (ir < 0) { free(canon); return -1; }
if (ir > 0 && reserved_import_path(out)) ir = 0;
if (ir > 0) {
char located[PATH_MAX];
if (locate_import(searchpath, rel, located,
sizeof located)) {
errno = 0;
char *selected = realpath(located, NULL);
int selected_errno = errno;
int same = selected != NULL
&& strcmp(selected, pkg->canon) == 0;
if (selected == NULL
&& selected_errno == ENOMEM) {
free(canon);
return sep_fail_nomem();
}
free(selected);
if (same) { free(canon); return 1; }
}
}
}
free(canon);
}
if (!e) break;
p = e + 1;
}
return 0;
}
/* The reserved local namespace is reversible, so filesystem identity never
* depends on a hash, request order, output name, declared package name, or
* another selected package. */
static char *
sep_local_import_base(const struct seppkg *p)
{
size_t prefix = strlen(SEP_LOCAL_IMPORT_PREFIX);
size_t canon_len = strlen(p->canon);
if (canon_len > ((size_t)-1 - prefix - 3) / 4) {
sep_fail_size();
return NULL;
}
size_t outsz = prefix + 2 + 4 * canon_len + 1;
char *out = malloc(outsz);
if (out == NULL) {
sep_fail_nomem();
return NULL;
}
size_t off = 0;
int n = snprintf(out, outsz, "%s.p", SEP_LOCAL_IMPORT_PREFIX);
if (n < 0 || (size_t)n >= outsz) { free(out); return NULL; }
off = (size_t)n;
static const char hex[] = "0123456789abcdef";
for (const unsigned char *s = (const unsigned char *)p->canon;
*s != '\0'; s++) {
unsigned char c = *s;
if ((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z')
|| (c >= '0' && c <= '9')) {
if (off + 1 >= outsz) { free(out); return NULL; }
out[off++] = (char)c;
} else if (c == '_' || c == '/') {
if (off + 2 >= outsz) { free(out); return NULL; }
out[off++] = '_';
out[off++] = c == '_' ? 'u' : 's';
} else {
if (off + 4 >= outsz) { free(out); return NULL; }
out[off++] = '_';
out[off++] = 'x';
out[off++] = hex[c >> 4];
out[off++] = hex[c & 15];
}
}
out[off] = '\0';
return out;
}
/* Finalization verifies every reached context before generated-main creation.
* Source bindings and explicit lookup identities remain authoritative; a
* literal root either round-trips through an active root or receives the
* reserved reversible local identity. */
static int
sep_finalize_directory_identities(struct sepgraph *g)
{
for (int pi = 0; pi < g->n; pi++) {
struct seppkg *p = &g->pkg[pi];
if (!p->is_dir || p->generated_main || p->failed || !p->loaded
|| p->role == SEP_ROLE_TEST_SUPPORT
|| p->import_base != NULL)
continue;
for (int ci = 0; ci < g->ncontext; ci++) {
if (sep_context_state(p, ci) != 2) continue;
size_t n = strlen(p->canon) + 1;
char *candidate = malloc(n);
if (candidate == NULL) return sep_fail_nomem();
int found = sep_reverse_import_base(g, p, ci, candidate, n);
if (found < 0) { free(candidate); return -1; }
if (found > 0 && sep_bind_import_base(g, pi, candidate) < 0) {
free(candidate);
return -1;
}
free(candidate);
}
}
for (int pi = 0; pi < g->n; pi++) {
struct seppkg *p = &g->pkg[pi];
if (!p->is_dir || p->generated_main || p->failed || !p->loaded
|| p->import_base != NULL)
continue;
const char *base = NULL;
for (int i = 0; i < g->n; i++) {
if (i == pi || !g->pkg[i].is_dir || g->pkg[i].generated_main
|| g->pkg[i].role == SEP_ROLE_TEST_SUPPORT
|| p->role == SEP_ROLE_TEST_SUPPORT
|| strcmp(g->pkg[i].canon, p->canon) != 0
|| g->pkg[i].import_base == NULL
|| (p->root && sep_path_is_vendored(
g->pkg[i].import_base)))
continue;
base = g->pkg[i].import_base;
break;
}
char *local = NULL;
if (base == NULL) {
local = sep_local_import_base(p);
if (local == NULL) return -1;
base = local;
}
if (sep_bind_import_base(g, pi, base) < 0) {
free(local);
return -1;
}
free(local);
}
for (int pi = 0; pi < g->n; pi++) {
struct seppkg *p = &g->pkg[pi];
if (!p->is_dir || p->generated_main || p->failed || !p->loaded)
continue;
free(p->artifact);
p->artifact = NULL;
if (p->variant == SEP_VARIANT_SAME_TEST)
p->artifact = sep_sprintf("%s-internal-test", p->path);
else if (p->variant == SEP_VARIANT_EXTERNAL)
p->artifact = sep_sprintf("%s-external-test", p->path);
if ((p->variant == SEP_VARIANT_SAME_TEST
|| p->variant == SEP_VARIANT_EXTERNAL) && p->artifact == NULL)
return -1;
}
return 0;
}
struct septopoframe {
int pkg;
int next_dep;
};
/* Iterative DFS post-order over the dep DAG → reverse-topo (deps before
* importer). Tri-color and `stack[0..nframe)` retain the exact live path and
* cycle diagnostic without tying valid graph depth to the C call stack. */
static int
sep_topo_visit(struct sepgraph *g, int pi, int *order, int *no,
int *stack, int depth)
{
(void)depth;
if (g->pkg[pi].color == 2) return 0;
if (g->pkg[pi].color == 1) {
fprintf(stderr, "ww: dependency cycle: %s\n",
g->pkg[pi].path[0] ? g->pkg[pi].path : "(root)");
return -1;
}
struct septopoframe *frames = NULL;
int nframe = 0, framecap = 0;
if (sep_reserve((void **)&frames, &framecap, 1,
sizeof *frames) < 0)
return -2;
g->pkg[pi].color = 1;
stack[0] = pi;
frames[nframe++] = (struct septopoframe){ pi, 0 };
while (nframe > 0) {
struct septopoframe *f = &frames[nframe - 1];
if (f->next_dep < g->pkg[f->pkg].ndeps) {
int dep = g->pkg[f->pkg].deps[f->next_dep++];
if (g->pkg[dep].color == 2) continue;
if (g->pkg[dep].color == 1) {
int j = 0;
while (j < nframe && stack[j] != dep) j++;
fprintf(stderr, "ww: dependency cycle: ");
for (int s = j; s < nframe; s++)
fprintf(stderr, "%s -> ",
g->pkg[stack[s]].path[0]
? g->pkg[stack[s]].path : "(root)");
fprintf(stderr, "%s\n", g->pkg[dep].path[0]
? g->pkg[dep].path : "(root)");
free(frames);
return -1;
}
if (nframe == INT_MAX) {
sep_fail_size();
free(frames);
return -2;
}
if (sep_reserve((void **)&frames, &framecap, nframe + 1,
sizeof *frames) < 0) {
free(frames);
return -2;
}
g->pkg[dep].color = 1;
stack[nframe] = dep;
frames[nframe++] = (struct septopoframe){ dep, 0 };
continue;
}
g->pkg[f->pkg].color = 2;
order[(*no)++] = f->pkg;
nframe--;
}
free(frames);
return 0;
}
/* Internal test variants replace their colocated production action in the
* corresponding test link closure. Canonical package identity has already
* made every remaining compiler qualifier globally unambiguous. */
static int
sep_internal_replaces_production(const struct sepgraph *g, int a, int b)
{
const struct seppkg *internal = &g->pkg[a];
const struct seppkg *production = &g->pkg[b];
if (internal->variant != SEP_VARIANT_SAME_TEST) {
internal = &g->pkg[b];
production = &g->pkg[a];
}
return internal->variant == SEP_VARIANT_SAME_TEST
&& production->variant == SEP_VARIANT_PRODUCTION
&& production->role != SEP_ROLE_TEST_SUPPORT
&& strcmp(internal->canon, production->canon) == 0;
}
/* A production action is physically omitted from an internal-test product
* because the augmented variant owns those same production sources. Map every
* edge through that replacement before scheduling initialization, exactly as
* the link-closure filter does. */
static int
sep_init_effective(const struct sepgraph *g, int variant_root, int pi)
{
if (variant_root >= 0 && variant_root < g->n
&& sep_internal_replaces_production(g, variant_root, pi))
return variant_root;
return pi;
}
static int
sep_init_cmp(const struct sepgraph *g, int a, int b)
{
int r = strcmp(g->pkg[a].path, g->pkg[b].path);
if (r != 0) return r;
if (g->pkg[a].variant != g->pkg[b].variant)
return g->pkg[a].variant - g->pkg[b].variant;
return g->pkg[a].role - g->pkg[b].role;
}
/* Go's linker uses a lexical ready queue over the reachable init-task DAG.
* Compute that schedule explicitly: dependencies become ready first; among
* otherwise independent actions canonical package identity breaks ties. */
static int
sep_init_order(const struct sepgraph *g, int root, int variant_root,
int **out, int *nout)
{
unsigned char *active = calloc((size_t)g->n, 1);
unsigned char *done = calloc((size_t)g->n, 1);
int *todo = calloc((size_t)g->n, sizeof *todo);
int *order = calloc((size_t)g->n, sizeof *order);
if (active == NULL || done == NULL || todo == NULL || order == NULL) {
if (!sep_fatal_allocation) (void)sep_fail_nomem();
free(order); free(todo); free(done); free(active);
return -1;
}
int ntodo = 0;
int effective_root = sep_init_effective(g, variant_root, root);
active[effective_root] = 1;
todo[ntodo++] = effective_root;
while (ntodo > 0) {
int pi = todo[--ntodo];
for (int k = 0; k < g->pkg[pi].ndeps; k++) {
int dep = sep_init_effective(g, variant_root,
g->pkg[pi].deps[k]);
if (!active[dep]) {
active[dep] = 1;
todo[ntodo++] = dep;
}
}
}
int nactive = 0;
for (int pi = 0; pi < g->n; pi++) if (active[pi]) nactive++;
int no = 0;
while (no < nactive) {
int best = -1;
for (int pi = 0; pi < g->n; pi++) {
if (!active[pi] || done[pi]) continue;
int blocked = 0;
for (int k = 0; k < g->pkg[pi].ndeps; k++) {
int dep = sep_init_effective(g, variant_root,
g->pkg[pi].deps[k]);
if (dep != pi && active[dep] && !done[dep]) {
blocked = 1;
break;
}
}
if (!blocked && (best < 0 || sep_init_cmp(g, pi, best) < 0))
best = pi;
}
if (best < 0) {
fprintf(stderr, "ww: dependency cycle in initialization closure\n");
free(order); free(todo); free(done); free(active);
return -1;
}
done[best] = 1;
order[no++] = best;
}
free(todo); free(done); free(active);
*out = order;
*nout = no;
return 0;
}
static int
sep_compose_init_dispatch(const struct sepgraph *g,
const struct sepproduct *product, const char *unitpath,
const char *asmpath)
{
int *order = NULL, norder = 0;
if (sep_init_order(g, product->root, product->variant_root,
&order, &norder) < 0)
return -1;
FILE *unit = fopen(unitpath, "wb");
if (unit == NULL) {
fprintf(stderr, "ww: cannot open %s\n", unitpath);
free(order);
return -1;
}
int bad = fprintf(unit, "//ww:init-root %s\n",
g->pkg[product->root].init_symbol) < 0;
for (int i = 0; i < norder && !bad; i++)
if (fprintf(unit, "//ww:init-call %s\n",
g->pkg[order[i]].init_symbol) < 0)
bad = 1;
if (fclose(unit) != 0) bad = 1;
if (bad) {
fprintf(stderr, "ww: cannot write initialization unit\n");
free(order);
return -1;
}
FILE *out = fopen(asmpath, "wb");
if (out == NULL) {
fprintf(stderr, "ww: cannot open %s\n", asmpath);
(void)unlink(unitpath);
free(order);
return -1;
}
bad = fputs("TEXT __ww..dispatch,$0\n"
"\tPUSHQ\tBP\n"
"\tMOVQ\tSP, BP\n"
"\tSUBQ\t$0, SP\n", out) == EOF;
for (int i = 0; i < norder && !bad; i++)
if (fprintf(out, "\tCALL\t%s(SB)\n",
g->pkg[order[i]].init_symbol) < 0)
bad = 1;
if (!bad && fputs("\tMOVQ\t$0, AX\n"
"\tMOVQ\tBP, SP\n"
"\tPOPQ\tBP\n"
"\tRET\n", out) == EOF)
bad = 1;
if (fclose(out) != 0) bad = 1;
free(order);
if (bad) {
fprintf(stderr, "ww: cannot write initialization assembly\n");
(void)unlink(unitpath);
(void)unlink(asmpath);
return -1;
}
return 0;
}
static int
sep_validate_module_closure(struct sepgraph *g, const int *order, int n,
int include_root)
{
for (int i = 0; i < n; i++) {
int a = order[i];
if ((!include_root && g->pkg[a].root)
|| g->pkg[a].path[0] == '\0') continue;
for (int j = i + 1; j < n; j++) {
int b = order[j];
if (!include_root && g->pkg[b].root) continue;
if (strcmp(g->pkg[a].path, g->pkg[b].path) == 0
&& !sep_internal_replaces_production(g, a, b)) {
fprintf(stderr,
"ww: product closure contains multiple packages named %s\n",
g->pkg[a].path);
return -1;
}
}
}
return 0;
}
/* Emit one of pi's own source files into the sep-unit under the
* //ww:module-reset primary boundary. No source or export outside pi's
* sorted owned-source set may enter this unit. */
static int
sep_emit_body(FILE *out, const char *path, const char *modpath)
{
char *buf;
u64 len;
if (sep_slurp(path, &buf, &len) < 0) {
fprintf(stderr, "ww: cannot read %s\n", path);
return -1;
}
/* #57: tag the primary body by its full dotted import path so the
* definer mangles == the importer reference; a root build (path "")
* stays a bare reset (keeps bare main). */
int bad = 0;
if (modpath != NULL && modpath[0] != '\0') {
if (fprintf(out, "//ww:module-reset %s\n", modpath) < 0)
bad = 1;
} else if (fputs("//ww:module-reset\n", out) == EOF) {
bad = 1;
}
if (fwrite(buf, 1, (size_t)len, out) != (size_t)len
|| fputc('\n', out) == EOF)
bad = 1;
free(buf);
if (bad) {
fprintf(stderr, "ww: cannot write package unit\n");
return -1;
}
return 0;
}
/* Filesystem paths are opaque byte strings. Preserve them in the persistent
* identity without letting a newline in a legal path escape its comment. */
static int
sep_emit_hex(FILE *out, const char *value)
{
static const char hex[] = "0123456789abcdef";
for (const unsigned char *p = (const unsigned char *)value; *p; p++)
if (fputc(hex[*p >> 4], out) == EOF
|| fputc(hex[*p & 15], out) == EOF)
return -1;
return 0;
}
static int
sep_emit_file_hex(FILE *out, const char *path)
{
FILE *in = fopen(path, "rb");
if (in == NULL) return -1;
static const char hex[] = "0123456789abcdef";
unsigned char buf[65536];
int bad = 0;
for (;;) {
size_t n = fread(buf, 1, sizeof buf, in);
for (size_t i = 0; i < n; i++)
if (fputc(hex[buf[i] >> 4], out) == EOF
|| fputc(hex[buf[i] & 15], out) == EOF) {
bad = 1;
break;
}
if (bad || n < sizeof buf) {
if (ferror(in)) bad = 1;
break;
}
}
if (fclose(in) != 0) bad = 1;
return bad ? -1 : 0;
}
/* Compose pi's sep-unit at `unitf` from only pi's byte-sorted sources.
* Direct exports are separate compiler inputs; the linker separately retains
* the reachable archive closure. */
static int
sep_compose_unit(struct sepgraph *g, int pi, const char *scratch,
const char *unitf)
{
if (g->pkg[pi].emit_context < 0
|| g->pkg[pi].emit_context >= g->ncontext)
return -1;
FILE *u = fopen(unitf, "wb");
if (u == NULL) {
fprintf(stderr, "ww: cannot open %s\n", unitf);
return -1;
}
int bodyrc = 0;
if (g->pkg[pi].generated_main) {
if (fprintf(u, "//ww:module-reset %s\npackage main;\n",
g->pkg[pi].path) < 0)
bodyrc = -1;
for (int i = 0; i < g->pkg[pi].ndeps && bodyrc == 0; i++)
if (fprintf(u, "import %s;\n",
g->pkg[g->pkg[pi].deps[i]].path) < 0)
bodyrc = -1;
} else if (g->pkg[pi].is_dir) {
for (int i = 0; i < g->pkg[pi].nsources && bodyrc == 0; i++)
bodyrc = sep_emit_body(u, g->pkg[pi].sources[i],
g->pkg[pi].path);
} else {
bodyrc = sep_emit_body(u, g->pkg[pi].entry, g->pkg[pi].path);
}
const char *own_suffix;
size_t own_parents;
if (bodyrc == 0 && sep_vendor_suffix(g->pkg[pi].path,
&own_suffix, &own_parents)) {
(void)own_suffix;
(void)own_parents;
if (fputs("//ww:vendor-dir ", u) == EOF
|| sep_emit_hex(u, g->pkg[pi].canon) < 0
|| fputc('\n', u) == EOF)
bodyrc = -1;
}
/* The opaque source-spelling map is also part of the persistent unit
* voucher. Include the canonical target directory so a vendored symlink
* retarget cannot reuse stale assembly even when export bytes are equal. */
for (int i = 0; i < g->pkg[pi].bindings.n && bodyrc == 0; i++) {
struct sepbind *b = &g->pkg[pi].bindings.v[i];
if (!sep_binding_first_map(g, &g->pkg[pi].bindings, i)) continue;
if (fprintf(u, "//ww:import-map %s %s ",
b->name, g->pkg[b->dep].path) < 0
|| sep_emit_hex(u, g->pkg[b->dep].canon) < 0
|| fputc('\n', u) == EOF)
bodyrc = -1;
}
/* Direct export bytes complete the source-action voucher. A compiler
* rejection can therefore retain the previous committed unit safely: if a
* dependency export changed, the next request still sees a unit mismatch. */
for (int i = 0; i < g->pkg[pi].ndeps && bodyrc == 0; i++) {
int dep = g->pkg[pi].deps[i];
char interface[SEP_ARTIFACT_MAX];
const char *suffix = g->pkg[dep].source_staged
? ".wwi.new" : ".wwi";
if (sep_fname(g, dep, scratch, suffix, interface,
sizeof interface) < 0
|| fprintf(u, "//ww:direct-export %s ",
g->pkg[dep].path) < 0
|| sep_emit_file_hex(u, interface) < 0
|| fputc('\n', u) == EOF)
bodyrc = -1;
}
if (fclose(u) != 0) {
fprintf(stderr, "ww: cannot close package unit %s\n", unitf);
return -1;
}
return bodyrc;
}
/* archive_o — write a deterministic one- or two-member SysV ar archive at
* `apath` wrapping `objpath` and the optional `initpath`. No armap / long-name table:
* w6l reads each member's ELF .symtab directly (obj.c elf_globals) and
* skips '/'-named members, so a package `.a` needs only the global magic,
* fixed 60-byte member headers, and the `.o` bytes (newline-padded to even).
* Zeroed mtime/uid/gid + fixed mode + fixed member names make the bytes
* a pure function of the object content → cstage `.a` == wwstage `.a`
* (rule 10). The wwstage twin is archiveo (selfhost/cmd/ww/main.ww). */
static int
archive_member(FILE *out, const char *objpath, const char *member)
{
if (strlen(member) > 16)
return -1;
FILE *in = fopen(objpath, "rb");
if (in == NULL) {
fprintf(stderr, "ww: cannot read %s\n", objpath);
return -1;
}
if (fseek(in, 0, SEEK_END) != 0) { fclose(in); return -1; }
long n = ftell(in);
if (n < 0 || fseek(in, 0, SEEK_SET) != 0) {
fclose(in);
return -1;
}
/* ar(5) fixes each member header at 60 bytes; the offsets below
* address fields in that serialized header. */
char hdr[60];
memset(hdr, ' ', sizeof hdr);
memcpy(hdr + 0, member, strlen(member));
hdr[16] = '0'; /* mtime (zeroed → determinism) */
hdr[28] = '0'; /* uid (zeroed) */
hdr[34] = '0'; /* gid (zeroed) */
memcpy(hdr + 40, "100644", 6); /* mode (fixed octal) */
char sz[12];
int bad = 0;
int szn = snprintf(sz, sizeof sz, "%lu", (unsigned long)n);
if (szn <= 0 || szn > 10) bad = 1;
else memcpy(hdr + 48, sz, (size_t)szn);
hdr[58] = 0x60; /* member-header magic byte */
hdr[59] = 0x0a;
if (fwrite(hdr, 1, sizeof hdr, out) != sizeof hdr) bad = 1;
unsigned char buf[8192];
long remaining = n;
while (!bad && remaining > 0) {
size_t want = remaining > (long)sizeof buf
? sizeof buf : (size_t)remaining;
size_t got = fread(buf, 1, want, in);
if (got != want || fwrite(buf, 1, got, out) != got) {
bad = 1;
break;
}
remaining -= (long)got;
}
if (fclose(in) != 0) bad = 1;
if ((n & 1) && fputc('\n', out) == EOF) bad = 1;
return bad ? -1 : 0;
}
static int
archive_o(const char *objpath, const char *initpath, const char *apath)
{
FILE *out = fopen(apath, "wb");
if (out == NULL) {
fprintf(stderr, "ww: cannot open %s\n", apath);
return -1;
}
int bad = fwrite("!<arch>\n", 1, 8, out) != 8;
if (!bad && archive_member(out, objpath, "pkg.o/") < 0) bad = 1;
if (!bad && initpath != NULL
&& archive_member(out, initpath, "init.o/") < 0)
bad = 1;
if (fclose(out) != 0) bad = 1;
if (bad) {
fprintf(stderr, "ww: cannot write archive %s\n", apath);
return -1;
}
return 0;
}
/* -w workdir freshness: a `-w DIR` workdir is a caller-owned persistent
* package-artifact tree that replaces the fresh `.sepwork` scratch.
* Staleness is pure content identity, never mtime: a package is reused only
* when its freshly composed unit byte-equals the committed unit, no direct
* dependency emitted a changed export, AND the driver/tool copies recorded in
* the dir byte-equal the live executables — every decision is reproducible by
* hand with cmp(1) against plain files. Artifacts, units, tool records, stamp,
* products, and statuses stage together and publish through one rollback-
* capable request transaction, so a killed or rejected build cannot expose a
* mixed generation. The caller serializes invocations per workdir (Make target
* = one workdir), and `make clean` reclaims the state; the wwstage twin is the
* fileequal/workdirstamp/transaction group in selfhost/cmd/ww/main.ww. */
/* `.s`/`.wwi` may be legitimately empty (an FFI-only package like rt
* emits no text), so committed presence is their freshness test; the
* rename-commit protocol owns integrity. `.o`/`.a` are never empty
* (ELF/ar headers), so a zero size there is always a torn write. */
static int
file_is_reg(const char *path)
{
struct stat st;
return lstat(path, &st) == 0 && S_ISREG(st.st_mode);
}
static int
file_size_nonzero(const char *path)
{
struct stat st;
return lstat(path, &st) == 0 && S_ISREG(st.st_mode) && st.st_size > 0;
}
/* Existence checks for caller-visible staging and rollback paths must never
* follow a terminal symlink. A dangling OUT.new is occupied, not permission
* to truncate its target through a later fopen(3). */
static int
path_exists_nofollow(const char *path)
{
struct stat st;
if (lstat(path, &st) == 0) return 1;
return errno == ENOENT ? 0 : -1;
}
/* Byte equality of two files; absence or IO error is inequality. */
static int
file_equal(const char *a, const char *b)
{
FILE *fa = fopen(a, "rb");
if (fa == NULL) return 0;
FILE *fb = fopen(b, "rb");
if (fb == NULL) { fclose(fa); return 0; }
static char ba[8192], bb[8192];
int eq = 1;
for (;;) {
size_t na = fread(ba, 1, sizeof ba, fa);
size_t nb = fread(bb, 1, sizeof bb, fb);
if (na != nb || memcmp(ba, bb, na) != 0) { eq = 0; break; }
if (na < sizeof ba) {
if (ferror(fa) || ferror(fb)) eq = 0;
break;
}
}
fclose(fa); fclose(fb);
return eq;
}
static int
copy_file_stage(const char *src, const char *dst)
{
FILE *in = fopen(src, "rb");
if (in == NULL) return -1;
FILE *out = fopen(dst, "wb");
if (out == NULL) { fclose(in); return -1; }
unsigned char buf[65536];
int bad = 0;
for (;;) {
size_t n = fread(buf, 1, sizeof buf, in);
if (n != 0 && fwrite(buf, 1, n, out) != n) bad = 1;
if (bad || n < sizeof buf) {
if (ferror(in)) bad = 1;
break;
}
}
if (fclose(in) != 0) bad = 1;
if (fclose(out) != 0) bad = 1;
if (bad) (void)unlink(dst);
return bad ? -1 : 0;
}
struct septxnentry {
char *stage;
char *dst;
char *backup;
int had_old;
int installed;
};
struct septxn {
struct septxnentry *v;
int n, cap;
};
static int
sep_txn_add(struct septxn *tx, const char *stage, const char *dst)
{
if (strcmp(stage, dst) == 0) {
fprintf(stderr, "ww: transaction path collision: %s\n", dst);
return -1;
}
for (int i = 0; i < tx->n; i++)
if (strcmp(tx->v[i].dst, dst) == 0
|| strcmp(tx->v[i].stage, stage) == 0
|| strcmp(tx->v[i].dst, stage) == 0
|| strcmp(tx->v[i].stage, dst) == 0) {
fprintf(stderr, "ww: transaction path collision: %s\n", dst);
return -1;
}
if (tx->n == INT_MAX || sep_reserve((void **)&tx->v, &tx->cap,
tx->n + 1, sizeof *tx->v) < 0)
return -1;
struct septxnentry *e = &tx->v[tx->n];
memset(e, 0, sizeof *e);
e->stage = strdup(stage);
e->dst = strdup(dst);
e->backup = sep_sprintf("%s.wwtxn.%ld.old", dst, (long)getpid());
if (e->stage == NULL || e->dst == NULL || e->backup == NULL) {
sep_fail_nomem();
free(e->backup); free(e->dst); free(e->stage);
memset(e, 0, sizeof *e);
return -1;
}
if (strlen(e->backup) + 1 > (size_t)PATH_MAX) {
fprintf(stderr, "ww: transaction path is too long\n");
free(e->backup); free(e->dst); free(e->stage);
memset(e, 0, sizeof *e);
return -1;
}
tx->n++;
return 0;
}
static void
sep_txn_discard(struct septxn *tx)
{
for (int i = 0; i < tx->n; i++)
if (tx->v[i].stage != NULL)
(void)unlink(tx->v[i].stage);
}
static void
sep_txn_free(struct septxn *tx)
{
for (int i = 0; i < tx->n; i++) {
free(tx->v[i].backup);
free(tx->v[i].dst);
free(tx->v[i].stage);
}
free(tx->v);
memset(tx, 0, sizeof *tx);
}
/* One request-wide rollback group: producers and linkers finish first; only
* then are old destinations parked and all staged files installed. */
static int
sep_txn_commit(struct septxn *tx)
{
for (int i = 0; i < tx->n; i++)
if (!file_is_reg(tx->v[i].stage)) {
fprintf(stderr, "ww: transaction stage is not a regular file: %s\n",
tx->v[i].stage);
goto rollback;
}
for (int i = 0; i < tx->n; i++)
if (path_exists_nofollow(tx->v[i].backup) != 0) {
fprintf(stderr, "ww: transaction backup already exists: %s\n",
tx->v[i].backup);
goto rollback;
}
for (int i = 0; i < tx->n; i++) {
if (rename(tx->v[i].dst, tx->v[i].backup) == 0)
tx->v[i].had_old = 1;
else if (errno != ENOENT) {
fprintf(stderr, "ww: cannot preserve transaction destination %s\n",
tx->v[i].dst);
goto rollback;
}
}
for (int i = 0; i < tx->n; i++) {
if (rename(tx->v[i].stage, tx->v[i].dst) != 0) {
fprintf(stderr, "ww: cannot install transaction destination %s\n",
tx->v[i].dst);
goto rollback;
}
tx->v[i].installed = 1;
}
/* Installation is the commit point. Backup cleanup cannot truthfully turn
* a fully installed generation into a rejected one; retain a recoverable
* old copy and report the cleanup failure instead of claiming rollback. */
for (int i = 0; i < tx->n; i++)
if (tx->v[i].had_old && unlink(tx->v[i].backup) != 0)
fprintf(stderr, "ww: cannot remove transaction backup %s\n",
tx->v[i].backup);
return 0;
rollback:
for (int i = tx->n - 1; i >= 0; i--) {
if (tx->v[i].installed
&& unlink(tx->v[i].dst) != 0 && errno != ENOENT)
fprintf(stderr, "ww: cannot roll back %s\n", tx->v[i].dst);
if (tx->v[i].had_old
&& rename(tx->v[i].backup, tx->v[i].dst) != 0)
fprintf(stderr, "ww: cannot restore %s\n", tx->v[i].dst);
(void)unlink(tx->v[i].stage);
}
return -1;
}
static int
sep_txn_add_pkg_suffix(struct septxn *tx, struct sepgraph *g, int pi,
const char *scratch, const char *stage_suffix, const char *dst_suffix)
{
char stage[SEP_ARTIFACT_MAX], dst[SEP_ARTIFACT_MAX];
if (sep_fname(g, pi, scratch, stage_suffix, stage, sizeof stage) < 0
|| sep_fname(g, pi, scratch, dst_suffix, dst, sizeof dst) < 0)
return -1;
return sep_txn_add(tx, stage, dst);
}
static char *
sep_product_stage_path(const char *dst)
{
char *path = sep_sprintf("%s.new", dst);
if (path != NULL && strlen(path) + 1 > (size_t)PATH_MAX) {
fprintf(stderr, "ww: product staging path is too long\n");
free(path);
return NULL;
}
return path;
}
static int
sep_write_text_stage(const char *path, const char *text)
{
FILE *f = fopen(path, "wb");
if (f == NULL) return -1;
int bad = fputs(text, f) == EOF;
if (fclose(f) != 0) bad = 1;
if (bad) (void)unlink(path);
return bad ? -1 : 0;
}
static int
sep_stage_product_status(struct sepproduct *p)
{
if (p->status == NULL) return 0;
if (p->stage_status == NULL)
p->stage_status = sep_product_stage_path(p->status);
if (p->stage_status == NULL) return -1;
if (path_exists_nofollow(p->stage_status) != 0) {
fprintf(stderr, "ww: product staging path already exists: %s\n",
p->stage_status);
free(p->stage_status);
p->stage_status = NULL;
return -1;
}
return sep_write_text_stage(p->stage_status, "ok\n");
}
static void
sep_free_product_staging(struct sepproduct *products, int nproducts)
{
for (int i = 0; i < nproducts; i++) {
free(products[i].stage_status);
free(products[i].stage_iface);
free(products[i].stage_out);
products[i].stage_status = NULL;
products[i].stage_iface = NULL;
products[i].stage_out = NULL;
}
}
static int
sep_prepare_product_stage(char **slot, const char *dst)
{
if (*slot == NULL) *slot = sep_product_stage_path(dst);
if (*slot == NULL) return -1;
if (path_exists_nofollow(*slot) != 0) {
fprintf(stderr, "ww: product staging path already exists: %s\n",
*slot);
free(*slot);
*slot = NULL;
return -1;
}
return 0;
}
/* Loader/coordinator-owned staging names are structural request inputs. Check
* every one before scratch acquisition or producer execution, and never treat
* a dangling symlink as an absent path. */
static int
sep_validate_request_staging(struct sepgraph *g, const char *scratch, int warm,
struct sepproduct *products, int nproducts, int root_package,
int publish_package, int emit_asm, int is_test)
{
if (warm) {
const char *suffix[] = { ".unit.new", ".wwi.new", ".s.new",
".o.new", ".a.new", ".init.unit.new", ".init.s.new",
".init.o.new" };
for (int pi = 0; pi < g->n; pi++) {
if (g->pkg[pi].failed || !g->pkg[pi].loaded) continue;
for (size_t si = 0; si < nelem(suffix); si++) {
char path[SEP_ARTIFACT_MAX];
if (sep_fname(g, pi, scratch, suffix[si], path,
sizeof path) < 0)
return -1;
if (path_exists_nofollow(path) != 0) {
fprintf(stderr,
"ww: package staging path already exists: %s\n",
path);
return -1;
}
}
}
const char *tool_suffix[] = { "/.wwtool.ww.new",
"/.wwtool.w6c.new", "/.wwtool.w6a.new",
"/.wwtool.stamp.new" };
for (size_t i = 0; i < nelem(tool_suffix); i++) {
char path[PATH_MAX];
int n = snprintf(path, sizeof path, "%s%s", scratch,
tool_suffix[i]);
if (n < 0 || (size_t)n >= sizeof path) return -1;
if (path_exists_nofollow(path) != 0) {
fprintf(stderr,
"ww: tool staging path already exists: %s\n", path);
return -1;
}
}
}
for (int i = 0; i < nproducts; i++) {
if (products[i].status != NULL
&& sep_prepare_product_stage(&products[i].stage_status,
products[i].status) < 0)
return -1;
if (emit_asm) continue;
int owns_output = root_package ? publish_package
: is_test || sep_root_is_command(&g->pkg[products[i].root]);
if (!owns_output) continue;
if (sep_prepare_product_stage(&products[i].stage_out,
products[i].out) < 0)
return -1;
if (root_package) {
char iface[PATH_MAX];
int n = snprintf(iface, sizeof iface, "%s.wwi",
products[i].out);
if (n < 0 || (size_t)n >= sizeof iface
|| sep_prepare_product_stage(&products[i].stage_iface,
iface) < 0)
return -1;
}
}
return 0;
}
/* A package publication writes OUT, OUT.new, OUT.wwi, and OUT.wwi.new.
* Validate the longest spelling before any producer tool can run. */
static int
validate_package_output_path(const char *out)
{
size_t n = strlen(out);
if ((size_t)PATH_MAX < sizeof ".wwi.wwtxn.9223372036854775807.old"
|| n > (size_t)PATH_MAX
- sizeof ".wwi.wwtxn.9223372036854775807.old") {
fprintf(stderr, "ww: package output path is too long\n");
return -1;
}
return 0;
}
static int
validate_command_output_path(const char *out)
{
if (out == NULL
|| (size_t)PATH_MAX < sizeof ".wwtxn.9223372036854775807.old"
|| strlen(out) > (size_t)PATH_MAX
- sizeof ".wwtxn.9223372036854775807.old") {
fprintf(stderr, "ww: command output path is too long\n");
return -1;
}
return 0;
}
/* The stamp pins the non-content build inputs a unit compare cannot see:
* the -T/-S/root-action shape of the producer pass and the artifact protocol
* revision (bump "fmt" when the unit/archive/commit format changes). */
static void
workdir_stamp_text(char *buf, size_t bufsz, int is_test, int emit_asm)
{
snprintf(buf, bufsz, "ww workdir fmt %d mode %s asm %d\n",
is_test ? 16 : 17, is_test ? "test" : "build", emit_asm);
}
static int
sep_discard_action_staging(int warm, const char *unit, const char *wwi,
const char *assembly, const char *object, const char *archive)
{
(void)warm;
const char *paths[] = { unit, wwi, assembly, object, archive };
for (size_t i = 0; i < sizeof paths / sizeof paths[0]; i++) {
if (unlink(paths[i]) == 0 || errno == ENOENT) continue;
fprintf(stderr, "ww: cannot remove staged package artifacts\n");
return -1;
}
return 0;
}
static int
sep_discard_init_staging(int warm, const char *unit, const char *assembly,
const char *object)
{
(void)warm;
const char *paths[] = { unit, assembly, object };
for (size_t i = 0; i < sizeof paths / sizeof paths[0]; i++) {
if (paths[i][0] == '\0'
|| unlink(paths[i]) == 0 || errno == ENOENT)
continue;
fprintf(stderr, "ww: cannot remove staged initialization artifacts\n");
return -1;
}
return 0;
}
static int
sep_discard_request_staging(struct sepgraph *g, const char *scratch, int warm,
struct sepproduct *products, int nproducts)
{
const char *warm_suffix[] = { ".unit.new", ".wwi.new", ".s.new",
".o.new", ".a.new", ".init.unit.new", ".init.s.new",
".init.o.new" };
const char *cold_suffix[] = { ".unit.ww", ".wwi", ".s", ".o", ".a",
".init.unit.ww", ".init.s", ".init.o" };
const char **suffix = warm ? warm_suffix : cold_suffix;
int rc = 0;
for (int pi = 0; pi < g->n; pi++)
for (size_t i = 0; i < nelem(warm_suffix); i++) {
char path[SEP_ARTIFACT_MAX];
if (sep_fname(g, pi, scratch, suffix[i], path,
sizeof path) < 0
|| (unlink(path) != 0 && errno != ENOENT))
rc = -1;
}
for (int i = 0; i < nproducts; i++) {
const char *path[] = { products[i].stage_out,
products[i].stage_iface, products[i].stage_status };
for (size_t j = 0; j < nelem(path); j++)
if (path[j] != NULL
&& unlink(path[j]) != 0 && errno != ENOENT)
rc = -1;
}
if (warm) {
const char *tool_suffix[] = { "/.wwtool.ww.new",
"/.wwtool.w6c.new", "/.wwtool.w6a.new",
"/.wwtool.stamp.new" };
for (size_t i = 0; i < nelem(tool_suffix); i++) {
char path[PATH_MAX];
int n = snprintf(path, sizeof path, "%s%s", scratch,
tool_suffix[i]);
if (n < 0 || (size_t)n >= sizeof path
|| (unlink(path) != 0 && errno != ENOENT))
rc = -1;
}
}
if (rc != 0) fprintf(stderr, "ww: cannot discard rejected request staging\n");
return rc;
}
struct sep_created_dirs {
char path[PATH_MAX];
unsigned short offset[(PATH_MAX + 1) / 2];
int n;
};
static void
sep_rollback_dirs(struct sep_created_dirs *created)
{
while (created->n > 0) {
size_t at = created->offset[--created->n];
char saved = created->path[at];
created->path[at] = '\0';
(void)rmdir(created->path);
created->path[at] = saved;
}
}
/* Create one caller-requested directory tree after semantic preflight. Record
* exactly the prefixes minted by this call so a later pre-producer setup
* failure can roll them back without touching pre-existing caller state. */
static int
sep_mkdirs(const char *path, mode_t mode, struct sep_created_dirs *created)
{
memset(created, 0, sizeof *created);
size_t n = strlen(path);
if (n == 0 || n >= PATH_MAX) return -1;
char buf[PATH_MAX];
memcpy(buf, path, n + 1);
while (n > 1 && buf[n - 1] == '/') buf[--n] = '\0';
memcpy(created->path, buf, n + 1);
for (char *p = buf + (buf[0] == '/' ? 1 : 0); ; p++) {
if (*p != '/' && *p != '\0') continue;
char saved = *p;
*p = '\0';
if (buf[0] != '\0') {
struct stat st;
if (stat(buf, &st) != 0) {
if (errno != ENOENT
|| created->n >= (int)(sizeof created->offset
/ sizeof created->offset[0])
|| mkdir(buf, mode) != 0) {
*p = saved;
sep_rollback_dirs(created);
return -1;
}
created->offset[created->n++] =
(unsigned short)(p - buf);
} else if (!S_ISDIR(st.st_mode)) {
*p = saved;
sep_rollback_dirs(created);
return -1;
}
}
*p = saved;
if (saved == '\0') break;
}
return 0;
}
/* build_sep_plan — discover dependencies for every requested product in one
* package universe, compile the dependency-first union once, then link each
* root from its own complete reachable archive closure. The dependency-first
* producer loop (one `w6c -c -I` per package,
* each package `.o` wrapped in its own deterministic `.a`), then a
* reverse-topo `w6l` of each root `.a` + reachable `.a` set + libwwrt.a. Side
* files land in a cold `<stem>.sepwork` dir, or under the persistent
* `-w` workdir with content-identity package reuse. */
static int
build_one_sep_impl(const char *src, int entry_is_dir,
const char *out, const char *objstem, const char *extra_includes,
const struct seplinkflags *linkflags, int publish_package,
int require_command, int is_test,
struct sepproduct *products, int nproducts, int emit_asm,
const char *workdir, int create_workdir, const char *create_output_dir,
char *scratchout, size_t scratchoutsz,
struct sepgraph **graphout)
{
sep_fatal_allocation = 0;
if (nproducts < 1) return 1;
const char *c6 = toolpath("WW_W6C", "w6c");
const char *a6 = toolpath("WW_W6A", "w6a");
const char *l6 = toolpath("WW_W6L", "w6l");
const char *libdir = envpath("WW_LIB");
if (libdir == NULL) {
static char libbuf[PATH_MAX];
int n = snprintf(libbuf, sizeof libbuf, "%s/../lib", self_dir);
if (n < 0 || (size_t)n >= sizeof libbuf) return 1;
libdir = libbuf;
}
const char *srcdir = envpath("WW_SRCLIB");
static char srcbuf[PATH_MAX];
if (srcdir == NULL) {
int n = snprintf(srcbuf, sizeof srcbuf, "%s/../../lib", self_dir);
if (n < 0 || (size_t)n >= sizeof srcbuf) return 1;
if (access(srcbuf, 0) == 0) srcdir = srcbuf;
else if (access("lib", 0) == 0) srcdir = "lib";
else srcdir = libdir;
}
const char *toolsrcdir = srcdir;
char srcd[PATH_MAX];
if (entry_is_dir) {
if (strlen(src) + 1 > sizeof srcd) return 1;
memcpy(srcd, src, strlen(src) + 1);
size_t n = strlen(srcd);
while (n > 1 && srcd[n-1] == '/') srcd[--n] = '\0';
} else {
const char *slash = strrchr(src, '/');
if (slash) {
size_t n = (size_t)(slash - src);
if (n >= sizeof srcd) n = sizeof srcd - 1;
memcpy(srcd, src, n);
srcd[n] = '\0';
} else { srcd[0] = '.'; srcd[1] = '\0'; }
}
char *stem = NULL;
if (entry_is_dir) {
const char *b = strrchr(srcd, '/');
const char *base = b ? b + 1 : srcd;
stem = sep_sprintf("%s/%s", srcd, base);
} else {
stem = strdup(src);
if (stem == NULL) return 1;
char *dot = strrchr(stem, '.');
if (dot && strcmp(dot, ".ww") == 0) *dot = '\0';
}
if (stem == NULL) return 1;
const char *ostem = (objstem && objstem[0]) ? objstem : stem;
int warm = workdir != NULL && workdir[0] != 0;
int workdir_exists = 0;
char scratch[PATH_MAX];
if (warm) {
struct stat wst;
int wr = stat(workdir, &wst);
if (wr == 0 && S_ISDIR(wst.st_mode)) {
workdir_exists = 1;
} else if (wr == 0 || !create_workdir || errno != ENOENT) {
fprintf(stderr, "ww: workdir %s is not a directory\n",
workdir);
return 1;
}
/* The workdir is caller-owned and persistent: no acquisition,
* no refusal, and scratchout stays empty so the wrapper never
* cleans it. */
if (strlen(workdir) + 1 > sizeof scratch) {
fprintf(stderr, "ww: workdir path is too long\n");
return 1;
}
memcpy(scratch, workdir, strlen(workdir) + 1);
} else {
int n = snprintf(scratch, sizeof scratch, "%s.sepwork", ostem);
if (n < 0 || (size_t)n >= sizeof scratch) {
fprintf(stderr, "ww: scratch path is too long\n");
return 1;
}
if (scratchout && strlen(scratch) + 1 > scratchoutsz) return 1;
}
int stale_all = 0, stampok = 0;
char toolw[PATH_MAX] = {0}, toolc[PATH_MAX] = {0};
char toola[PATH_MAX] = {0}, stampf[PATH_MAX] = {0};
char stampwant[128];
if (warm) {
if (self_path == NULL || !file_is_reg(self_path)) {
fprintf(stderr, "ww: cannot read driver identity %s\n",
self_path ? self_path : "(unknown)");
return 1;
}
int nw = snprintf(toolw, sizeof toolw, "%s/.wwtool.ww", scratch);
int nc = snprintf(toolc, sizeof toolc, "%s/.wwtool.w6c", scratch);
int na = snprintf(toola, sizeof toola, "%s/.wwtool.w6a", scratch);
int ns = snprintf(stampf, sizeof stampf, "%s/.wwtool.stamp", scratch);
if (nw < 0 || nc < 0 || na < 0 || ns < 0
|| (size_t)nw >= sizeof toolw || (size_t)nc >= sizeof toolc
|| (size_t)na >= sizeof toola || (size_t)ns >= sizeof stampf) {
fprintf(stderr, "ww: workdir path is too long\n");
return 1;
}
workdir_stamp_text(stampwant, sizeof stampwant, is_test, emit_asm);
char got[128] = {0};
FILE *sf = fopen(stampf, "rb");
if (sf) {
size_t rn = fread(got, 1, sizeof got - 1, sf);
got[rn] = 0;
fclose(sf);
}
stampok = strcmp(stampwant, got) == 0;
if (!stampok || !file_equal(toolw, self_path)
|| !file_equal(toolc, c6)
|| (!emit_asm && !file_equal(toola, a6)))
stale_all = 1;
}
struct sepgraph *g = calloc(1, sizeof *g);
if (g == NULL) {
fprintf(stderr, "ww: out of memory\n");
return 1;
}
g->support_context = -1;
if (graphout) *graphout = g;
for (int i = 0; i < nproducts; i++) {
products[i].support = -1;
products[i].stage_out = NULL;
products[i].stage_iface = NULL;
products[i].stage_status = NULL;
}
for (int i = 0; i < nproducts; i++) {
const char *entry = products[i].dir != NULL
? products[i].dir : src;
char contextdir[PATH_MAX];
const char *contextroot = entry;
if (!entry_is_dir) {
const char *slash = strrchr(entry, '/');
if (slash != NULL) {
size_t n = (size_t)(slash - entry);
if (n >= sizeof contextdir) return 1;
memcpy(contextdir, entry, n);
contextdir[n] = '\0';
} else {
snprintf(contextdir, sizeof contextdir, ".");
}
contextroot = contextdir;
}
const char *selector = products[i].variant == SEP_VARIANT_PRODUCTION
? NULL : products[i].test_package;
const char *requested_path = products[i].identity != NULL
? products[i].identity : "";
products[i].context = sep_context_for(g, contextroot,
extra_includes, toolsrcdir, requested_path);
if (products[i].context < 0) return 1;
char *inferred_path = NULL;
const char *rootpath = requested_path;
if (entry_is_dir && rootpath[0] == '\0') {
int inferred = sep_context_import_base(g, products[i].context,
&inferred_path);
if (inferred < 0) return 1;
if (inferred > 0) rootpath = inferred_path;
}
products[i].root = sep_find_or_add_variant(g, rootpath, entry,
entry_is_dir, products[i].variant, selector,
SEP_ROLE_NORMAL, products[i].artifact, 1);
free(inferred_path);
if (products[i].root < 0) return 1;
products[i].variant_root = products[i].root;
}
const char *test_support_module = "test";
/* -T generates a dispatcher whose support qualifier is selected by the
* command. Represent that compiler-generated requirement as a direct edge
* of the generated-main action. It normally coalesces with an explicit
* toolchain `import test`;
* when user source occupies that identity, the reserved graph alias keeps
* it distinct. The linker receives the same support archive closure. */
if (is_test) {
char tpath[PATH_MAX];
int tdir = 0;
if (locate_import(toolsrcdir, "test", tpath, sizeof tpath)) {
tdir = 1;
char *support_search = sep_sprintf("%s:%s",
toolsrcdir, toolsrcdir);
if (support_search == NULL) return 1;
g->support_context = sep_context_add(g, toolsrcdir,
support_search, tpath, toolsrcdir);
free(support_search);
if (g->support_context < 0) return 1;
errno = 0;
char *tc = realpath(tpath, NULL);
if (tc == NULL && errno == ENOMEM) {
sep_fail_nomem();
return 1;
}
int collision = 0;
for (int i = 0; i < nproducts; i++) {
int root_is_support = tc != NULL
&& strcmp(g->pkg[products[i].root].canon, tc) == 0;
const char *name = products[i].test_package;
if (!root_is_support && name != NULL
&& (strcmp(name, "test") == 0
|| strcmp(name, "test_test") == 0))
collision = 1;
}
for (int i = 0; i < nproducts && !collision; i++) {
char userpath[PATH_MAX];
int userdir = 0;
if (locate_import(g->context[products[i].context].searchpath,
"test", userpath, sizeof userpath)) {
userdir = 1;
(void)userdir;
errno = 0;
char *uc = realpath(userpath, NULL);
if (uc == NULL && errno == ENOMEM) {
free(tc);
sep_fail_nomem();
return 1;
}
if (tc != NULL && uc != NULL
&& strcmp(tc, uc) != 0)
collision = 1;
free(uc);
}
}
if (collision) test_support_module = SEP_TEST_SUPPORT_MODULE;
for (int i = 0; i < nproducts; i++) {
int root = products[i].root;
int root_is_support = tc != NULL
&& strcmp(g->pkg[root].canon, tc) == 0;
/* A same-test build of the runtime package already owns run
* and its source imports. An external test still needs the
* colocated production node, which is also its support dep. */
if (root_is_support
&& strcmp(test_support_module, "test") == 0
&& products[i].variant != SEP_VARIANT_EXTERNAL) {
products[i].support = root;
continue;
}
int ti;
if (strcmp(test_support_module,
SEP_TEST_SUPPORT_MODULE) == 0)
ti = sep_find_or_add_role(g, test_support_module,
tpath, tdir, SEP_ROLE_TEST_SUPPORT, NULL);
else
ti = sep_find_or_add(g, test_support_module, tpath,
tdir);
if (ti < 0) return 1;
g->pkg[ti].test_support = 1;
products[i].support = ti;
}
free(tc);
}
}
for (int i = 0; i < nproducts; i++) {
int root = products[i].variant_root;
/* Raw single-file test fixtures are the one retained non-directory
* exception: keep compiler-owned test-main synthesis in that action.
* Its support export is still an exact direct input. */
if (is_test && !entry_is_dir) {
int support = products[i].support;
if (support >= 0 && support != root
&& sep_add_dep(g, root, support) < 0)
return 1;
g->pkg[root].link_entry = 1;
}
int lr = sep_load_pkg(g, root, products[i].context);
if (lr == -2) return 1;
if (lr == SEP_LOAD_INTERNAL || lr == SEP_LOAD_VENDOR) return 1;
if (lr < 0) {
g->pkg[root].failed = 1;
continue;
}
if (products[i].test_package != NULL
&& strcmp(g->pkg[root].name, products[i].test_package) != 0) {
fprintf(stderr,
"ww: package-test selector does not match loaded package\n");
g->pkg[root].failed = 1;
}
}
if (is_test) {
for (int i = 0; i < nproducts; i++) {
int variant = products[i].variant_root;
int support = products[i].support;
if (support >= 0 && support != variant) {
int lr = sep_load_pkg(g, support, products[i].context);
if (lr == -2) return 1;
if (lr == SEP_LOAD_INTERNAL || lr == SEP_LOAD_VENDOR)
return 1;
if (lr < 0) g->pkg[variant].failed = 1;
}
}
}
if (g->identity_failed) return 1;
if (sep_finalize_directory_identities(g) < 0) return 1;
if (!is_test) {
for (int i = 0; i < nproducts; i++) {
int root = products[i].variant_root;
if (!g->pkg[root].failed)
g->pkg[root].link_entry =
sep_root_is_command(&g->pkg[root]);
}
}
if (is_test && entry_is_dir) {
for (int i = 0; i < nproducts; i++) {
int variant = products[i].variant_root;
int support = products[i].support;
if (g->pkg[variant].failed
|| (support >= 0 && g->pkg[support].failed)) {
products[i].root = variant;
continue;
}
int mainpkg = sep_add_generated_main(g, &products[i], i,
support);
if (mainpkg < 0) return 1;
products[i].root = mainpkg;
}
}
for (int pi = 0; pi < g->n; pi++) {
g->pkg[pi].init_symbol = sep_package_init_symbol(&g->pkg[pi]);
if (g->pkg[pi].init_symbol == NULL) return 1;
}
for (int i = 0; i < nproducts; i++) {
int root = products[i].root;
if (!g->pkg[root].failed && sep_root_is_command(&g->pkg[root])
&& validate_command_output_path(products[i].out) < 0)
return 1;
if (products[i].status != NULL
&& validate_command_output_path(products[i].status) < 0)
return 1;
}
int root_package = !is_test && nproducts == 1
&& !g->pkg[products[0].root].failed
&& !sep_root_is_command(&g->pkg[products[0].root]);
if (sep_validate_artifact_paths(g, scratch) < 0)
return 1;
if (warm && workdir_exists && sep_validate_workdir_owners(g, scratch) < 0)
return 1;
int *order = calloc((size_t)g->n, sizeof *order);
int *stack = calloc((size_t)g->n, sizeof *stack);
int norder = 0;
if (order == NULL || stack == NULL) {
fprintf(stderr, "ww: out of memory\n");
free(stack); free(order); return 1;
}
/* Diagnose cycles per product before constructing the shared union. A
* variant-local cycle does not erase another root's attribution, although
* any failure still rejects the request-wide publication transaction. */
for (int i = 0; i < nproducts; i++) {
int root = products[i].root;
if (g->pkg[root].failed) continue;
for (int pi = 0; pi < g->n; pi++) g->pkg[pi].color = 0;
int ignored = 0;
int tr = sep_topo_visit(g, root, order, &ignored, stack, 0);
if (tr == -2) {
free(stack); free(order); return 1;
}
if (tr < 0
|| sep_validate_module_closure(g, order, ignored, 1) < 0)
g->pkg[root].failed = 1;
}
/* Internal-test substitution can create a cycle that is absent from the
* ordinary production graph (I -> X -> P becomes I -> X -> I). Go rejects
* that effective test graph during loading, before any producer action. */
for (int i = 0; i < nproducts; i++) {
int root = products[i].root;
if (g->pkg[root].failed) continue;
int *initcheck = NULL, ninitcheck = 0;
if (sep_init_order(g, root, products[i].variant_root,
&initcheck, &ninitcheck) < 0)
g->pkg[root].failed = 1;
free(initcheck);
}
if (sep_fatal_allocation) {
free(stack); free(order);
return 1;
}
if (!is_test && require_command) {
int root = products[0].root;
if (!g->pkg[root].failed
&& !sep_root_is_command(&g->pkg[root])) {
fprintf(stderr, "ww: package %s is not a main package\n",
g->pkg[root].path[0] ? g->pkg[root].path
: g->pkg[root].canon);
free(stack); free(order);
return 1;
}
}
if (!g->pkg[products[0].root].failed
&& root_package && publish_package && !emit_asm
&& validate_package_output_path(out) < 0) {
free(stack); free(order);
return 1;
}
for (int pi = 0; pi < g->n; pi++) g->pkg[pi].color = 0;
for (int i = 0; i < nproducts; i++) {
int root = products[i].root;
if (!g->pkg[root].failed
&& sep_topo_visit(g, root, order, &norder, stack, 0) < 0) {
free(stack); free(order); return 1;
}
}
free(stack);
/* Propagate already-known package-load failures through the union before
* acquiring scratch or completion state. Independent sibling roots may
* remain viable for deterministic staging/diagnosis, but any failed product
* rejects publication; an entirely rejected cold request leaves no tree. */
for (int oi = 0; oi < norder; oi++) {
int pi = order[oi];
for (int k = 0; k < g->pkg[pi].ndeps; k++)
if (g->pkg[g->pkg[pi].deps[k]].failed)
g->pkg[pi].failed = 1;
}
int viable_product = 0;
for (int i = 0; i < nproducts; i++)
if (!g->pkg[products[i].root].failed) viable_product = 1;
if (!viable_product) {
free(order);
return 1;
}
if (sep_validate_request_staging(g, scratch, warm, products, nproducts,
root_package, publish_package, emit_asm, is_test) < 0) {
sep_free_product_staging(products, nproducts);
free(order);
return 1;
}
/* Product completion and persistent package state remain untouched until
* all source-derived imports, contextual legality, cycles, command kind,
* output paths, and action closures have passed their pre-tool checks. */
struct sep_created_dirs created_output = {0};
struct sep_created_dirs created_work = {0};
if (create_output_dir != NULL && create_output_dir[0] != '\0'
&& sep_mkdirs(create_output_dir, 0700, &created_output) != 0) {
fprintf(stderr, "ww: cannot create build output directory %s\n",
create_output_dir);
free(order);
return 1;
}
if (warm && !workdir_exists) {
if (!create_workdir
|| sep_mkdirs(scratch, 0700, &created_work) != 0) {
fprintf(stderr, "ww: workdir %s is not a directory\n", scratch);
sep_rollback_dirs(&created_output);
free(order);
return 1;
}
workdir_exists = 1;
}
if (!warm) {
if (mkdir(scratch, 0755) != 0) {
fprintf(stderr, "ww: cannot create scratch %s\n", scratch);
sep_rollback_dirs(&created_work);
sep_rollback_dirs(&created_output);
free(order);
return 1;
}
/* The wrapper owns only the directory this invocation acquired. */
if (scratchout != NULL)
memcpy(scratchout, scratch, strlen(scratch) + 1);
}
struct septxn tx = {0};
int any_failed = 0;
for (int i = 0; i < nproducts; i++)
if (g->pkg[products[i].root].failed) any_failed = 1;
for (int oi = 0; oi < norder; oi++) {
int pi = order[oi];
for (int k = 0; k < g->pkg[pi].ndeps; k++)
if (g->pkg[g->pkg[pi].deps[k]].failed)
g->pkg[pi].failed = 1;
if (g->pkg[pi].failed) {
any_failed = 1;
continue;
}
char unitf[SEP_ARTIFACT_MAX], wwi[SEP_ARTIFACT_MAX];
char asmf[SEP_ARTIFACT_MAX], obj[SEP_ARTIFACT_MAX];
char apath[SEP_ARTIFACT_MAX], unitnew[SEP_ARTIFACT_MAX];
char wwinew[SEP_ARTIFACT_MAX], asmnew[SEP_ARTIFACT_MAX];
char objnew[SEP_ARTIFACT_MAX], anew[SEP_ARTIFACT_MAX];
char initunitf[SEP_ARTIFACT_MAX] = "";
char initasmf[SEP_ARTIFACT_MAX] = "";
char initobj[SEP_ARTIFACT_MAX] = "";
char initunitnew[SEP_ARTIFACT_MAX] = "";
char initasmnew[SEP_ARTIFACT_MAX] = "";
char initobjnew[SEP_ARTIFACT_MAX] = "";
sep_fname(g, pi, scratch, ".unit.ww", unitf, sizeof unitf);
sep_fname(g, pi, scratch, ".wwi", wwi, sizeof wwi);
sep_fname(g, pi, scratch, ".s", asmf, sizeof asmf);
sep_fname(g, pi, scratch, ".o", obj, sizeof obj);
sep_fname(g, pi, scratch, ".a", apath, sizeof apath);
sep_fname(g, pi, scratch, ".unit.new", unitnew, sizeof unitnew);
sep_fname(g, pi, scratch, ".wwi.new", wwinew, sizeof wwinew);
sep_fname(g, pi, scratch, ".s.new", asmnew, sizeof asmnew);
sep_fname(g, pi, scratch, ".o.new", objnew, sizeof objnew);
sep_fname(g, pi, scratch, ".a.new", anew, sizeof anew);
int product_index = -1;
if (g->pkg[pi].link_entry) {
for (int i = 0; i < nproducts; i++)
if (products[i].root == pi) {
product_index = i;
break;
}
if (product_index < 0) {
fprintf(stderr,
"ww: executable action has no owning product\n");
g->pkg[pi].failed = 1;
any_failed = 1;
continue;
}
sep_fname(g, pi, scratch, ".init.unit.ww", initunitf,
sizeof initunitf);
sep_fname(g, pi, scratch, ".init.s", initasmf,
sizeof initasmf);
sep_fname(g, pi, scratch, ".init.o", initobj,
sizeof initobj);
sep_fname(g, pi, scratch, ".init.unit.new", initunitnew,
sizeof initunitnew);
sep_fname(g, pi, scratch, ".init.s.new", initasmnew,
sizeof initasmnew);
sep_fname(g, pi, scratch, ".init.o.new", initobjnew,
sizeof initobjnew);
}
/* Classic scratch has no committed generation to preserve. Alias the
* cleanup paths to its in-place outputs so a rejected producer leaves
* no partial action or dispatcher artifacts. */
if (!warm) {
memcpy(unitnew, unitf, strlen(unitf) + 1);
memcpy(wwinew, wwi, strlen(wwi) + 1);
memcpy(asmnew, asmf, strlen(asmf) + 1);
memcpy(objnew, obj, strlen(obj) + 1);
memcpy(anew, apath, strlen(apath) + 1);
if (product_index >= 0) {
memcpy(initunitnew, initunitf, strlen(initunitf) + 1);
memcpy(initasmnew, initasmf, strlen(initasmf) + 1);
memcpy(initobjnew, initobj, strlen(initobj) + 1);
}
}
/* Warm mode compiles from staged `.new` paths and commits by
* rename; classic mode keeps its exact in-place paths. */
const char *cu = warm ? unitnew : unitf;
const char *cw = warm ? wwinew : wwi;
const char *cs = warm ? asmnew : asmf;
const char *co = warm ? objnew : obj;
const char *ca = warm ? anew : apath;
const char *ciu = warm ? initunitnew : initunitf;
const char *cis = warm ? initasmnew : initasmf;
const char *cio = warm ? initobjnew : initobj;
if (sep_discard_action_staging(warm, unitnew, wwinew, asmnew,
objnew, anew) < 0
|| sep_discard_init_staging(warm, initunitnew, initasmnew,
initobjnew) < 0) {
g->pkg[pi].failed = 1;
any_failed = 1;
continue;
}
if (sep_compose_unit(g, pi, scratch, cu) < 0) {
(void)sep_discard_action_staging(warm, unitnew, wwinew,
asmnew, objnew, anew);
(void)sep_discard_init_staging(warm, initunitnew,
initasmnew, initobjnew);
g->pkg[pi].failed = 1;
any_failed = 1;
continue;
}
if (product_index >= 0
&& sep_compose_init_dispatch(g, &products[product_index],
ciu, cis) < 0) {
(void)sep_discard_action_staging(warm, unitnew, wwinew,
asmnew, objnew, anew);
(void)sep_discard_init_staging(warm, initunitnew,
initasmnew, initobjnew);
g->pkg[pi].failed = 1;
any_failed = 1;
continue;
}
int deps_changed = 0;
for (int k = 0; k < g->pkg[pi].ndeps; k++)
if (g->pkg[g->pkg[pi].deps[k]].export_changed)
deps_changed = 1;
int source_reusable = warm && !stale_all && !deps_changed
&& file_equal(unitnew, unitf)
&& file_is_reg(asmf)
&& file_is_reg(wwi)
&& (emit_asm || (file_size_nonzero(obj)
&& (product_index >= 0 || file_size_nonzero(apath))));
int init_reusable = product_index < 0
|| (warm && !stale_all
&& file_is_reg(initunitf)
&& file_equal(initunitnew, initunitf)
&& file_is_reg(initasmf)
&& (emit_asm || file_size_nonzero(initobj)));
if (source_reusable && init_reusable
&& (emit_asm || file_size_nonzero(apath))) {
if (sep_discard_action_staging(warm, unitnew, wwinew,
asmnew, objnew, anew) < 0
|| sep_discard_init_staging(warm, initunitnew,
initasmnew, initobjnew) < 0) {
g->pkg[pi].failed = 1;
any_failed = 1;
}
continue;
}
/* A changed closure is owned by the root dispatcher, not the source
* compile action. Rebuild that member and the existing root archive
* without recompiling an otherwise reusable root package. */
if (source_reusable && product_index >= 0) {
(void)unlink(unitnew);
if (!emit_asm) {
char *iaargv[] = {"w6a", "-o", (char *)cio,
(char *)cis, NULL};
if (run_argv(a6, iaargv) != 0
|| archive_o(obj, cio, ca) != 0) {
fprintf(stderr,
"ww: initialization archive failed for %s\n",
g->pkg[pi].path[0] ? g->pkg[pi].path : "(root)");
g->pkg[pi].failed = 1;
any_failed = 1;
(void)sep_discard_action_staging(warm, unitnew,
wwinew, asmnew, objnew, anew);
(void)sep_discard_init_staging(warm, initunitnew,
initasmnew, initobjnew);
continue;
}
}
if (warm) {
g->pkg[pi].init_staged = 1;
if (!emit_asm) g->pkg[pi].archive_staged = 1;
}
continue;
}
/* Staged producers cannot disturb the previous generation. Its source
* and dispatcher vouchers remain committed until commit actually opens;
* direct-export bytes in the source voucher prevent stale later reuse. */
if (product_index >= 0 && init_reusable && warm
&& sep_discard_init_staging(warm, initunitnew, initasmnew,
initobjnew) < 0) {
fprintf(stderr,
"ww: cannot remove staged initialization assembly\n");
(void)sep_discard_action_staging(warm, unitnew, wwinew,
asmnew, objnew, anew);
(void)sep_discard_init_staging(warm, initunitnew,
initasmnew, initobjnew);
g->pkg[pi].failed = 1;
any_failed = 1;
continue;
}
int nmaps = 0;
for (int k = 0; k < g->pkg[pi].bindings.n; k++) {
if (!sep_binding_first_map(g, &g->pkg[pi].bindings, k))
continue;
if (nmaps == INT_MAX) {
sep_fail_size();
(void)sep_discard_action_staging(warm, unitnew, wwinew,
asmnew, objnew, anew);
(void)sep_discard_init_staging(warm, initunitnew,
initasmnew, initobjnew);
goto request_fail;
}
nmaps++;
}
size_t cargvcap = 18;
if ((size_t)g->pkg[pi].ndeps > ((size_t)-1 - cargvcap) / 3) {
fprintf(stderr, "ww: package graph is too large\n");
(void)sep_discard_action_staging(warm, unitnew, wwinew,
asmnew, objnew, anew);
(void)sep_discard_init_staging(warm, initunitnew,
initasmnew, initobjnew);
goto request_fail;
}
cargvcap += 3 * (size_t)g->pkg[pi].ndeps;
if ((size_t)nmaps > ((size_t)-1 - cargvcap) / 3) {
fprintf(stderr, "ww: package graph is too large\n");
(void)sep_discard_action_staging(warm, unitnew, wwinew,
asmnew, objnew, anew);
(void)sep_discard_init_staging(warm, initunitnew,
initasmnew, initobjnew);
goto request_fail;
}
cargvcap += 3 * (size_t)nmaps;
char **cargv = calloc(cargvcap, sizeof *cargv);
char (*importfiles)[SEP_ARTIFACT_MAX] = NULL;
if (g->pkg[pi].ndeps > 0)
importfiles = calloc((size_t)g->pkg[pi].ndeps,
sizeof *importfiles);
if (cargv == NULL || (g->pkg[pi].ndeps > 0
&& importfiles == NULL)) {
fprintf(stderr, "ww: out of memory\n");
(void)sep_discard_action_staging(warm, unitnew, wwinew,
asmnew, objnew, anew);
(void)sep_discard_init_staging(warm, initunitnew,
initasmnew, initobjnew);
free(importfiles);
free(cargv);
goto request_fail;
}
int cpos = 0;
cargv[cpos++] = "w6c";
if (g->pkg[pi].generated_main
|| (is_test && g->pkg[pi].root && !g->pkg[pi].is_dir)) {
cargv[cpos++] = "-T";
cargv[cpos++] = "--entry";
cargv[cpos++] = "--test-support-module";
cargv[cpos++] = (char *)test_support_module;
if (g->pkg[pi].generated_main) {
cargv[cpos++] = "--test-target-package";
cargv[cpos++] = g->pkg[g->pkg[pi].generated_target].path;
}
} else {
if (g->pkg[pi].variant == SEP_VARIANT_SAME_TEST
|| g->pkg[pi].variant == SEP_VARIANT_EXTERNAL)
cargv[cpos++] = "--test-package";
if (sep_command_compiler_marker(g, pi))
cargv[cpos++] = "--command-package";
if (g->pkg[pi].link_entry)
cargv[cpos++] = "--entry";
if (g->pkg[pi].test_support) {
cargv[cpos++] = "--test-support-module";
cargv[cpos++] = (char *)test_support_module;
}
}
cargv[cpos++] = "--package-init-symbol";
cargv[cpos++] = g->pkg[pi].init_symbol;
if (product_index >= 0) {
cargv[cpos++] = "--init-dispatch-symbol";
cargv[cpos++] = "__ww..dispatch";
}
cargv[cpos++] = "-c";
for (int k = 0; k < g->pkg[pi].ndeps; k++) {
int dj = g->pkg[pi].deps[k];
const char *suffix = g->pkg[dj].source_staged
? ".wwi.new" : ".wwi";
sep_fname(g, dj, scratch, suffix, importfiles[k],
sizeof importfiles[k]);
cargv[cpos++] = "--import";
cargv[cpos++] = g->pkg[dj].path;
cargv[cpos++] = importfiles[k];
}
for (int k = 0; k < g->pkg[pi].bindings.n; k++) {
struct sepbind *b = &g->pkg[pi].bindings.v[k];
if (!sep_binding_first_map(g, &g->pkg[pi].bindings, k))
continue;
cargv[cpos++] = "--import-map";
cargv[cpos++] = b->name;
cargv[cpos++] = g->pkg[b->dep].path;
}
cargv[cpos++] = "-I";
cargv[cpos++] = (char *)cw;
cargv[cpos++] = "-o";
cargv[cpos++] = (char *)cs;
cargv[cpos++] = (char *)cu;
cargv[cpos] = NULL;
int compilerc = run_argv(c6, cargv);
free(importfiles);
free(cargv);
if (compilerc != 0) {
fprintf(stderr, "ww: w6c failed for %s\n",
g->pkg[pi].path[0] ? g->pkg[pi].path : "(root)");
g->pkg[pi].failed = 1;
any_failed = 1;
(void)sep_discard_action_staging(warm, unitnew, wwinew,
asmnew, objnew, anew);
(void)sep_discard_init_staging(warm, initunitnew,
initasmnew, initobjnew);
continue;
}
g->pkg[pi].export_changed = !warm
|| !file_equal(wwinew, wwi);
if (!emit_asm) {
char *aargv[] = {"w6a", "-o", (char *)co,
(char *)cs, NULL};
if (run_argv(a6, aargv) != 0) {
fprintf(stderr, "ww: w6a failed for %s\n",
g->pkg[pi].path[0] ? g->pkg[pi].path : "(root)");
g->pkg[pi].failed = 1;
any_failed = 1;
(void)sep_discard_action_staging(warm, unitnew, wwinew,
asmnew, objnew, anew);
(void)sep_discard_init_staging(warm, initunitnew,
initasmnew, initobjnew);
continue;
}
}
if (!emit_asm && product_index >= 0 && !init_reusable) {
char *iaargv[] = {"w6a", "-o", (char *)cio,
(char *)cis, NULL};
if (run_argv(a6, iaargv) != 0) {
fprintf(stderr, "ww: w6a failed for initialization of %s\n",
g->pkg[pi].path[0] ? g->pkg[pi].path : "(root)");
g->pkg[pi].failed = 1;
any_failed = 1;
(void)sep_discard_action_staging(warm, unitnew, wwinew,
asmnew, objnew, anew);
(void)sep_discard_init_staging(warm, initunitnew,
initasmnew, initobjnew);
continue;
}
}
/* Executable and generated-test roots add their dispatcher as a fixed
* second archive member. All other package archives remain one-member. */
if (!emit_asm) {
const char *archive_init = product_index < 0 ? NULL
: init_reusable ? initobj : cio;
if (archive_o(co, archive_init, ca) != 0) {
fprintf(stderr, "ww: archive failed for %s\n",
g->pkg[pi].path[0] ? g->pkg[pi].path : "(root)");
g->pkg[pi].failed = 1;
any_failed = 1;
(void)sep_discard_action_staging(warm, unitnew, wwinew,
asmnew, objnew, anew);
(void)sep_discard_init_staging(warm, initunitnew,
initasmnew, initobjnew);
continue;
}
}
if (warm) {
g->pkg[pi].source_staged = 1;
if (!emit_asm) g->pkg[pi].archive_staged = 1;
if (product_index >= 0 && !init_reusable)
g->pkg[pi].init_staged = 1;
}
}
if (any_failed) goto request_fail;
if (emit_asm) {
for (int i = 0; i < nproducts; i++)
if (sep_stage_product_status(&products[i]) != 0) {
fprintf(stderr, "ww: cannot stage package-build product\n");
goto request_fail;
}
goto prepare_transaction;
}
if (root_package) {
int root = products[0].root;
if (publish_package) {
char archive[SEP_ARTIFACT_MAX], iface[SEP_ARTIFACT_MAX];
char outiface[SEP_ARTIFACT_MAX];
const char *asuffix = warm && g->pkg[root].archive_staged
? ".a.new" : ".a";
const char *isuffix = warm && g->pkg[root].source_staged
? ".wwi.new" : ".wwi";
sep_fname(g, root, scratch, asuffix, archive, sizeof archive);
sep_fname(g, root, scratch, isuffix, iface, sizeof iface);
int on = snprintf(outiface, sizeof outiface, "%s.wwi", out);
if (on < 0 || (size_t)on >= sizeof outiface) {
fprintf(stderr, "ww: package output path is too long\n");
goto request_fail;
}
if (sep_prepare_product_stage(&products[0].stage_out, out) < 0
|| sep_prepare_product_stage(&products[0].stage_iface,
outiface) < 0
|| copy_file_stage(archive, products[0].stage_out) != 0
|| copy_file_stage(iface, products[0].stage_iface) != 0) {
fprintf(stderr,
"ww: cannot stage package artifact %s\n", out);
goto request_fail;
}
}
if (sep_stage_product_status(&products[0]) != 0) {
fprintf(stderr, "ww: cannot stage package-build product\n");
goto request_fail;
}
goto prepare_transaction;
}
/* Each product gets its own reverse-topological archive closure: root `.a`
* first, then every transitively reachable package `.a`, then libwwrt.a. An
* internal test variant already contains production sources, so its
* colocated production archive is omitted without dropping dependencies. */
char rtpaths[2][PATH_MAX];
int nrt = 1;
int rn = snprintf(rtpaths[0], sizeof rtpaths[0], "%s/libwwrt.a", libdir);
int have_archive = rn >= 0 && (size_t)rn < sizeof rtpaths[0]
&& access(rtpaths[0], 0) == 0;
if (!have_archive) {
nrt = 2;
rn = snprintf(rtpaths[0], sizeof rtpaths[0],
"%s/../obj/rt/start.o", self_dir);
if (rn < 0 || (size_t)rn >= sizeof rtpaths[0]) goto request_fail;
rn = snprintf(rtpaths[1], sizeof rtpaths[1],
"%s/../obj/rt/syscall.o", self_dir);
if (rn < 0 || (size_t)rn >= sizeof rtpaths[1]) goto request_fail;
}
int nlibdirs = linkflags ? linkflags->nlibdirs : 0;
int nlibs = linkflags ? linkflags->nlibs : 0;
for (int i = 0; i < nproducts; i++) {
int root = products[i].root;
int variant_root = products[i].variant_root;
if (!is_test && !sep_root_is_command(&g->pkg[root])) {
if (sep_stage_product_status(&products[i]) != 0) {
fprintf(stderr, "ww: cannot stage package-build product\n");
goto request_fail;
}
continue;
}
if (sep_prepare_product_stage(&products[i].stage_out,
products[i].out) < 0)
goto request_fail;
for (int pi = 0; pi < g->n; pi++) g->pkg[pi].color = 0;
int *linkorder = calloc((size_t)g->n, sizeof *linkorder);
int *linkstack = calloc((size_t)g->n, sizeof *linkstack);
int nlink = 0;
if (linkorder == NULL || linkstack == NULL) {
fprintf(stderr, "ww: out of memory\n");
free(linkstack); free(linkorder); goto request_fail;
}
if (sep_topo_visit(g, root, linkorder, &nlink,
linkstack, 0) < 0) {
free(linkstack); free(linkorder); goto request_fail;
}
free(linkstack);
size_t largvcap = 4;
if ((size_t)nlink > (size_t)-1 - largvcap
|| (size_t)nrt > (size_t)-1 - largvcap - (size_t)nlink
|| (size_t)nlibdirs > ((size_t)-1 - largvcap
- (size_t)nlink - (size_t)nrt) / 2
|| (size_t)nlibs > ((size_t)-1 - largvcap
- (size_t)nlink - (size_t)nrt
- 2 * (size_t)nlibdirs) / 2) {
fprintf(stderr, "ww: package graph is too large\n");
free(linkorder);
goto request_fail;
}
largvcap += (size_t)nlink + (size_t)nrt
+ 2 * (size_t)nlibdirs + 2 * (size_t)nlibs;
char **largv = calloc(largvcap, sizeof *largv);
char (*linkpaths)[SEP_ARTIFACT_MAX] = calloc((size_t)nlink,
sizeof *linkpaths);
if (largv == NULL || linkpaths == NULL) {
fprintf(stderr, "ww: out of memory\n");
free(linkpaths); free(largv); free(linkorder);
goto request_fail;
}
int pos = 0, npath = 0;
largv[pos++] = "w6l";
largv[pos++] = "-o";
largv[pos++] = products[i].stage_out;
for (int oi = nlink - 1; oi >= 0; oi--) {
int pi = linkorder[oi];
if (variant_root >= 0
&& sep_internal_replaces_production(g, variant_root, pi))
continue;
const char *suffix = warm && g->pkg[pi].archive_staged
? ".a.new" : ".a";
sep_fname(g, pi, scratch, suffix, linkpaths[npath],
sizeof linkpaths[npath]);
largv[pos++] = linkpaths[npath++];
}
free(linkorder);
for (int ri = 0; ri < nrt; ri++) largv[pos++] = rtpaths[ri];
for (int li = 0; li < nlibdirs; li++) {
largv[pos++] = "-L";
largv[pos++] = (char *)linkflags->libdirs[li];
}
for (int li = 0; li < nlibs; li++) {
largv[pos++] = "-l";
largv[pos++] = (char *)linkflags->libs[li];
}
largv[pos] = NULL;
int linkrc = run_argv(l6, largv);
free(linkpaths);
free(largv);
if (linkrc != 0) {
fprintf(stderr, "ww: w6l failed\n");
g->pkg[root].failed = 1;
goto request_fail;
}
if (sep_stage_product_status(&products[i]) != 0) {
fprintf(stderr, "ww: cannot stage package-test product\n");
goto request_fail;
}
}
prepare_transaction:
if (warm) {
for (int oi = 0; oi < norder; oi++) {
int pi = order[oi];
if (g->pkg[pi].source_staged
&& (sep_txn_add_pkg_suffix(&tx, g, pi, scratch,
".wwi.new", ".wwi") < 0
|| sep_txn_add_pkg_suffix(&tx, g, pi, scratch,
".s.new", ".s") < 0
|| (!emit_asm && sep_txn_add_pkg_suffix(&tx, g, pi,
scratch, ".o.new", ".o") < 0)))
goto request_fail;
if (g->pkg[pi].init_staged
&& (sep_txn_add_pkg_suffix(&tx, g, pi, scratch,
".init.s.new", ".init.s") < 0
|| (!emit_asm && sep_txn_add_pkg_suffix(&tx, g, pi,
scratch, ".init.o.new", ".init.o") < 0)))
goto request_fail;
if (g->pkg[pi].archive_staged
&& sep_txn_add_pkg_suffix(&tx, g, pi, scratch,
".a.new", ".a") < 0)
goto request_fail;
if (g->pkg[pi].source_staged
&& sep_txn_add_pkg_suffix(&tx, g, pi, scratch,
".unit.new", ".unit.ww") < 0)
goto request_fail;
if (g->pkg[pi].init_staged
&& sep_txn_add_pkg_suffix(&tx, g, pi, scratch,
".init.unit.new", ".init.unit.ww") < 0)
goto request_fail;
}
const char *toolsrc[] = { self_path, c6, a6 };
const char *tooldst[] = { toolw, toolc, toola };
int ntools = emit_asm ? 2 : 3;
for (int i = 0; i < ntools; i++) {
if (file_equal(tooldst[i], toolsrc[i])) continue;
char stage[PATH_MAX];
int sn = snprintf(stage, sizeof stage, "%s.new", tooldst[i]);
if (sn < 0 || (size_t)sn >= sizeof stage
|| copy_file_stage(toolsrc[i], stage) != 0
|| sep_txn_add(&tx, stage, tooldst[i]) < 0) {
fprintf(stderr, "ww: cannot stage workdir tool identity\n");
goto request_fail;
}
}
if (!stampok) {
char stage[PATH_MAX];
int sn = snprintf(stage, sizeof stage, "%s.new", stampf);
if (sn < 0 || (size_t)sn >= sizeof stage
|| sep_write_text_stage(stage, stampwant) != 0
|| sep_txn_add(&tx, stage, stampf) < 0) {
fprintf(stderr, "ww: cannot stage workdir stamp\n");
goto request_fail;
}
}
}
for (int i = 0; i < nproducts; i++) {
if (products[i].stage_out != NULL
&& sep_txn_add(&tx, products[i].stage_out,
products[i].out) < 0)
goto request_fail;
if (products[i].stage_iface != NULL) {
char outiface[SEP_ARTIFACT_MAX];
int on = snprintf(outiface, sizeof outiface, "%s.wwi",
products[i].out);
if (on < 0 || (size_t)on >= sizeof outiface
|| sep_txn_add(&tx, products[i].stage_iface,
outiface) < 0)
goto request_fail;
}
}
for (int i = 0; i < nproducts; i++)
if (products[i].stage_status != NULL
&& sep_txn_add(&tx, products[i].stage_status,
products[i].status) < 0)
goto request_fail;
if (sep_txn_commit(&tx) != 0) goto request_fail;
sep_txn_free(&tx);
sep_free_product_staging(products, nproducts);
free(order);
return 0;
request_fail:
sep_txn_discard(&tx);
(void)sep_discard_request_staging(g, scratch, warm, products, nproducts);
sep_txn_free(&tx);
sep_free_product_staging(products, nproducts);
if (!warm) {
if (rmdir(scratch) != 0 && errno != ENOENT)
fprintf(stderr, "ww: cannot remove rejected scratch %s\n", scratch);
else if (scratchout != NULL) scratchout[0] = '\0';
} else {
sep_rollback_dirs(&created_work);
}
sep_rollback_dirs(&created_output);
free(order);
return 1;
}
/* build_one_sep — thin wrapper over build_one_sep_impl. `ww build` and an
* explicit `ww test -o` retain caller-visible `.sepwork` artifacts; their
* caller owns that exact tree. `ww run` and a no-output single-file test use
* internal scratch and remove it on success and failure. One cleanup site
* covers every internal-scratch impl return. The path is nonempty only after
* this invocation successfully created the exact `.sepwork` tree. */
static int
build_one_sep(const char *src, int entry_is_dir, const char *root_identity,
const char *out,
const char *objstem, const char *extra_includes,
const struct seplinkflags *linkflags, int publish_package,
int require_command, int is_test,
int root_variant, const char *test_package, int emit_asm,
int keepscratch, const char *workdir)
{
char scratch[PATH_MAX] = {0};
struct sepgraph *g = NULL;
struct sepproduct product = {
.dir = src,
.out = out,
.identity = root_identity,
.test_package = test_package,
.status = NULL,
.artifact = NULL,
.variant = root_variant,
.root = -1,
.variant_root = -1,
.support = -1,
};
if (!entry_is_dir)
product.artifact = "__root";
int r = build_one_sep_impl(src, entry_is_dir, out, objstem,
extra_includes, linkflags, publish_package, require_command, is_test,
&product, 1, emit_asm, workdir, 0, NULL, scratch,
sizeof scratch, &g);
sep_graph_free(g);
if (!keepscratch && scratch[0]) {
size_t sl = strlen(scratch);
if (sl > 8 && strcmp(scratch + sl - 8, ".sepwork") == 0) {
int cleanrc = 1;
pid_t pid = fork();
if (pid == 0) {
execl("/bin/rm", "rm", "-rf", "--", scratch,
(char *)NULL);
_exit(127);
}
if (pid > 0) {
int status = 0;
if (waitpid(pid, &status, 0) == pid &&
WIFEXITED(status))
cleanrc = WEXITSTATUS(status);
}
if (cleanrc != 0) {
fprintf(stderr, "ww: cannot remove scratch %s\n", scratch);
if (r == 0) r = 1;
}
}
}
return r;
}
/* The package coordinator submits every selected directory/variant root in one
* request. Its first output owns the shared cold sepwork tree; every product
* remains an independent root compile and link inside that tree. */
static int
build_package_tests(const char *src, const char *root_identity,
const char *extra_includes, const char *workdir,
struct sepproduct *products, int nproducts, int is_test,
int publish_package, const struct seplinkflags *linkflags, int emit_asm,
int create_workdir, const char *create_output_dir)
{
char scratch[PATH_MAX] = {0};
struct sepgraph *g = NULL;
for (int i = 0; i < nproducts; i++)
products[i].identity = root_identity;
int r = build_one_sep_impl(src, 1, products[0].out,
products[0].out, extra_includes, linkflags, publish_package, 0, is_test,
products, nproducts, emit_asm, workdir, create_workdir,
create_output_dir, scratch, sizeof scratch, &g);
sep_graph_free(g);
return r;
}
static int
do_version(void)
{
printf("ww %s\n", WW_VERSION);
return 0;
}
/* Compose the standard module search path: cwd : <extra-includes> : selected
* source library. The `extra` string is colon-separated -I dirs from argv. */
static char *
search_path(const char *extra)
{
const char *libdir = envpath("WW_SRCLIB");
static char libbuf[PATH_MAX];
if (libdir == NULL) {
libdir = envpath("WW_LIB");
}
if (libdir == NULL) {
int n = snprintf(libbuf, sizeof libbuf, "%s/../../lib", self_dir);
if (n < 0 || (size_t)n >= sizeof libbuf) return NULL;
if (access(libbuf, 0) == 0) libdir = libbuf;
else if (access("lib", 0) == 0) libdir = "lib";
else {
n = snprintf(libbuf, sizeof libbuf, "%s/../lib", self_dir);
if (n < 0 || (size_t)n >= sizeof libbuf) return NULL;
libdir = libbuf;
}
}
if (extra && extra[0])
return sep_sprintf(".:%s:%s", extra, libdir);
return sep_sprintf(".:%s", libdir);
}
static void
basename_no_ext(const char *path, char *out, size_t outsz)
{
const char *base = strrchr(path, '/');
base = base ? base + 1 : path;
snprintf(out, outsz, "%s", base);
char *dot = strrchr(out, '.');
if (dot && strcmp(dot, ".ww") == 0) *dot = '\0';
}
static int
resolve_module(const char *name, const char *incs, char *out, size_t outsz,
int *is_dir)
{
struct stat st;
if (stat(name, &st) == 0) {
if (S_ISREG(st.st_mode)) {
if (strlen(name) + 1 > outsz) return 0;
memcpy(out, name, strlen(name) + 1);
*is_dir = 0;
return 1;
}
if (S_ISDIR(st.st_mode)) {
if (strlen(name) + 1 > outsz) return 0;
memcpy(out, name, strlen(name) + 1);
*is_dir = 1;
return 1;
}
}
if (reserved_import_path(name)) return 0;
char *sp = search_path(incs);
char *path_form = malloc(strlen(name) + 1);
if (sp == NULL || path_form == NULL) {
free(sp); free(path_form);
return 0;
}
if (import_path_form(name, path_form, strlen(name) + 1) < 0) {
free(sp); free(path_form);
return 0;
}
int found = locate_module(sp, path_form, out, outsz, is_dir);
free(sp);
free(path_form);
return found;
}
static int
cli_copy(const char *cmd, const char *flag, char *out, size_t outsz,
const char *value)
{
size_t n = strlen(value);
if (n + 1 > outsz) {
fprintf(stderr, "ww %s: %s path is too long\n", cmd, flag);
return -1;
}
memcpy(out, value, n + 1);
return 0;
}
static int
include_append(const char *cmd, char *incs, size_t incsz, const char *dir)
{
size_t n = strlen(incs), dn = strlen(dir);
if (n + (n != 0) + dn + 1 > incsz) {
fprintf(stderr, "ww %s: -I search path is too long\n", cmd);
return -1;
}
if (n != 0) incs[n++] = ':';
memcpy(incs + n, dir, dn + 1);
return 0;
}
static int
include_buffer_for_args(int argc, char **argv, char **out, size_t *outsz)
{
size_t n = 1;
for (int i = 0; i < argc; i++) {
size_t an = strlen(argv[i]);
if (n == (size_t)-1 || an > (size_t)-1 - n - 1) {
fprintf(stderr, "ww: package graph is too large\n");
return -1;
}
n += an + 1;
}
char *p = calloc(n, 1);
if (p == NULL) {
fprintf(stderr, "ww: out of memory\n");
return -1;
}
*out = p;
*outsz = n;
return 0;
}
/* Returns the index past the last arg consumed for positionals (so callers
* can pick up trailing args), or -1 if a flag is missing its argument
* (diagnostic already emitted). `cmd` names the subcommand for the
* diagnostic, byte-identical to the wwstage twin's per-subcommand wording
* (selfhost/cmd/ww/main.ww dobuild/dorun). */
static int
parse_build_flags(const char *cmd, int argc, char **argv,
char *incs, size_t incsz,
struct seplinkflags *linkflags,
char *outpath, size_t outsz,
char *workdir, size_t workdirsz,
const char **src_out, int *emit_asm_out, int *terminator_out)
{
*src_out = NULL;
if (emit_asm_out) *emit_asm_out = 0;
if (terminator_out) *terminator_out = 0;
int i = 0;
for (; i < argc; i++) {
if (strcmp(cmd, "build") == 0 && strcmp(argv[i], "--") == 0) {
if (terminator_out) *terminator_out = 1;
i++;
break;
} else if (strcmp(argv[i], "-S") == 0) {
if (emit_asm_out == NULL) {
fprintf(stderr, "ww %s: unknown flag\n", cmd);
return -1;
}
*emit_asm_out = 1;
} else if (strcmp(argv[i], "-w") == 0) {
if (workdir == NULL) {
fprintf(stderr, "ww %s: unknown flag\n", cmd);
return -1;
}
if (i + 1 >= argc) {
fprintf(stderr,
"ww %s: -w needs an argument\n", cmd);
return -1;
}
if (cli_copy(cmd, "-w", workdir, workdirsz, argv[++i]) < 0)
return -1;
} else if (strncmp(argv[i], "-w", 2) == 0 && argv[i][2]) {
if (workdir == NULL) {
fprintf(stderr, "ww %s: unknown flag\n", cmd);
return -1;
}
if (cli_copy(cmd, "-w", workdir, workdirsz, argv[i] + 2) < 0)
return -1;
} else if (strncmp(argv[i], "-l", 2) == 0 && argv[i][2]) {
if (linkflags->nlibs >= SEP_MAXLFLAGS) {
fprintf(stderr, "ww %s: too many -l\n", cmd);
return -1;
}
linkflags->libs[linkflags->nlibs++] = argv[i] + 2;
} else if (strcmp(argv[i], "-l") == 0) {
if (i + 1 >= argc) {
fprintf(stderr,
"ww %s: -l needs an argument\n", cmd);
return -1;
}
if (linkflags->nlibs >= SEP_MAXLFLAGS) {
fprintf(stderr, "ww %s: too many -l\n", cmd);
return -1;
}
linkflags->libs[linkflags->nlibs++] = argv[++i];
} else if (strcmp(argv[i], "-L") == 0) {
if (i + 1 >= argc) {
fprintf(stderr,
"ww %s: -L needs an argument\n", cmd);
return -1;
}
if (linkflags->nlibdirs >= SEP_MAXLFLAGS) {
fprintf(stderr, "ww %s: too many -L\n", cmd);
return -1;
}
linkflags->libdirs[linkflags->nlibdirs++] = argv[++i];
} else if (strncmp(argv[i], "-L", 2) == 0 && argv[i][2]) {
if (linkflags->nlibdirs >= SEP_MAXLFLAGS) {
fprintf(stderr, "ww %s: too many -L\n", cmd);
return -1;
}
linkflags->libdirs[linkflags->nlibdirs++] = argv[i] + 2;
} else if (strcmp(argv[i], "-I") == 0) {
if (i + 1 >= argc) {
fprintf(stderr,
"ww %s: -I needs an argument\n", cmd);
return -1;
}
if (include_append(cmd, incs, incsz, argv[++i]) < 0)
return -1;
} else if (strncmp(argv[i], "-I", 2) == 0 && argv[i][2]) {
if (include_append(cmd, incs, incsz, argv[i] + 2) < 0)
return -1;
} else if (strcmp(argv[i], "-o") == 0) {
if (i + 1 >= argc) {
fprintf(stderr,
"ww %s: -o needs an argument\n", cmd);
return -1;
}
if (cli_copy(cmd, "-o", outpath, outsz, argv[++i]) < 0)
return -1;
} else if (strncmp(argv[i], "-o", 2) == 0 && argv[i][2]) {
if (cli_copy(cmd, "-o", outpath, outsz, argv[i] + 2) < 0)
return -1;
} else if (argv[i][0] == '-') {
fprintf(stderr, "ww %s: unknown flag\n", cmd);
return -1;
} else if (*src_out == NULL) {
*src_out = argv[i];
/* Go's FlagSet stops at the first positional. For build,
* everything after it is package-request input, even "--". */
if (strcmp(cmd, "build") == 0) {
i++;
break;
}
} else {
break; /* leave remaining argv to caller (run-args) */
}
}
return i;
}
static int
do_build(int argc, char **argv)
{
const char *src = NULL;
struct seplinkflags linkflags = {0};
size_t incsz = 0;
char *incs = NULL;
if (include_buffer_for_args(argc, argv, &incs, &incsz) < 0) return 1;
char outflag[PATH_MAX] = {0};
char workdir[PATH_MAX] = {0};
int emit_asm = 0;
int saw_terminator = 0;
int next = parse_build_flags("build", argc, argv, incs, incsz,
&linkflags,
outflag, sizeof outflag, workdir, sizeof workdir,
&src, &emit_asm, &saw_terminator);
if (next < 0) {
free(incs);
return 2;
}
if (src == NULL) src = ".";
if (strstr(src, "...") != NULL || next < argc || saw_terminator) {
free(incs);
return exec_package_command(argc, argv, src, NULL, NULL, 0, 1);
}
struct stat requested;
int literal = stat(src, &requested) == 0;
char resolved[PATH_MAX];
int is_dir = 0;
if (!resolve_module(src, incs, resolved, sizeof resolved, &is_dir)) {
fprintf(stderr, "ww build: cannot find module %s\n", src);
free(incs);
return 1;
}
char out[PATH_MAX];
const char *objstem = NULL;
if (outflag[0]) {
/* -o sets both the binary path and the intermediate stem so
* artifacts land beside the requested output (T3). */
memcpy(out, outflag, strlen(outflag) + 1);
objstem = out;
} else if (is_dir) {
char tmp[PATH_MAX];
memcpy(tmp, resolved, strlen(resolved) + 1);
size_t n = strlen(tmp);
while (n > 1 && tmp[n-1] == '/') tmp[--n] = '\0';
const char *b = strrchr(tmp, '/');
snprintf(out, sizeof out, "%s", b ? b + 1 : tmp);
} else {
basename_no_ext(resolved, out, sizeof out);
}
const char *root_identity = !literal && is_dir ? src : NULL;
int rc = build_one_sep(resolved, is_dir, root_identity, out, objstem, incs,
&linkflags, outflag[0] != '\0', 0, 0, SEP_VARIANT_PRODUCTION, NULL,
emit_asm, 1, workdir);
free(incs);
return rc;
}
static int
do_run(int argc, char **argv)
{
const char *src = NULL;
struct seplinkflags linkflags = {0};
size_t incsz = 0;
char *incs = NULL;
if (include_buffer_for_args(argc, argv, &incs, &incsz) < 0) return 1;
char outflag[PATH_MAX] = {0}; /* -o accepted+ignored: run always uses the temp */
int next = parse_build_flags("run", argc, argv, incs, incsz,
&linkflags,
outflag, sizeof outflag, NULL, 0, &src, NULL, NULL);
if (next < 0) { free(incs); return 2; }
if (src == NULL) src = ".";
struct stat requested;
int literal = stat(src, &requested) == 0;
char resolved[PATH_MAX];
int is_dir = 0;
if (!resolve_module(src, incs, resolved, sizeof resolved, &is_dir)) {
fprintf(stderr, "ww run: cannot find module %s\n", src);
free(incs);
return 1;
}
char tmpdir[PATH_MAX], tmp[PATH_MAX];
snprintf(tmpdir, sizeof tmpdir, "/tmp/ww_run_%d", getpid());
if (mkdir(tmpdir, 0700) != 0) {
fprintf(stderr, "ww: cannot create temporary directory %s\n", tmpdir);
free(incs);
return 1;
}
int tn = snprintf(tmp, sizeof tmp, "%s/main", tmpdir);
if (tn < 0 || (size_t)tn >= sizeof tmp) {
free(incs);
return 1;
}
/* The freshly acquired directory owns both the executable and the
* adjacent main.sepwork tree. Nothing outside it is adopted or removed. */
const char *root_identity = !literal && is_dir ? src : NULL;
int buildrc = build_one_sep(resolved, is_dir, root_identity, tmp, tmp, incs,
&linkflags,
0, 1, 0, SEP_VARIANT_PRODUCTION, NULL, 0, 0, NULL);
free(incs);
if (buildrc != 0) {
if (unlink(tmp) != 0 && errno != ENOENT)
fputs("ww: cannot remove temporary output\n", stderr);
if (rmdir(tmpdir) != 0)
fputs("ww: cannot remove temporary directory\n", stderr);
return 1;
}
pid_t pid = fork();
if (pid < 0) {
perror("ww: fork");
if (unlink(tmp) != 0 && errno != ENOENT)
fputs("ww: cannot remove temporary output\n", stderr);
if (rmdir(tmpdir) != 0)
fputs("ww: cannot remove temporary directory\n", stderr);
return 1;
}
if (pid == 0) {
int n_extra = argc - next;
char **xargv = calloc((size_t)n_extra + 2, sizeof *xargv);
xargv[0] = tmp;
for (int i = 0; i < n_extra; i++) xargv[i+1] = argv[next + i];
xargv[n_extra+1] = NULL;
execv(tmp, xargv);
perror("ww: exec");
_exit(127);
}
int status = 0;
pid_t got;
do { got = waitpid(pid, &status, 0); } while (got < 0 && errno == EINTR);
int rc = got == pid && WIFEXITED(status) ? WEXITSTATUS(status) : 1;
if (got != pid) perror("ww: waitpid");
if (unlink(tmp) != 0 && errno != ENOENT) {
fputs("ww: cannot remove temporary output\n", stderr);
if (rc == 0) rc = 1;
}
if (rmdir(tmpdir) != 0) {
fputs("ww: cannot remove temporary directory\n", stderr);
if (rc == 0) rc = 1;
}
return rc;
}
static int
do_test(int argc, char **argv)
{
const char *src = NULL;
struct sepproduct *products = NULL;
int nproducts = 0, productcap = 0;
size_t incsz = 0;
char *incs = NULL;
if (include_buffer_for_args(argc, argv, &incs, &incsz) < 0) return 1;
/* -c (Go's `go test -c`) builds the test binary without running it.
* -S + -o <stem> stops after the lib/test-inclusive package `.s`
* outputs are emitted. Both routes use build_one_sep's is_test bundle
* and T3 objstem redirect.
* -T stays internal to w6c; the driver never sees it. -l/-L carry no
* meaning for a test build, so they (and any unknown flag) are rejected
* rather than silently swallowed — byte-identical wording to the
* wwstage twin (selfhost/cmd/ww/main.ww dotest). */
int compileonly = 0;
int emit_asm = 0;
char outstem[PATH_MAX] = {0};
char workdir[PATH_MAX] = {0};
int packageopts = 0;
int package_build = 0;
int package_publish = 0;
int package_create_workdir = 0;
const char *package_create_output_dir = NULL;
struct seplinkflags package_linkflags = {0};
int afterdash = 0;
const char *request_identity = NULL;
/* #17: an optional second positional after the target is a fnmatch
* name-filter pattern, forwarded to the test binary as argv[1]. Only
* meaningful for a single test file/module — rejected in dir mode. */
const char *pattern = NULL;
for (int i = 0; i < argc; i++) {
if (afterdash) continue;
if (argv[i][0] == '-') {
if (strcmp(argv[i], "--") == 0) {
packageopts = 1;
afterdash = 1;
continue;
}
if (argv[i][1] == 'I') {
const char *dir;
if (argv[i][2]) {
dir = argv[i] + 2;
} else {
if (i + 1 >= argc) {
fprintf(stderr,
"ww test: -I needs an argument\n");
return 2;
}
dir = argv[++i];
}
if (include_append("test", incs, incsz, dir) < 0)
return 2;
} else if (strcmp(argv[i], "-c") == 0) {
compileonly = 1;
} else if (strcmp(argv[i], "--ww-root-identity") == 0) {
if (i + 1 >= argc || request_identity != NULL
|| argv[i + 1][0] == '\0'
|| reserved_import_path(argv[i + 1])) {
fprintf(stderr,
"ww test: invalid --ww-root-identity\n");
return 2;
}
request_identity = argv[++i];
} else if (strcmp(argv[i], "--ww-package-build") == 0) {
if (package_build) {
fprintf(stderr,
"ww test: invalid --ww-package-build\n");
return 2;
}
package_build = 1;
compileonly = 1;
} else if (strcmp(argv[i], "--ww-package-publish") == 0) {
if (package_publish) {
fprintf(stderr,
"ww test: invalid --ww-package-publish\n");
return 2;
}
package_publish = 1;
} else if (strcmp(argv[i], "--ww-create-workdir") == 0) {
if (package_create_workdir) {
fprintf(stderr,
"ww test: invalid --ww-create-workdir\n");
return 2;
}
package_create_workdir = 1;
} else if (strcmp(argv[i], "--ww-create-output-dir") == 0) {
if (i + 1 >= argc || package_create_output_dir != NULL
|| argv[i + 1][0] == '\0') {
fprintf(stderr,
"ww test: invalid --ww-create-output-dir\n");
return 2;
}
package_create_output_dir = argv[++i];
} else if (strcmp(argv[i], "--ww-package-test") == 0) {
if (i + 5 >= argc) {
fprintf(stderr,
"ww test: --ww-package-test needs kind, package, directory, output, and status\n");
return 2;
}
const char *kind = argv[++i];
const char *name = argv[++i];
const char *dir = argv[++i];
const char *output = argv[++i];
const char *status = argv[++i];
size_t pn = strlen(name);
int variant = SEP_VARIANT_EXTERNAL;
if (strcmp(kind, "production") == 0)
variant = SEP_VARIANT_PRODUCTION;
else if (strcmp(kind, "same") == 0)
variant = SEP_VARIANT_SAME_TEST;
if ((strcmp(kind, "production") != 0
&& strcmp(kind, "same") != 0
&& strcmp(kind, "external") != 0)
|| pn == 0
|| dir[0] == '\0' || output[0] == '\0'
|| status[0] == '\0'
|| (strcmp(kind, "external") == 0
&& (pn <= 5
|| strcmp(name + pn - 5,
"_test") != 0))) {
fprintf(stderr,
"ww test: invalid --ww-package-test variant\n");
return 2;
}
if (nproducts == INT_MAX) {
sep_fail_size();
return 1;
}
if (sep_reserve((void **)&products, &productcap,
nproducts + 1, sizeof *products) < 0)
return 1;
products[nproducts].dir = dir;
products[nproducts].out = output;
products[nproducts].test_package = name;
products[nproducts].status = status;
products[nproducts].artifact = NULL;
products[nproducts].variant = variant;
products[nproducts].root = -1;
products[nproducts].variant_root = -1;
products[nproducts].support = -1;
nproducts++;
} else if (strcmp(argv[i], "-S") == 0) {
emit_asm = 1;
} else if (package_build
&& strncmp(argv[i], "-l", 2) == 0 && argv[i][2]) {
if (package_linkflags.nlibs >= SEP_MAXLFLAGS) {
fprintf(stderr, "ww test: too many -l\n");
return 2;
}
package_linkflags.libs[package_linkflags.nlibs++] = argv[i] + 2;
} else if (package_build && strcmp(argv[i], "-l") == 0) {
if (i + 1 >= argc) {
fprintf(stderr, "ww test: -l needs an argument\n");
return 2;
}
if (package_linkflags.nlibs >= SEP_MAXLFLAGS) {
fprintf(stderr, "ww test: too many -l\n");
return 2;
}
package_linkflags.libs[package_linkflags.nlibs++] = argv[++i];
} else if (package_build && strcmp(argv[i], "-L") == 0) {
if (i + 1 >= argc) {
fprintf(stderr, "ww test: -L needs an argument\n");
return 2;
}
if (package_linkflags.nlibdirs >= SEP_MAXLFLAGS) {
fprintf(stderr, "ww test: too many -L\n");
return 2;
}
package_linkflags.libdirs[package_linkflags.nlibdirs++] = argv[++i];
} else if (package_build
&& strncmp(argv[i], "-L", 2) == 0 && argv[i][2]) {
if (package_linkflags.nlibdirs >= SEP_MAXLFLAGS) {
fprintf(stderr, "ww test: too many -L\n");
return 2;
}
package_linkflags.libdirs[package_linkflags.nlibdirs++] = argv[i] + 2;
} else if (strcmp(argv[i], "-list") == 0) {
packageopts = 1;
} else if (strcmp(argv[i], "-j") == 0 ||
strcmp(argv[i], "-run") == 0 ||
strcmp(argv[i], "-filter") == 0) {
if (i + 1 >= argc) {
fprintf(stderr, "ww test: %s needs an argument\n",
argv[i]);
return 2;
}
packageopts = 1;
i++;
} else if (strncmp(argv[i], "-timeout-ms=", 12) == 0 &&
argv[i][12] != '\0') {
packageopts = 1;
} else if (strcmp(argv[i], "-o") == 0) {
if (i + 1 >= argc) {
fprintf(stderr,
"ww test: -o needs an argument\n");
return 2;
}
if (cli_copy("test", "-o", outstem, sizeof outstem,
argv[++i]) < 0) return 2;
} else if (argv[i][1] == 'o' && argv[i][2]) {
if (cli_copy("test", "-o", outstem, sizeof outstem,
argv[i] + 2) < 0) return 2;
} else if (strcmp(argv[i], "-w") == 0) {
if (i + 1 >= argc) {
fprintf(stderr,
"ww test: -w needs an argument\n");
return 2;
}
if (cli_copy("test", "-w", workdir, sizeof workdir,
argv[++i]) < 0) return 2;
} else if (argv[i][1] == 'w' && argv[i][2]) {
if (cli_copy("test", "-w", workdir, sizeof workdir,
argv[i] + 2) < 0) return 2;
} else {
fprintf(stderr, "ww test: unknown flag\n");
return 2;
}
} else if (src == NULL) {
src = argv[i];
} else if (pattern == NULL) {
pattern = argv[i];
}
}
const char *target = src ? src : ".";
if (emit_asm && !outstem[0] && nproducts == 0) {
fprintf(stderr, "ww test: -S needs -o\n");
return 2;
}
for (int i = 1; i < nproducts; i++) {
struct sepproduct p = products[i];
int j = i;
while (j > 0 && (strcmp(products[j - 1].dir, p.dir) > 0
|| (strcmp(products[j - 1].dir, p.dir) == 0
&& products[j - 1].variant > p.variant))) {
products[j] = products[j - 1];
j--;
}
products[j] = p;
}
for (int i = 0; i < nproducts; i++) {
if (i > 0 && strcmp(products[i - 1].dir, products[i].dir) == 0
&& products[i - 1].variant == products[i].variant) {
fprintf(stderr,
"ww test: duplicate --ww-package-test variant for directory\n");
return 2;
}
/* Artifact identity is derived from canonical package identity after
* discovery; product position is deliberately not an action key. */
products[i].artifact = NULL;
}
if (nproducts != 0 && packageopts) {
fprintf(stderr,
"ww test: package-test variant rejects package options\n");
return 2;
}
if (package_build && nproducts == 0) {
fprintf(stderr, "ww test: --ww-package-build needs products\n");
return 2;
}
if (package_publish && (!package_build || nproducts != 1)) {
fprintf(stderr, "ww test: invalid --ww-package-publish\n");
return 2;
}
if (package_create_output_dir != NULL && !package_build) {
fprintf(stderr, "ww test: invalid private directory creation\n");
return 2;
}
if (package_create_workdir && nproducts == 0) {
fprintf(stderr, "ww test: invalid private directory creation\n");
return 2;
}
if (package_create_workdir && !workdir[0]) {
fprintf(stderr, "ww test: --ww-create-workdir needs -w\n");
return 2;
}
if (!package_build && (package_linkflags.nlibdirs != 0
|| package_linkflags.nlibs != 0)) {
fprintf(stderr, "ww test: unknown flag\n");
return 2;
}
if (package_build) {
for (int i = 0; i < nproducts; i++) {
if (products[i].variant != SEP_VARIANT_PRODUCTION) {
fprintf(stderr,
"ww test: package-build products must be production\n");
return 2;
}
}
}
if (pattern != NULL) {
struct stat first;
if (stat(target, &first) != 0 || !S_ISREG(first.st_mode)) {
char first_resolved[PATH_MAX];
int first_is_dir = 0;
if (stat(target, &first) == 0
|| !resolve_module(target, incs, first_resolved,
sizeof first_resolved, &first_is_dir)
|| first_is_dir) {
return exec_package_command(argc, argv, src,
first_is_dir ? first_resolved : NULL,
first_is_dir ? target : request_identity,
0, 0);
}
/* A logical import that resolves to one regular source keeps
* the historical second-positional test-name filter. */
}
}
if (nproducts != 0 && outstem[0]) {
fprintf(stderr,
"ww test: package-test products reject -o\n");
return 2;
}
/* Every local spelling containing "..." is a package pattern. It enters
* request selection before literal-path or logical-import resolution. */
if (strstr(target, "...") != NULL) {
if (nproducts != 0) {
fprintf(stderr,
"ww test: package-test variant needs one directory\n");
return 2;
}
if (emit_asm) {
fprintf(stderr,
"ww test: -S needs a single test file\n");
return 2;
}
/* -c -o forwards: the coordinator names the single
* package's artifact and rejects a multi-package fan-out. */
if (outstem[0] && !compileonly) {
fprintf(stderr,
"ww test: -o needs -c for a package target\n");
return 2;
}
/* -w forwards one caller-owned semantic-action store shared by
* the complete selected package universe. */
return exec_package_command(argc, argv, src, NULL, NULL, 0, 0);
}
struct stat st;
if (stat(target, &st) != 0) {
/* not a literal path — try module resolution and run as
* a single test program. */
char resolved[PATH_MAX];
int is_dir = 0;
if (!resolve_module(target, incs, resolved, sizeof resolved,
&is_dir)) {
fprintf(stderr, "ww test: cannot find %s\n", target);
return 1;
}
if (is_dir) {
if (emit_asm && nproducts == 0) {
fprintf(stderr,
"ww test: -S needs a single test file\n");
return 2;
}
if (outstem[0] && !compileonly) {
fprintf(stderr,
"ww test: -o needs -c for a package target\n");
return 2;
}
if (nproducts != 0) {
if (!compileonly) {
fprintf(stderr,
"ww test: package-test products need -c\n");
return 2;
}
int r = build_package_tests(resolved, request_identity,
incs, workdir, products, nproducts,
package_build ? 0 : 1, package_publish,
package_build ? &package_linkflags : NULL, emit_asm,
package_create_workdir, package_create_output_dir);
free(products);
free(incs);
return r;
}
return exec_package_command(argc, argv, src, resolved,
request_identity != NULL ? request_identity : target, 0, 0);
}
if (packageopts) {
fprintf(stderr,
"ww test: package options need a directory\n");
return 2;
}
if (nproducts != 0) {
fprintf(stderr,
"ww test: package-test variant needs one directory\n");
return 2;
}
char tmpdir[PATH_MAX] = {0}, tmp[PATH_MAX];
const char *outp;
int owntmp = !outstem[0] && !workdir[0];
if (outstem[0]) outp = outstem;
else if (workdir[0]) {
/* The workdir owns the persistent test binary the same
* way it owns the package artifacts. */
int tn = snprintf(tmp, sizeof tmp, "%s/main", workdir);
if (tn < 0 || (size_t)tn >= sizeof tmp) {
fputs("ww: workdir path is too long\n", stderr);
return 1;
}
outp = tmp;
} else {
snprintf(tmpdir, sizeof tmpdir, "/tmp/ww_test_%d", getpid());
if (mkdir(tmpdir, 0700) != 0) {
fprintf(stderr, "ww: cannot create temporary directory %s\n",
tmpdir);
return 1;
}
int tn = snprintf(tmp, sizeof tmp, "%s/main", tmpdir);
if (tn < 0 || (size_t)tn >= sizeof tmp) return 1;
outp = tmp;
}
/* No-o redirects internal scratch to /tmp rather than beside the
* source. An explicit -o names the caller-owned artifact stem. */
int br = build_one_sep(resolved, is_dir, NULL, outp,
outstem[0] ? outstem : tmp, incs, NULL, 0, 0, 1,
SEP_VARIANT_PRODUCTION, NULL, emit_asm,
outstem[0] ? 1 : 0, workdir);
if (br != 0) {
if (owntmp && unlink(outp) != 0 && errno != ENOENT)
fputs("ww: cannot remove temporary output\n", stderr);
if (owntmp && rmdir(tmpdir) != 0)
fputs("ww: cannot remove temporary directory\n", stderr);
return 1;
}
if (compileonly || emit_asm) {
int cleanfail = 0;
if (owntmp && unlink(outp) != 0 && errno != ENOENT) {
fputs("ww: cannot remove temporary output\n", stderr);
cleanfail = 1;
}
if (owntmp && rmdir(tmpdir) != 0) {
fputs("ww: cannot remove temporary directory\n", stderr);
cleanfail = 1;
}
return cleanfail ? 1 : 0;
}
int rc = run_test_bin(outp, pattern);
if (owntmp && unlink(outp) != 0 && errno != ENOENT) {
fputs("ww: cannot remove temporary output\n", stderr);
if (rc == 0) rc = 1;
}
if (owntmp && rmdir(tmpdir) != 0) {
fputs("ww: cannot remove temporary directory\n", stderr);
if (rc == 0) rc = 1;
}
return rc;
}
if (S_ISREG(st.st_mode)) {
if (nproducts != 0) {
fprintf(stderr,
"ww test: package-test variant needs one directory\n");
return 2;
}
if (packageopts) {
fprintf(stderr,
"ww test: package options need a directory\n");
return 2;
}
char tmpdir[PATH_MAX] = {0}, tmp[PATH_MAX];
const char *outp;
int owntmp = !outstem[0] && !workdir[0];
if (outstem[0]) outp = outstem;
else if (workdir[0]) {
int tn = snprintf(tmp, sizeof tmp, "%s/main", workdir);
if (tn < 0 || (size_t)tn >= sizeof tmp) {
fputs("ww: workdir path is too long\n", stderr);
return 1;
}
outp = tmp;
} else {
snprintf(tmpdir, sizeof tmpdir, "/tmp/ww_test_%d", getpid());
if (mkdir(tmpdir, 0700) != 0) {
fprintf(stderr, "ww: cannot create temporary directory %s\n",
tmpdir);
return 1;
}
int tn = snprintf(tmp, sizeof tmp, "%s/main", tmpdir);
if (tn < 0 || (size_t)tn >= sizeof tmp) return 1;
outp = tmp;
}
/* See module-mode note: no-o scratch is redirected to /tmp. */
int br = build_one_sep(target, 0, NULL, outp, outstem[0] ? outstem : tmp,
incs, NULL, 0, 0, 1, SEP_VARIANT_PRODUCTION, NULL, emit_asm,
outstem[0] ? 1 : 0, workdir);
if (br != 0) {
if (owntmp && unlink(outp) != 0 && errno != ENOENT)
fputs("ww: cannot remove temporary output\n", stderr);
if (owntmp && rmdir(tmpdir) != 0)
fputs("ww: cannot remove temporary directory\n", stderr);
return 1;
}
if (compileonly || emit_asm) {
int cleanfail = 0;
if (owntmp && unlink(outp) != 0 && errno != ENOENT) {
fputs("ww: cannot remove temporary output\n", stderr);
cleanfail = 1;
}
if (owntmp && rmdir(tmpdir) != 0) {
fputs("ww: cannot remove temporary directory\n", stderr);
cleanfail = 1;
}
return cleanfail ? 1 : 0;
}
int rc = run_test_bin(outp, pattern);
if (owntmp && unlink(outp) != 0 && errno != ENOENT) {
fputs("ww: cannot remove temporary output\n", stderr);
if (rc == 0) rc = 1;
}
if (owntmp && rmdir(tmpdir) != 0) {
fputs("ww: cannot remove temporary directory\n", stderr);
if (rc == 0) rc = 1;
}
return rc;
}
if (!S_ISDIR(st.st_mode)) {
fprintf(stderr, "ww test: %s is neither file nor directory\n", target);
return 1;
}
if (emit_asm && nproducts == 0) {
fprintf(stderr, "ww test: -S needs a single test file\n");
return 2;
}
if (outstem[0] && !compileonly) {
fprintf(stderr, "ww test: -o needs -c for a package target\n");
return 2;
}
if (nproducts != 0) {
if (!compileonly) {
fprintf(stderr,
"ww test: package-test products need -c\n");
return 2;
}
int r = build_package_tests(target, request_identity, incs, workdir,
products, nproducts, package_build ? 0 : 1,
package_publish, package_build ? &package_linkflags : NULL,
emit_asm, package_create_workdir, package_create_output_dir);
free(products);
free(incs);
return r;
}
return exec_package_command(argc, argv, src, NULL, request_identity,
src == NULL, 0);
}
int
main(int argc, char **argv)
{
if (argc >= 1) {
self_path = argv[0];
char buf[PATH_MAX];
if (strlen(argv[0]) + 1 > sizeof buf) {
fputs("ww: driver path is too long\n", stderr);
return 1;
}
memcpy(buf, argv[0], strlen(argv[0]) + 1);
self_dir = strdup(dirname(buf));
}
if (argc < 2) { fputs(usage, stderr); return 2; }
const char *cmd = argv[1];
if (strcmp(cmd, "-V") == 0 || strcmp(cmd, "version") == 0)
return do_version();
if (strcmp(cmd, "-h") == 0 || strcmp(cmd, "--help") == 0) {
fputs(usage, stdout); return 0;
}
if (strcmp(cmd, "build") == 0) return do_build(argc - 2, argv + 2);
if (strcmp(cmd, "run") == 0) return do_run(argc - 2, argv + 2);
if (strcmp(cmd, "test") == 0) return do_test(argc - 2, argv + 2);
fprintf(stderr, "ww: unknown subcommand: %s\n", cmd);
fputs(usage, stderr);
return 2;
}