Files
ww/cmd/ww/main.c

7761 lines
240 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 <fcntl.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_name_is(const char *s, size_t n, const char *word)
{
return strlen(word) == n && memcmp(s, word, n) == 0;
}
static int
sep_known_os(const char *s, size_t n)
{
static const char *known[] = {
"aix", "android", "darwin", "dragonfly", "freebsd", "hurd",
"illumos", "ios", "js", "linux", "nacl", "netbsd", "openbsd",
"plan9", "solaris", "wasip1", "windows", "zos",
};
for (size_t i = 0; i < sizeof known / sizeof known[0]; i++)
if (sep_name_is(s, n, known[i])) return 1;
return 0;
}
static int
sep_known_arch(const char *s, size_t n)
{
static const char *known[] = {
"386", "amd64", "amd64p32", "arm", "armbe", "arm64", "arm64be",
"loong64", "mips", "mipsle", "mips64", "mips64le", "mips64p32",
"mips64p32le", "ppc", "ppc64", "ppc64le", "riscv", "riscv64",
"s390", "s390x", "sparc", "sparc64", "wasm",
};
for (size_t i = 0; i < sizeof known / sizeof known[0]; i++)
if (sep_name_is(s, n, known[i])) return 1;
return 0;
}
static int
sep_target_tag(const char *s, size_t n)
{
return sep_name_is(s, n, "linux") || sep_name_is(s, n, "amd64");
}
/* Go 1.26.5 build.go:1980-2027 filters known platform suffixes before
* opening a source. WW currently has one honest target, linux/amd64. */
static int
sep_source_matches_target(const char *name)
{
size_t stem = strcspn(name, ".");
if (memchr(name, '_', stem) == NULL) return 1;
size_t end = stem;
size_t last = end;
while (last > 0 && name[last - 1] != '_') last--;
if (sep_name_is(name + last, end - last, "test")) {
if (last == 0) return 1;
end = last - 1;
last = end;
while (last > 0 && name[last - 1] != '_') last--;
}
if (last == 0) return 1;
const char *final = name + last;
size_t nfinal = end - last;
size_t prevend = last - 1;
size_t prev = prevend;
while (prev > 0 && name[prev - 1] != '_') prev--;
if (prev < prevend
&& sep_known_os(name + prev, prevend - prev)
&& sep_known_arch(final, nfinal))
return sep_target_tag(final, nfinal)
&& sep_target_tag(name + prev, prevend - prev);
if (sep_known_os(final, nfinal) || sep_known_arch(final, nfinal))
return sep_target_tag(final, nfinal);
return 1;
}
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_VARIANT_TEST_COPY 4
#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 **names = NULL;
int nnames = 0, capnames = 0;
char **prod = NULL, **tests = NULL;
int nprod = 0, capprod = 0, ntests = 0, captests = 0;
struct dirent *ent = NULL;
for (;;) {
errno = 0;
ent = readdir(d);
if (ent == NULL) break;
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;
if (source_list_add(&names, &nnames, &capnames, nm) < 0) {
source_list_free(names, nnames);
closedir(d);
*out_files = NULL;
return -2;
}
}
int direrr = errno;
closedir(d);
if (direrr != 0) {
source_list_free(names, nnames);
*out_files = NULL;
return -1;
}
if (nnames > 1) qsort(names, (size_t)nnames, sizeof *names, strs_cmp);
for (int ni = 0; ni < nnames; ni++) {
const char *nm = names[ni];
size_t nl = strlen(nm);
if (!sep_source_matches_target(nm)) 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(names, nnames);
source_list_free(prod, nprod);
source_list_free(tests, ntests);
*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(names, nnames);
source_list_free(prod, nprod);
source_list_free(tests, ntests);
*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(names, nnames);
source_list_free(prod, nprod);
source_list_free(tests, ntests);
*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(names, nnames);
source_list_free(prod, nprod);
source_list_free(tests, ntests);
*out_files = NULL;
return -2;
}
int has_test = !is_test ? source_has_test_decl(path) : 0;
if (has_test < 0) {
source_list_free(names, nnames);
source_list_free(prod, nprod);
source_list_free(tests, ntests);
*out_files = NULL;
return -2;
}
if (has_test > 0) {
fprintf(stderr,
"ww: %s: @test declaration outside *_test.ww\n",
path);
source_list_free(names, nnames);
source_list_free(prod, nprod);
source_list_free(tests, ntests);
*out_files = NULL;
return -2;
}
if (is_test) {
char *package = NULL;
if (source_package_name(path, &package) < 0) {
source_list_free(names, nnames);
source_list_free(prod, nprod);
source_list_free(tests, ntests);
*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(names, nnames);
source_list_free(prod, nprod);
source_list_free(tests, ntests);
*out_files = NULL;
return -2;
}
}
source_list_free(names, nnames);
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 sepfoldrange {
u32 lo, hi;
int stride, delta;
};
/* Unicode 15.0 simple-fold minima generated from Go 1.26.5's byte-pinned
* unicode tables. This is a collision key only; exact spellings remain the
* sole package, action, symbol, export, and artifact identities. */
static const struct sepfoldrange sep_fold_ranges[] = {
{ 0x000041, 0x00005a, 1, 32 },
{ 0x0000e0, 0x0000f6, 1, -32 },
{ 0x0000f8, 0x0000fe, 1, -32 },
{ 0x000101, 0x00012f, 2, -1 },
{ 0x000133, 0x000137, 2, -1 },
{ 0x00013a, 0x000148, 2, -1 },
{ 0x00014b, 0x000177, 2, -1 },
{ 0x000178, 0x000178, 1, -121 },
{ 0x00017a, 0x00017e, 2, -1 },
{ 0x00017f, 0x00017f, 1, -268 },
{ 0x000183, 0x000185, 2, -1 },
{ 0x000188, 0x000188, 1, -1 },
{ 0x00018c, 0x00018c, 1, -1 },
{ 0x000192, 0x000192, 1, -1 },
{ 0x000199, 0x000199, 1, -1 },
{ 0x0001a1, 0x0001a5, 2, -1 },
{ 0x0001a8, 0x0001a8, 1, -1 },
{ 0x0001ad, 0x0001ad, 1, -1 },
{ 0x0001b0, 0x0001b0, 1, -1 },
{ 0x0001b4, 0x0001b6, 2, -1 },
{ 0x0001b9, 0x0001b9, 1, -1 },
{ 0x0001bd, 0x0001bd, 1, -1 },
{ 0x0001c5, 0x0001c5, 1, -1 },
{ 0x0001c6, 0x0001c6, 1, -2 },
{ 0x0001c8, 0x0001c8, 1, -1 },
{ 0x0001c9, 0x0001c9, 1, -2 },
{ 0x0001cb, 0x0001cb, 1, -1 },
{ 0x0001cc, 0x0001cc, 1, -2 },
{ 0x0001ce, 0x0001dc, 2, -1 },
{ 0x0001dd, 0x0001dd, 1, -79 },
{ 0x0001df, 0x0001ef, 2, -1 },
{ 0x0001f2, 0x0001f2, 1, -1 },
{ 0x0001f3, 0x0001f3, 1, -2 },
{ 0x0001f5, 0x0001f5, 1, -1 },
{ 0x0001f6, 0x0001f6, 1, -97 },
{ 0x0001f7, 0x0001f7, 1, -56 },
{ 0x0001f9, 0x00021f, 2, -1 },
{ 0x000220, 0x000220, 1, -130 },
{ 0x000223, 0x000233, 2, -1 },
{ 0x00023c, 0x00023c, 1, -1 },
{ 0x00023d, 0x00023d, 1, -163 },
{ 0x000242, 0x000242, 1, -1 },
{ 0x000243, 0x000243, 1, -195 },
{ 0x000247, 0x00024f, 2, -1 },
{ 0x000253, 0x000253, 1, -210 },
{ 0x000254, 0x000254, 1, -206 },
{ 0x000256, 0x000257, 1, -205 },
{ 0x000259, 0x000259, 1, -202 },
{ 0x00025b, 0x00025b, 1, -203 },
{ 0x000260, 0x000260, 1, -205 },
{ 0x000263, 0x000263, 1, -207 },
{ 0x000268, 0x000268, 1, -209 },
{ 0x000269, 0x000269, 1, -211 },
{ 0x00026f, 0x00026f, 1, -211 },
{ 0x000272, 0x000272, 1, -213 },
{ 0x000275, 0x000275, 1, -214 },
{ 0x000280, 0x000280, 1, -218 },
{ 0x000283, 0x000283, 1, -218 },
{ 0x000288, 0x000288, 1, -218 },
{ 0x000289, 0x000289, 1, -69 },
{ 0x00028a, 0x00028b, 1, -217 },
{ 0x00028c, 0x00028c, 1, -71 },
{ 0x000292, 0x000292, 1, -219 },
{ 0x000371, 0x000373, 2, -1 },
{ 0x000377, 0x000377, 1, -1 },
{ 0x000399, 0x000399, 1, -84 },
{ 0x00039c, 0x00039c, 1, -743 },
{ 0x0003ac, 0x0003ac, 1, -38 },
{ 0x0003ad, 0x0003af, 1, -37 },
{ 0x0003b1, 0x0003b8, 1, -32 },
{ 0x0003b9, 0x0003b9, 1, -116 },
{ 0x0003ba, 0x0003bb, 1, -32 },
{ 0x0003bc, 0x0003bc, 1, -775 },
{ 0x0003bd, 0x0003c1, 1, -32 },
{ 0x0003c2, 0x0003c2, 1, -31 },
{ 0x0003c3, 0x0003cb, 1, -32 },
{ 0x0003cc, 0x0003cc, 1, -64 },
{ 0x0003cd, 0x0003ce, 1, -63 },
{ 0x0003d0, 0x0003d0, 1, -62 },
{ 0x0003d1, 0x0003d1, 1, -57 },
{ 0x0003d5, 0x0003d5, 1, -47 },
{ 0x0003d6, 0x0003d6, 1, -54 },
{ 0x0003d7, 0x0003d7, 1, -8 },
{ 0x0003d9, 0x0003ef, 2, -1 },
{ 0x0003f0, 0x0003f0, 1, -86 },
{ 0x0003f1, 0x0003f1, 1, -80 },
{ 0x0003f3, 0x0003f3, 1, -116 },
{ 0x0003f4, 0x0003f4, 1, -92 },
{ 0x0003f5, 0x0003f5, 1, -96 },
{ 0x0003f8, 0x0003f8, 1, -1 },
{ 0x0003f9, 0x0003f9, 1, -7 },
{ 0x0003fb, 0x0003fb, 1, -1 },
{ 0x0003fd, 0x0003ff, 1, -130 },
{ 0x000430, 0x00044f, 1, -32 },
{ 0x000450, 0x00045f, 1, -80 },
{ 0x000461, 0x000481, 2, -1 },
{ 0x00048b, 0x0004bf, 2, -1 },
{ 0x0004c2, 0x0004ce, 2, -1 },
{ 0x0004cf, 0x0004cf, 1, -15 },
{ 0x0004d1, 0x00052f, 2, -1 },
{ 0x000561, 0x000586, 1, -48 },
{ 0x0013f8, 0x0013fd, 1, -8 },
{ 0x001c80, 0x001c80, 1, -6254 },
{ 0x001c81, 0x001c81, 1, -6253 },
{ 0x001c82, 0x001c82, 1, -6244 },
{ 0x001c83, 0x001c84, 1, -6242 },
{ 0x001c85, 0x001c85, 1, -6243 },
{ 0x001c86, 0x001c86, 1, -6236 },
{ 0x001c87, 0x001c87, 1, -6181 },
{ 0x001c90, 0x001cba, 1, -3008 },
{ 0x001cbd, 0x001cbf, 1, -3008 },
{ 0x001e01, 0x001e95, 2, -1 },
{ 0x001e9b, 0x001e9b, 1, -59 },
{ 0x001e9e, 0x001e9e, 1, -7615 },
{ 0x001ea1, 0x001eff, 2, -1 },
{ 0x001f08, 0x001f0f, 1, -8 },
{ 0x001f18, 0x001f1d, 1, -8 },
{ 0x001f28, 0x001f2f, 1, -8 },
{ 0x001f38, 0x001f3f, 1, -8 },
{ 0x001f48, 0x001f4d, 1, -8 },
{ 0x001f59, 0x001f5f, 2, -8 },
{ 0x001f68, 0x001f6f, 1, -8 },
{ 0x001f88, 0x001f8f, 1, -8 },
{ 0x001f98, 0x001f9f, 1, -8 },
{ 0x001fa8, 0x001faf, 1, -8 },
{ 0x001fb8, 0x001fb9, 1, -8 },
{ 0x001fba, 0x001fbb, 1, -74 },
{ 0x001fbc, 0x001fbc, 1, -9 },
{ 0x001fbe, 0x001fbe, 1, -7289 },
{ 0x001fc8, 0x001fcb, 1, -86 },
{ 0x001fcc, 0x001fcc, 1, -9 },
{ 0x001fd8, 0x001fd9, 1, -8 },
{ 0x001fda, 0x001fdb, 1, -100 },
{ 0x001fe8, 0x001fe9, 1, -8 },
{ 0x001fea, 0x001feb, 1, -112 },
{ 0x001fec, 0x001fec, 1, -7 },
{ 0x001ff8, 0x001ff9, 1, -128 },
{ 0x001ffa, 0x001ffb, 1, -126 },
{ 0x001ffc, 0x001ffc, 1, -9 },
{ 0x002126, 0x002126, 1, -7549 },
{ 0x00212a, 0x00212a, 1, -8383 },
{ 0x00212b, 0x00212b, 1, -8294 },
{ 0x00214e, 0x00214e, 1, -28 },
{ 0x002170, 0x00217f, 1, -16 },
{ 0x002184, 0x002184, 1, -1 },
{ 0x0024d0, 0x0024e9, 1, -26 },
{ 0x002c30, 0x002c5f, 1, -48 },
{ 0x002c61, 0x002c61, 1, -1 },
{ 0x002c62, 0x002c62, 1, -10743 },
{ 0x002c63, 0x002c63, 1, -3814 },
{ 0x002c64, 0x002c64, 1, -10727 },
{ 0x002c65, 0x002c65, 1, -10795 },
{ 0x002c66, 0x002c66, 1, -10792 },
{ 0x002c68, 0x002c6c, 2, -1 },
{ 0x002c6d, 0x002c6d, 1, -10780 },
{ 0x002c6e, 0x002c6e, 1, -10749 },
{ 0x002c6f, 0x002c6f, 1, -10783 },
{ 0x002c70, 0x002c70, 1, -10782 },
{ 0x002c73, 0x002c73, 1, -1 },
{ 0x002c76, 0x002c76, 1, -1 },
{ 0x002c7e, 0x002c7f, 1, -10815 },
{ 0x002c81, 0x002ce3, 2, -1 },
{ 0x002cec, 0x002cee, 2, -1 },
{ 0x002cf3, 0x002cf3, 1, -1 },
{ 0x002d00, 0x002d25, 1, -7264 },
{ 0x002d27, 0x002d27, 1, -7264 },
{ 0x002d2d, 0x002d2d, 1, -7264 },
{ 0x00a641, 0x00a649, 2, -1 },
{ 0x00a64a, 0x00a64a, 1, -35266 },
{ 0x00a64b, 0x00a64b, 1, -35267 },
{ 0x00a64d, 0x00a66d, 2, -1 },
{ 0x00a681, 0x00a69b, 2, -1 },
{ 0x00a723, 0x00a72f, 2, -1 },
{ 0x00a733, 0x00a76f, 2, -1 },
{ 0x00a77a, 0x00a77c, 2, -1 },
{ 0x00a77d, 0x00a77d, 1, -35332 },
{ 0x00a77f, 0x00a787, 2, -1 },
{ 0x00a78c, 0x00a78c, 1, -1 },
{ 0x00a78d, 0x00a78d, 1, -42280 },
{ 0x00a791, 0x00a793, 2, -1 },
{ 0x00a797, 0x00a7a9, 2, -1 },
{ 0x00a7aa, 0x00a7aa, 1, -42308 },
{ 0x00a7ab, 0x00a7ab, 1, -42319 },
{ 0x00a7ac, 0x00a7ac, 1, -42315 },
{ 0x00a7ad, 0x00a7ad, 1, -42305 },
{ 0x00a7ae, 0x00a7ae, 1, -42308 },
{ 0x00a7b0, 0x00a7b0, 1, -42258 },
{ 0x00a7b1, 0x00a7b1, 1, -42282 },
{ 0x00a7b2, 0x00a7b2, 1, -42261 },
{ 0x00a7b5, 0x00a7c3, 2, -1 },
{ 0x00a7c4, 0x00a7c4, 1, -48 },
{ 0x00a7c5, 0x00a7c5, 1, -42307 },
{ 0x00a7c6, 0x00a7c6, 1, -35384 },
{ 0x00a7c8, 0x00a7ca, 2, -1 },
{ 0x00a7d1, 0x00a7d1, 1, -1 },
{ 0x00a7d7, 0x00a7d9, 2, -1 },
{ 0x00a7f6, 0x00a7f6, 1, -1 },
{ 0x00ab53, 0x00ab53, 1, -928 },
{ 0x00ab70, 0x00abbf, 1, -38864 },
{ 0x00ff41, 0x00ff5a, 1, -32 },
{ 0x010428, 0x01044f, 1, -40 },
{ 0x0104d8, 0x0104fb, 1, -40 },
{ 0x010597, 0x0105a1, 1, -39 },
{ 0x0105a3, 0x0105b1, 1, -39 },
{ 0x0105b3, 0x0105b9, 1, -39 },
{ 0x0105bb, 0x0105bc, 1, -39 },
{ 0x010cc0, 0x010cf2, 1, -64 },
{ 0x0118c0, 0x0118df, 1, -32 },
{ 0x016e60, 0x016e7f, 1, -32 },
{ 0x01e922, 0x01e943, 1, -34 },
};
/* Go 1.26.5 strconv.IsPrint tables keep %q-style collision diagnostics
* byte-stable for printable Unicode and arbitrary filesystem bytes. */
static const u16 sep_print16[424] = {
0x0020, 0x007e, 0x00a1, 0x0377, 0x037a, 0x037f, 0x0384, 0x0556,
0x0559, 0x058a, 0x058d, 0x05c7, 0x05d0, 0x05ea, 0x05ef, 0x05f4,
0x0606, 0x070d, 0x0710, 0x074a, 0x074d, 0x07b1, 0x07c0, 0x07fa,
0x07fd, 0x082d, 0x0830, 0x085b, 0x085e, 0x086a, 0x0870, 0x088e,
0x0898, 0x098c, 0x098f, 0x0990, 0x0993, 0x09b2, 0x09b6, 0x09b9,
0x09bc, 0x09c4, 0x09c7, 0x09c8, 0x09cb, 0x09ce, 0x09d7, 0x09d7,
0x09dc, 0x09e3, 0x09e6, 0x09fe, 0x0a01, 0x0a0a, 0x0a0f, 0x0a10,
0x0a13, 0x0a39, 0x0a3c, 0x0a42, 0x0a47, 0x0a48, 0x0a4b, 0x0a4d,
0x0a51, 0x0a51, 0x0a59, 0x0a5e, 0x0a66, 0x0a76, 0x0a81, 0x0ab9,
0x0abc, 0x0acd, 0x0ad0, 0x0ad0, 0x0ae0, 0x0ae3, 0x0ae6, 0x0af1,
0x0af9, 0x0b0c, 0x0b0f, 0x0b10, 0x0b13, 0x0b39, 0x0b3c, 0x0b44,
0x0b47, 0x0b48, 0x0b4b, 0x0b4d, 0x0b55, 0x0b57, 0x0b5c, 0x0b63,
0x0b66, 0x0b77, 0x0b82, 0x0b8a, 0x0b8e, 0x0b95, 0x0b99, 0x0b9f,
0x0ba3, 0x0ba4, 0x0ba8, 0x0baa, 0x0bae, 0x0bb9, 0x0bbe, 0x0bc2,
0x0bc6, 0x0bcd, 0x0bd0, 0x0bd0, 0x0bd7, 0x0bd7, 0x0be6, 0x0bfa,
0x0c00, 0x0c39, 0x0c3c, 0x0c4d, 0x0c55, 0x0c5a, 0x0c5d, 0x0c5d,
0x0c60, 0x0c63, 0x0c66, 0x0c6f, 0x0c77, 0x0cb9, 0x0cbc, 0x0ccd,
0x0cd5, 0x0cd6, 0x0cdd, 0x0ce3, 0x0ce6, 0x0cf3, 0x0d00, 0x0d4f,
0x0d54, 0x0d63, 0x0d66, 0x0d96, 0x0d9a, 0x0dbd, 0x0dc0, 0x0dc6,
0x0dca, 0x0dca, 0x0dcf, 0x0ddf, 0x0de6, 0x0def, 0x0df2, 0x0df4,
0x0e01, 0x0e3a, 0x0e3f, 0x0e5b, 0x0e81, 0x0ebd, 0x0ec0, 0x0ed9,
0x0edc, 0x0edf, 0x0f00, 0x0f6c, 0x0f71, 0x0fda, 0x1000, 0x10c7,
0x10cd, 0x10cd, 0x10d0, 0x124d, 0x1250, 0x125d, 0x1260, 0x128d,
0x1290, 0x12b5, 0x12b8, 0x12c5, 0x12c8, 0x1315, 0x1318, 0x135a,
0x135d, 0x137c, 0x1380, 0x1399, 0x13a0, 0x13f5, 0x13f8, 0x13fd,
0x1400, 0x169c, 0x16a0, 0x16f8, 0x1700, 0x1715, 0x171f, 0x1736,
0x1740, 0x1753, 0x1760, 0x1773, 0x1780, 0x17dd, 0x17e0, 0x17e9,
0x17f0, 0x17f9, 0x1800, 0x1819, 0x1820, 0x1878, 0x1880, 0x18aa,
0x18b0, 0x18f5, 0x1900, 0x192b, 0x1930, 0x193b, 0x1940, 0x1940,
0x1944, 0x196d, 0x1970, 0x1974, 0x1980, 0x19ab, 0x19b0, 0x19c9,
0x19d0, 0x19da, 0x19de, 0x1a1b, 0x1a1e, 0x1a7c, 0x1a7f, 0x1a89,
0x1a90, 0x1a99, 0x1aa0, 0x1aad, 0x1ab0, 0x1ace, 0x1b00, 0x1b4c,
0x1b50, 0x1bf3, 0x1bfc, 0x1c37, 0x1c3b, 0x1c49, 0x1c4d, 0x1c88,
0x1c90, 0x1cba, 0x1cbd, 0x1cc7, 0x1cd0, 0x1cfa, 0x1d00, 0x1f15,
0x1f18, 0x1f1d, 0x1f20, 0x1f45, 0x1f48, 0x1f4d, 0x1f50, 0x1f7d,
0x1f80, 0x1fd3, 0x1fd6, 0x1fef, 0x1ff2, 0x1ffe, 0x2010, 0x2027,
0x2030, 0x205e, 0x2070, 0x2071, 0x2074, 0x209c, 0x20a0, 0x20c0,
0x20d0, 0x20f0, 0x2100, 0x218b, 0x2190, 0x2426, 0x2440, 0x244a,
0x2460, 0x2b73, 0x2b76, 0x2cf3, 0x2cf9, 0x2d27, 0x2d2d, 0x2d2d,
0x2d30, 0x2d67, 0x2d6f, 0x2d70, 0x2d7f, 0x2d96, 0x2da0, 0x2e5d,
0x2e80, 0x2ef3, 0x2f00, 0x2fd5, 0x2ff0, 0x2ffb, 0x3001, 0x3096,
0x3099, 0x30ff, 0x3105, 0x31e3, 0x31f0, 0xa48c, 0xa490, 0xa4c6,
0xa4d0, 0xa62b, 0xa640, 0xa6f7, 0xa700, 0xa7ca, 0xa7d0, 0xa7d9,
0xa7f2, 0xa82c, 0xa830, 0xa839, 0xa840, 0xa877, 0xa880, 0xa8c5,
0xa8ce, 0xa8d9, 0xa8e0, 0xa953, 0xa95f, 0xa97c, 0xa980, 0xa9d9,
0xa9de, 0xaa36, 0xaa40, 0xaa4d, 0xaa50, 0xaa59, 0xaa5c, 0xaac2,
0xaadb, 0xaaf6, 0xab01, 0xab06, 0xab09, 0xab0e, 0xab11, 0xab16,
0xab20, 0xab6b, 0xab70, 0xabed, 0xabf0, 0xabf9, 0xac00, 0xd7a3,
0xd7b0, 0xd7c6, 0xd7cb, 0xd7fb, 0xf900, 0xfa6d, 0xfa70, 0xfad9,
0xfb00, 0xfb06, 0xfb13, 0xfb17, 0xfb1d, 0xfbc2, 0xfbd3, 0xfd8f,
0xfd92, 0xfdc7, 0xfdcf, 0xfdcf, 0xfdf0, 0xfe19, 0xfe20, 0xfe6b,
0xfe70, 0xfefc, 0xff01, 0xffbe, 0xffc2, 0xffc7, 0xffca, 0xffcf,
0xffd2, 0xffd7, 0xffda, 0xffdc, 0xffe0, 0xffee, 0xfffc, 0xfffd,
};
static const u16 sep_not_print16[133] = {
0x00ad, 0x038b, 0x038d, 0x03a2, 0x0530, 0x0590, 0x061c, 0x06dd,
0x083f, 0x085f, 0x08e2, 0x0984, 0x09a9, 0x09b1, 0x09de, 0x0a04,
0x0a29, 0x0a31, 0x0a34, 0x0a37, 0x0a3d, 0x0a5d, 0x0a84, 0x0a8e,
0x0a92, 0x0aa9, 0x0ab1, 0x0ab4, 0x0ac6, 0x0aca, 0x0b00, 0x0b04,
0x0b29, 0x0b31, 0x0b34, 0x0b5e, 0x0b84, 0x0b91, 0x0b9b, 0x0b9d,
0x0bc9, 0x0c0d, 0x0c11, 0x0c29, 0x0c45, 0x0c49, 0x0c57, 0x0c8d,
0x0c91, 0x0ca9, 0x0cb4, 0x0cc5, 0x0cc9, 0x0cdf, 0x0cf0, 0x0d0d,
0x0d11, 0x0d45, 0x0d49, 0x0d80, 0x0d84, 0x0db2, 0x0dbc, 0x0dd5,
0x0dd7, 0x0e83, 0x0e85, 0x0e8b, 0x0ea4, 0x0ea6, 0x0ec5, 0x0ec7,
0x0ecf, 0x0f48, 0x0f98, 0x0fbd, 0x0fcd, 0x10c6, 0x1249, 0x1257,
0x1259, 0x1289, 0x12b1, 0x12bf, 0x12c1, 0x12d7, 0x1311, 0x1680,
0x176d, 0x1771, 0x180e, 0x191f, 0x1a5f, 0x1b7f, 0x1f58, 0x1f5a,
0x1f5c, 0x1f5e, 0x1fb5, 0x1fc5, 0x1fdc, 0x1ff5, 0x208f, 0x2b96,
0x2d26, 0x2da7, 0x2daf, 0x2db7, 0x2dbf, 0x2dc7, 0x2dcf, 0x2dd7,
0x2ddf, 0x2e9a, 0x3040, 0x3130, 0x318f, 0x321f, 0xa7d2, 0xa7d4,
0xa9ce, 0xa9ff, 0xab27, 0xab2f, 0xfb37, 0xfb3d, 0xfb3f, 0xfb42,
0xfb45, 0xfe53, 0xfe67, 0xfe75, 0xffe7,
};
static const u32 sep_print32[508] = {
0x010000, 0x01004d, 0x010050, 0x01005d, 0x010080, 0x0100fa, 0x010100, 0x010102,
0x010107, 0x010133, 0x010137, 0x01019c, 0x0101a0, 0x0101a0, 0x0101d0, 0x0101fd,
0x010280, 0x01029c, 0x0102a0, 0x0102d0, 0x0102e0, 0x0102fb, 0x010300, 0x010323,
0x01032d, 0x01034a, 0x010350, 0x01037a, 0x010380, 0x0103c3, 0x0103c8, 0x0103d5,
0x010400, 0x01049d, 0x0104a0, 0x0104a9, 0x0104b0, 0x0104d3, 0x0104d8, 0x0104fb,
0x010500, 0x010527, 0x010530, 0x010563, 0x01056f, 0x0105bc, 0x010600, 0x010736,
0x010740, 0x010755, 0x010760, 0x010767, 0x010780, 0x0107ba, 0x010800, 0x010805,
0x010808, 0x010838, 0x01083c, 0x01083c, 0x01083f, 0x01089e, 0x0108a7, 0x0108af,
0x0108e0, 0x0108f5, 0x0108fb, 0x01091b, 0x01091f, 0x010939, 0x01093f, 0x01093f,
0x010980, 0x0109b7, 0x0109bc, 0x0109cf, 0x0109d2, 0x010a06, 0x010a0c, 0x010a35,
0x010a38, 0x010a3a, 0x010a3f, 0x010a48, 0x010a50, 0x010a58, 0x010a60, 0x010a9f,
0x010ac0, 0x010ae6, 0x010aeb, 0x010af6, 0x010b00, 0x010b35, 0x010b39, 0x010b55,
0x010b58, 0x010b72, 0x010b78, 0x010b91, 0x010b99, 0x010b9c, 0x010ba9, 0x010baf,
0x010c00, 0x010c48, 0x010c80, 0x010cb2, 0x010cc0, 0x010cf2, 0x010cfa, 0x010d27,
0x010d30, 0x010d39, 0x010e60, 0x010ead, 0x010eb0, 0x010eb1, 0x010efd, 0x010f27,
0x010f30, 0x010f59, 0x010f70, 0x010f89, 0x010fb0, 0x010fcb, 0x010fe0, 0x010ff6,
0x011000, 0x01104d, 0x011052, 0x011075, 0x01107f, 0x0110c2, 0x0110d0, 0x0110e8,
0x0110f0, 0x0110f9, 0x011100, 0x011147, 0x011150, 0x011176, 0x011180, 0x0111f4,
0x011200, 0x011241, 0x011280, 0x0112a9, 0x0112b0, 0x0112ea, 0x0112f0, 0x0112f9,
0x011300, 0x01130c, 0x01130f, 0x011310, 0x011313, 0x011344, 0x011347, 0x011348,
0x01134b, 0x01134d, 0x011350, 0x011350, 0x011357, 0x011357, 0x01135d, 0x011363,
0x011366, 0x01136c, 0x011370, 0x011374, 0x011400, 0x011461, 0x011480, 0x0114c7,
0x0114d0, 0x0114d9, 0x011580, 0x0115b5, 0x0115b8, 0x0115dd, 0x011600, 0x011644,
0x011650, 0x011659, 0x011660, 0x01166c, 0x011680, 0x0116b9, 0x0116c0, 0x0116c9,
0x011700, 0x01171a, 0x01171d, 0x01172b, 0x011730, 0x011746, 0x011800, 0x01183b,
0x0118a0, 0x0118f2, 0x0118ff, 0x011906, 0x011909, 0x011909, 0x01190c, 0x011938,
0x01193b, 0x011946, 0x011950, 0x011959, 0x0119a0, 0x0119a7, 0x0119aa, 0x0119d7,
0x0119da, 0x0119e4, 0x011a00, 0x011a47, 0x011a50, 0x011aa2, 0x011ab0, 0x011af8,
0x011b00, 0x011b09, 0x011c00, 0x011c45, 0x011c50, 0x011c6c, 0x011c70, 0x011c8f,
0x011c92, 0x011cb6, 0x011d00, 0x011d36, 0x011d3a, 0x011d47, 0x011d50, 0x011d59,
0x011d60, 0x011d98, 0x011da0, 0x011da9, 0x011ee0, 0x011ef8, 0x011f00, 0x011f3a,
0x011f3e, 0x011f59, 0x011fb0, 0x011fb0, 0x011fc0, 0x011ff1, 0x011fff, 0x012399,
0x012400, 0x012474, 0x012480, 0x012543, 0x012f90, 0x012ff2, 0x013000, 0x01342f,
0x013440, 0x013455, 0x014400, 0x014646, 0x016800, 0x016a38, 0x016a40, 0x016a69,
0x016a6e, 0x016ac9, 0x016ad0, 0x016aed, 0x016af0, 0x016af5, 0x016b00, 0x016b45,
0x016b50, 0x016b77, 0x016b7d, 0x016b8f, 0x016e40, 0x016e9a, 0x016f00, 0x016f4a,
0x016f4f, 0x016f87, 0x016f8f, 0x016f9f, 0x016fe0, 0x016fe4, 0x016ff0, 0x016ff1,
0x017000, 0x0187f7, 0x018800, 0x018cd5, 0x018d00, 0x018d08, 0x01aff0, 0x01b122,
0x01b132, 0x01b132, 0x01b150, 0x01b152, 0x01b155, 0x01b155, 0x01b164, 0x01b167,
0x01b170, 0x01b2fb, 0x01bc00, 0x01bc6a, 0x01bc70, 0x01bc7c, 0x01bc80, 0x01bc88,
0x01bc90, 0x01bc99, 0x01bc9c, 0x01bc9f, 0x01cf00, 0x01cf2d, 0x01cf30, 0x01cf46,
0x01cf50, 0x01cfc3, 0x01d000, 0x01d0f5, 0x01d100, 0x01d126, 0x01d129, 0x01d172,
0x01d17b, 0x01d1ea, 0x01d200, 0x01d245, 0x01d2c0, 0x01d2d3, 0x01d2e0, 0x01d2f3,
0x01d300, 0x01d356, 0x01d360, 0x01d378, 0x01d400, 0x01d49f, 0x01d4a2, 0x01d4a2,
0x01d4a5, 0x01d4a6, 0x01d4a9, 0x01d50a, 0x01d50d, 0x01d546, 0x01d54a, 0x01d6a5,
0x01d6a8, 0x01d7cb, 0x01d7ce, 0x01da8b, 0x01da9b, 0x01daaf, 0x01df00, 0x01df1e,
0x01df25, 0x01df2a, 0x01e000, 0x01e018, 0x01e01b, 0x01e02a, 0x01e030, 0x01e06d,
0x01e08f, 0x01e08f, 0x01e100, 0x01e12c, 0x01e130, 0x01e13d, 0x01e140, 0x01e149,
0x01e14e, 0x01e14f, 0x01e290, 0x01e2ae, 0x01e2c0, 0x01e2f9, 0x01e2ff, 0x01e2ff,
0x01e4d0, 0x01e4f9, 0x01e7e0, 0x01e8c4, 0x01e8c7, 0x01e8d6, 0x01e900, 0x01e94b,
0x01e950, 0x01e959, 0x01e95e, 0x01e95f, 0x01ec71, 0x01ecb4, 0x01ed01, 0x01ed3d,
0x01ee00, 0x01ee24, 0x01ee27, 0x01ee3b, 0x01ee42, 0x01ee42, 0x01ee47, 0x01ee54,
0x01ee57, 0x01ee64, 0x01ee67, 0x01ee9b, 0x01eea1, 0x01eebb, 0x01eef0, 0x01eef1,
0x01f000, 0x01f02b, 0x01f030, 0x01f093, 0x01f0a0, 0x01f0ae, 0x01f0b1, 0x01f0f5,
0x01f100, 0x01f1ad, 0x01f1e6, 0x01f202, 0x01f210, 0x01f23b, 0x01f240, 0x01f248,
0x01f250, 0x01f251, 0x01f260, 0x01f265, 0x01f300, 0x01f6d7, 0x01f6dc, 0x01f6ec,
0x01f6f0, 0x01f6fc, 0x01f700, 0x01f776, 0x01f77b, 0x01f7d9, 0x01f7e0, 0x01f7eb,
0x01f7f0, 0x01f7f0, 0x01f800, 0x01f80b, 0x01f810, 0x01f847, 0x01f850, 0x01f859,
0x01f860, 0x01f887, 0x01f890, 0x01f8ad, 0x01f8b0, 0x01f8b1, 0x01f900, 0x01fa53,
0x01fa60, 0x01fa6d, 0x01fa70, 0x01fa7c, 0x01fa80, 0x01fa88, 0x01fa90, 0x01fac5,
0x01face, 0x01fadb, 0x01fae0, 0x01fae8, 0x01faf0, 0x01faf8, 0x01fb00, 0x01fbca,
0x01fbf0, 0x01fbf9, 0x020000, 0x02a6df, 0x02a700, 0x02b739, 0x02b740, 0x02b81d,
0x02b820, 0x02cea1, 0x02ceb0, 0x02ebe0, 0x02f800, 0x02fa1d, 0x030000, 0x03134a,
0x031350, 0x0323af, 0x0e0100, 0x0e01ef,
};
static const u16 sep_not_print32[112] = {
0x000c, 0x0027, 0x003b, 0x003e, 0x018f, 0x039e, 0x057b,
0x058b, 0x0593, 0x0596, 0x05a2, 0x05b2, 0x05ba, 0x0786, 0x07b1,
0x0809, 0x0836, 0x0856, 0x08f3, 0x0a04, 0x0a14, 0x0a18, 0x0e7f,
0x0eaa, 0x10bd, 0x1135, 0x11e0, 0x1212, 0x1287, 0x1289, 0x128e,
0x129e, 0x1304, 0x1329, 0x1331, 0x1334, 0x133a, 0x145c, 0x1914,
0x1917, 0x1936, 0x1c09, 0x1c37, 0x1ca8, 0x1d07, 0x1d0a, 0x1d3b,
0x1d3e, 0x1d66, 0x1d69, 0x1d8f, 0x1d92, 0x1f11, 0x246f, 0x6a5f,
0x6abf, 0x6b5a, 0x6b62, 0xaff4, 0xaffc, 0xafff, 0xd455, 0xd49d,
0xd4ad, 0xd4ba, 0xd4bc, 0xd4c4, 0xd506, 0xd515, 0xd51d, 0xd53a,
0xd53f, 0xd545, 0xd551, 0xdaa0, 0xe007, 0xe022, 0xe025, 0xe7e7,
0xe7ec, 0xe7ef, 0xe7ff, 0xee04, 0xee20, 0xee23, 0xee28, 0xee33,
0xee38, 0xee3a, 0xee48, 0xee4a, 0xee4c, 0xee50, 0xee53, 0xee58,
0xee5a, 0xee5c, 0xee5e, 0xee60, 0xee63, 0xee6b, 0xee73, 0xee78,
0xee7d, 0xee7f, 0xee8a, 0xeea4, 0xeeaa, 0xf0c0, 0xf0d0, 0xfabe,
0xfb93,
};
struct sepfoldentry {
char *scope; /* canonical directory; NULL for package identities */
char *key; /* request-only Go simple-fold key */
char *exact; /* exact canonical identity or selected basename */
};
struct sepfoldset {
struct sepfoldentry *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 *for_test; /* directory product owning a recompiled action */
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_targets; /* sorted ptest/pxtest target actions */
int ngenerated_targets;
int generated_targetcap;
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 sepfoldset package_folds;
struct sepfoldset file_folds;
};
struct sepproduct {
const char *dir;
const char *out;
const char *identity; /* explicit canonical lookup identity, if any */
const char *test_package;
const char *production_package;
const char *internal_package;
const char *external_package;
const char *status;
const char *publish; /* optional retained test executable */
const char *artifact;
int variant;
int directory_product;
int no_tests;
int context;
int root;
int variant_root; /* retained single-unit root outside directory products */
int production_root;
int ptest;
int pxtest;
int support; /* direct generated-main support action, or -1 */
char *stage_out; /* request-private linked/published output */
char *stage_publish; /* request-private retained executable copy */
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_topo_visit(struct sepgraph *, int, int *, int *, int *, int);
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_folds(struct sepfoldset *set, int need)
{
return sep_reserve((void **)&set->v, &set->cap, need,
sizeof *set->v);
}
static int
sep_utf8_width(const char *value, size_t len, size_t index)
{
const unsigned char *s = (const unsigned char *)value;
unsigned char c = s[index];
if (c < 0x80) return 1;
if (c >= 0xc2 && c <= 0xdf && index + 1 < len
&& s[index + 1] >= 0x80 && s[index + 1] <= 0xbf)
return 2;
if (index + 2 < len && s[index + 2] >= 0x80
&& s[index + 2] <= 0xbf) {
unsigned char c1 = s[index + 1];
if ((c == 0xe0 && c1 >= 0xa0 && c1 <= 0xbf)
|| (c >= 0xe1 && c <= 0xec && c1 >= 0x80 && c1 <= 0xbf)
|| (c == 0xed && c1 >= 0x80 && c1 <= 0x9f)
|| (c >= 0xee && c <= 0xef && c1 >= 0x80 && c1 <= 0xbf))
return 3;
}
if (index + 3 < len && s[index + 2] >= 0x80
&& s[index + 2] <= 0xbf && s[index + 3] >= 0x80
&& s[index + 3] <= 0xbf) {
unsigned char c1 = s[index + 1];
if ((c == 0xf0 && c1 >= 0x90 && c1 <= 0xbf)
|| (c >= 0xf1 && c <= 0xf3 && c1 >= 0x80 && c1 <= 0xbf)
|| (c == 0xf4 && c1 >= 0x80 && c1 <= 0x8f))
return 4;
}
return 1;
}
static u32
sep_utf8_rune(const char *value, size_t index, int width)
{
const unsigned char *s = (const unsigned char *)value;
u32 c0 = s[index];
if (width == 1) return c0 >= 0x80 ? 0xfffd : c0;
u32 c1 = s[index + 1];
if (width == 2) return ((c0 & 0x1f) << 6) | (c1 & 0x3f);
u32 c2 = s[index + 2];
if (width == 3)
return ((c0 & 0x0f) << 12) | ((c1 & 0x3f) << 6)
| (c2 & 0x3f);
u32 c3 = s[index + 3];
return ((c0 & 0x07) << 18) | ((c1 & 0x3f) << 12)
| ((c2 & 0x3f) << 6) | (c3 & 0x3f);
}
static u32
sep_fold_rune(u32 r)
{
int lo = 0;
int hi = (int)(sizeof sep_fold_ranges / sizeof sep_fold_ranges[0]);
while (lo < hi) {
int middle = lo + (hi - lo) / 2;
if (sep_fold_ranges[middle].lo <= r) lo = middle + 1;
else hi = middle;
}
if (lo > 0) {
const struct sepfoldrange *range = &sep_fold_ranges[lo - 1];
if (r <= range->hi && (r - range->lo) % (u32)range->stride == 0)
return (u32)((long)r + range->delta);
}
return r;
}
static int
sep_utf8_encoded_width(u32 r)
{
if (r <= 0x7f) return 1;
if (r <= 0x7ff) return 2;
if (r <= 0xffff) return 3;
return 4;
}
static size_t
sep_utf8_encode(char *out, size_t offset, u32 r)
{
if (r <= 0x7f) {
out[offset++] = (char)r;
} else if (r <= 0x7ff) {
out[offset++] = (char)(0xc0 | (r >> 6));
out[offset++] = (char)(0x80 | (r & 0x3f));
} else if (r <= 0xffff) {
out[offset++] = (char)(0xe0 | (r >> 12));
out[offset++] = (char)(0x80 | ((r >> 6) & 0x3f));
out[offset++] = (char)(0x80 | (r & 0x3f));
} else {
out[offset++] = (char)(0xf0 | (r >> 18));
out[offset++] = (char)(0x80 | ((r >> 12) & 0x3f));
out[offset++] = (char)(0x80 | ((r >> 6) & 0x3f));
out[offset++] = (char)(0x80 | (r & 0x3f));
}
return offset;
}
static char *
sep_to_fold(const char *value)
{
size_t len = strlen(value);
size_t total = 0;
for (size_t i = 0; i < len;) {
int width = sep_utf8_width(value, len, i);
u32 r = sep_fold_rune(sep_utf8_rune(value, i, width));
int encoded = sep_utf8_encoded_width(r);
if (total > (size_t)-1 - (size_t)encoded - 1) {
sep_fail_size();
return NULL;
}
total += (size_t)encoded;
i += (size_t)width;
}
char *out = malloc(total + 1);
if (out == NULL) {
sep_fail_nomem();
return NULL;
}
size_t offset = 0;
for (size_t i = 0; i < len;) {
int width = sep_utf8_width(value, len, i);
u32 r = sep_fold_rune(sep_utf8_rune(value, i, width));
offset = sep_utf8_encode(out, offset, r);
i += (size_t)width;
}
out[offset] = '\0';
return out;
}
static int
sep_bsearch16(const u16 *values, int n, u16 target)
{
int lo = 0, hi = n;
while (lo < hi) {
int middle = lo + (hi - lo) / 2;
if (values[middle] < target) lo = middle + 1;
else hi = middle;
}
return lo;
}
static int
sep_bsearch32(const u32 *values, int n, u32 target)
{
int lo = 0, hi = n;
while (lo < hi) {
int middle = lo + (hi - lo) / 2;
if (values[middle] < target) lo = middle + 1;
else hi = middle;
}
return lo;
}
static int
sep_is_print(u32 r)
{
if (r <= 0xff) {
if (r >= 0x20 && r <= 0x7e) return 1;
if (r >= 0xa1) return r != 0xad;
return 0;
}
if (r < 0x10000) {
u16 rr = (u16)r;
int nprint = (int)(sizeof sep_print16 / sizeof sep_print16[0]);
int i = sep_bsearch16(sep_print16, nprint, rr);
if (i >= nprint) return 0;
int start = i;
if (start % 2 != 0) start--;
if (start + 1 >= nprint || rr < sep_print16[start]
|| sep_print16[start + 1] < rr) return 0;
int nexcluded = (int)(sizeof sep_not_print16
/ sizeof sep_not_print16[0]);
int excluded = sep_bsearch16(sep_not_print16, nexcluded, rr);
return excluded >= nexcluded || sep_not_print16[excluded] != rr;
}
int nprint = (int)(sizeof sep_print32 / sizeof sep_print32[0]);
int i = sep_bsearch32(sep_print32, nprint, r);
if (i >= nprint) return 0;
int start = i;
if (start % 2 != 0) start--;
if (start + 1 >= nprint || r < sep_print32[start]
|| sep_print32[start + 1] < r) return 0;
if (r >= 0x20000) return 1;
u16 rr = (u16)(r - 0x10000);
int nexcluded = (int)(sizeof sep_not_print32
/ sizeof sep_not_print32[0]);
int excluded = sep_bsearch16(sep_not_print32, nexcluded, rr);
return excluded >= nexcluded || sep_not_print32[excluded] != rr;
}
static void
sep_put_quoted(const char *value)
{
static const char hex[] = "0123456789abcdef";
size_t len = strlen(value);
fputc('"', stderr);
for (size_t i = 0; i < len;) {
unsigned char c = (unsigned char)value[i];
int width = sep_utf8_width(value, len, i);
if (c >= 0x80 && width == 1) {
fputs("\\x", stderr);
fputc(hex[c >> 4], stderr);
fputc(hex[c & 0x0f], stderr);
i++;
continue;
}
if (width > 1) {
u32 r = sep_utf8_rune(value, i, width);
if (sep_is_print(r)) fwrite(value + i, 1, (size_t)width, stderr);
else if (r < 0x10000) fprintf(stderr, "\\u%04x", r);
else fprintf(stderr, "\\U%08x", r);
i += (size_t)width;
continue;
}
switch (c) {
case '"': fputs("\\\"", stderr); break;
case '\\': fputs("\\\\", stderr); break;
case '\a': fputs("\\a", stderr); break;
case '\b': fputs("\\b", stderr); break;
case '\f': fputs("\\f", stderr); break;
case '\n': fputs("\\n", stderr); break;
case '\r': fputs("\\r", stderr); break;
case '\t': fputs("\\t", stderr); break;
case '\v': fputs("\\v", stderr); break;
default:
if (c < 0x20 || c == 0x7f) {
fputs("\\x", stderr);
fputc(hex[c >> 4], stderr);
fputc(hex[c & 0x0f], stderr);
} else fputc(c, stderr);
}
i++;
}
fputc('"', stderr);
}
static void
sep_diag_fold_collision(const char *kind, const char *left, const char *right)
{
if (strcmp(left, right) > 0) {
const char *swap = left;
left = right;
right = swap;
}
fprintf(stderr, "ww: case-insensitive %s collision: ", kind);
sep_put_quoted(left);
fputs(" and ", stderr);
sep_put_quoted(right);
fputc('\n', stderr);
}
static int
sep_register_package_fold(struct sepgraph *g, const char *exact)
{
char *key = sep_to_fold(exact);
if (key == NULL) return -1;
struct sepfoldset *set = &g->package_folds;
int lo = 0, hi = set->n;
while (lo < hi) {
int middle = lo + (hi - lo) / 2;
int cmp = strcmp(set->v[middle].key, key);
if (cmp < 0) lo = middle + 1;
else hi = middle;
}
if (lo < set->n && strcmp(set->v[lo].key, key) == 0) {
free(key);
if (strcmp(set->v[lo].exact, exact) == 0) return 0;
g->identity_failed = 1;
sep_diag_fold_collision("import", set->v[lo].exact, exact);
return -1;
}
char *spelling = strdup(exact);
if (spelling == NULL) {
free(key);
return sep_fail_nomem();
}
if (set->n == INT_MAX || sep_reserve_folds(set, set->n + 1) < 0) {
if (set->n == INT_MAX) sep_fail_size();
free(key);
free(spelling);
return -1;
}
memmove(&set->v[lo + 1], &set->v[lo],
(size_t)(set->n - lo) * sizeof set->v[0]);
set->v[lo].scope = NULL;
set->v[lo].key = key;
set->v[lo].exact = spelling;
set->n++;
return 0;
}
static int
sep_file_fold_cmp(const struct sepfoldentry *entry, const char *scope,
const char *key)
{
int cmp = strcmp(entry->scope, scope);
if (cmp != 0) return cmp;
return strcmp(entry->key, key);
}
static int
sep_register_file_fold(struct sepgraph *g, const char *scope,
const char *source)
{
const char *slash = strrchr(source, '/');
const char *exact = slash == NULL ? source : slash + 1;
char *key = sep_to_fold(exact);
if (key == NULL) return -1;
struct sepfoldset *set = &g->file_folds;
int lo = 0, hi = set->n;
while (lo < hi) {
int middle = lo + (hi - lo) / 2;
int cmp = sep_file_fold_cmp(&set->v[middle], scope, key);
if (cmp < 0) lo = middle + 1;
else hi = middle;
}
if (lo < set->n && sep_file_fold_cmp(&set->v[lo], scope, key) == 0) {
free(key);
if (strcmp(set->v[lo].exact, exact) == 0) return 0;
g->identity_failed = 1;
sep_diag_fold_collision("file name", set->v[lo].exact, exact);
return -1;
}
char *owned_scope = strdup(scope);
char *spelling = strdup(exact);
if (owned_scope == NULL || spelling == NULL) {
free(key);
free(owned_scope);
free(spelling);
return sep_fail_nomem();
}
if (set->n == INT_MAX || sep_reserve_folds(set, set->n + 1) < 0) {
if (set->n == INT_MAX) sep_fail_size();
free(key);
free(owned_scope);
free(spelling);
return -1;
}
memmove(&set->v[lo + 1], &set->v[lo],
(size_t)(set->n - lo) * sizeof set->v[0]);
set->v[lo].scope = owned_scope;
set->v[lo].key = key;
set->v[lo].exact = spelling;
set->n++;
return 0;
}
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-v3:";
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_write(&s, &zero, 1);
if (p->for_test != NULL)
sep_sha256_write(&s, p->for_test, strlen(p->for_test));
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;
if (p->role != SEP_ROLE_TEST_SUPPORT
&& sep_register_package_fold(g, base) < 0) return -1;
g->identity_failed = 1;
sep_diag_directory_identities(p->entry, p->import_base, base);
return -1;
}
if (p->role != SEP_ROLE_TEST_SUPPORT
&& sep_register_package_fold(g, base) < 0) 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 : "";
if (is_dir && base[0] != '\0' && role != SEP_ROLE_TEST_SUPPORT
&& (reserved_import_path(base) || sep_import_base_valid(base))
&& sep_register_package_fold(g, base) < 0) {
free(canon);
return -1;
}
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);
free(p->for_test);
free(p->generated_targets);
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->package_folds.n; i++) {
free(g->package_folds.v[i].scope);
free(g->package_folds.v[i].key);
free(g->package_folds.v[i].exact);
}
for (int i = 0; i < g->file_folds.n; i++) {
free(g->file_folds.v[i].scope);
free(g->file_folds.v[i].key);
free(g->file_folds.v[i].exact);
}
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->package_folds.v);
free(g->file_folds.v);
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->variant == SEP_VARIANT_SAME_TEST)
&& 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;
}
/* Preserve normal resolution and legality, then bind an external
* self-import to the already-created augmented package when present. */
int di = -1;
if (external_production)
for (int candidate = 0; candidate < g->n; candidate++) {
struct seppkg *q = &g->pkg[candidate];
if (q->variant == SEP_VARIANT_SAME_TEST
&& q->role == SEP_ROLE_NORMAL
&& strcmp(q->canon, canon) == 0
&& (q->import_base != NULL
? strcmp(q->import_base,
resolved.identity) == 0
: g->pkg[pi].import_base == NULL)) {
di = candidate;
break;
}
}
if (di < 0)
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;
int rcanon = strcmp(g->pkg[a].canon, g->pkg[b].canon);
if (rcanon != 0) return rcanon;
if (g->pkg[a].for_test == NULL || g->pkg[b].for_test == NULL)
return g->pkg[a].for_test == NULL
? (g->pkg[b].for_test == NULL ? 0 : -1) : 1;
return strcmp(g->pkg[a].for_test, g->pkg[b].for_test);
}
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 one compiler-owned main for a canonical directory test product.
* The direct dependency set and the compiler target subset are kept
* separately because support is a dependency but never a test target. */
static int
sep_add_generated_main(struct sepgraph *g, struct sepproduct *product,
int ordinal, int support)
{
(void)ordinal;
int targets[2], ntargets = 0;
if (product->ptest >= 0) targets[ntargets++] = product->ptest;
if (product->pxtest >= 0) targets[ntargets++] = product->pxtest;
if (ntargets == 0) return -1;
if (ntargets == 2 && sep_dep_cmp(g, targets[0], targets[1]) > 0) {
int t = targets[0]; targets[0] = targets[1]; targets[1] = t;
}
int owner = targets[0];
const char *base = g->pkg[owner].import_base;
if (base == NULL || base[0] == '\0') return -1;
char *path = sep_sprintf("__wwtestmain.%s.main", base);
char *artifact = sep_sprintf("%s-test-main", base);
char *canon = sep_sprintf("%s#directory-test-main",
g->pkg[owner].canon);
char *entry = strdup(g->pkg[owner].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 support_is_target = 0;
for (int k = 0; k < ntargets; k++)
if (targets[k] == support) support_is_target = 1;
int wants_support = support >= 0 && !support_is_target;
int same_targets = g->pkg[i].ngenerated_targets == ntargets;
for (int k = 0; k < ntargets && same_targets; k++)
if (g->pkg[i].generated_targets[k] != targets[k])
same_targets = 0;
int has_support = 0;
for (int k = 0; k < g->pkg[i].ndeps; k++) {
if (wants_support && g->pkg[i].deps[k] == support)
has_support = 1;
}
if (same_targets && has_support == wants_support
&& g->pkg[i].ndeps == ntargets + 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->loaded = 1;
p->emit_context = product->context;
if (sep_set_context_state(p, product->context, 2) < 0)
goto fail;
for (int k = 0; k < ntargets; k++) {
if (sep_reserve((void **)&p->generated_targets,
&p->generated_targetcap, p->ngenerated_targets + 1,
sizeof *p->generated_targets) < 0)
goto fail;
p->generated_targets[p->ngenerated_targets++] = targets[k];
if (sep_add_dep(g, g->n, targets[k]) < 0) goto fail;
}
int support_is_target = 0;
for (int k = 0; k < ntargets; k++)
if (targets[k] == support) support_is_target = 1;
if (support >= 0 && !support_is_target
&& 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);
free(p->generated_targets);
memset(p, 0, sizeof *p);
return -1;
}
static int
sep_rewrite_action_deps(struct sepgraph *g, int pi, const int *replacement,
int nreplacement)
{
struct seppkg *p = &g->pkg[pi];
int *deps = NULL, ndeps = 0, depcap = 0;
for (int i = 0; i < p->ndeps; i++) {
int dep = p->deps[i];
if (dep >= 0 && dep < nreplacement) dep = replacement[dep];
int found = 0;
for (int j = 0; j < ndeps; j++)
if (deps[j] == dep) { found = 1; break; }
if (found) continue;
if (ndeps == INT_MAX
|| sep_reserve((void **)&deps, &depcap, ndeps + 1,
sizeof *deps) < 0) {
free(deps);
return -1;
}
deps[ndeps++] = dep;
}
for (int i = 1; i < ndeps; i++) {
int dep = deps[i], j = i;
while (j > 0 && sep_dep_cmp(g, deps[j - 1], dep) > 0) {
deps[j] = deps[j - 1];
j--;
}
deps[j] = dep;
}
for (int i = 0; i < p->bindings.n; i++) {
int dep = p->bindings.v[i].dep;
if (dep >= 0 && dep < nreplacement)
p->bindings.v[i].dep = replacement[dep];
}
if (p->bindings.n > 1)
qsort(p->bindings.v, (size_t)p->bindings.n,
sizeof *p->bindings.v, sep_binding_cmp);
free(p->deps);
p->deps = deps;
p->ndeps = ndeps;
p->depcap = depcap;
return 0;
}
static int
sep_clone_for_test(struct sepgraph *g, int original, const char *owner,
const int *replacement, int nreplacement)
{
if (g->n == INT_MAX || sep_reserve_packages(g, g->n + 1) < 0)
return -1;
struct seppkg *src = &g->pkg[original];
int ni = g->n++;
struct seppkg *p = &g->pkg[ni];
memset(p, 0, sizeof *p);
p->path = strdup(src->path);
p->import_base = src->import_base != NULL
? strdup(src->import_base) : NULL;
p->entry = strdup(src->entry);
p->canon = strdup(src->canon);
p->name = src->name != NULL ? strdup(src->name) : NULL;
p->test_package = src->test_package != NULL
? strdup(src->test_package) : NULL;
p->for_test = strdup(owner);
if (p->path == NULL || (src->import_base != NULL && p->import_base == NULL)
|| p->entry == NULL || p->canon == NULL
|| (src->name != NULL && p->name == NULL)
|| (src->test_package != NULL && p->test_package == NULL)
|| p->for_test == NULL) {
sep_fail_nomem();
goto fail;
}
p->is_dir = src->is_dir;
p->variant = SEP_VARIANT_TEST_COPY;
p->role = src->role;
p->loaded = src->loaded;
p->failed = src->failed;
p->test_support = src->test_support;
p->emit_context = src->emit_context;
if (src->context_cap > 0) {
p->context_state = malloc((size_t)src->context_cap);
if (p->context_state == NULL) {
sep_fail_nomem();
goto fail;
}
memcpy(p->context_state, src->context_state,
(size_t)src->context_cap);
p->context_cap = src->context_cap;
}
int sourcecap = 0;
for (int i = 0; i < src->nsources; i++) {
if (source_list_add(&p->sources, &p->nsources, &sourcecap,
src->sources[i]) < 0)
goto fail;
}
for (int i = 0; i < src->bindings.n; i++) {
struct sepbind *b = &src->bindings.v[i];
Pos pos = { b->source, b->line, b->col };
if (sep_binding_add(&p->bindings, b->kind, b->name,
b->dep, pos) < 0)
goto fail;
}
if (src->ndeps > 0) {
p->deps = malloc((size_t)src->ndeps * sizeof *p->deps);
if (p->deps == NULL) {
sep_fail_nomem();
goto fail;
}
memcpy(p->deps, src->deps, (size_t)src->ndeps * sizeof *p->deps);
p->ndeps = p->depcap = src->ndeps;
}
if (sep_rewrite_action_deps(g, ni, replacement, nreplacement) < 0)
goto fail;
return ni;
fail:
sep_pkg_free_fields(p);
g->n--;
return -1;
}
/* Go's recompileForTest is copy-on-write over one directory product. Keep
* canonical package paths unchanged while giving every rebuilt action a
* product-scoped storage identity, then rewire both graph edges and original
* source bindings before any compiler action is formed. */
static int
sep_recompile_for_test(struct sepgraph *g, struct sepproduct *product)
{
if (product->production_root < 0 || product->ptest < 0
|| product->production_root == product->ptest)
return 0;
int nbase = g->n;
int *order = calloc((size_t)nbase, sizeof *order);
int *stack = calloc((size_t)nbase, sizeof *stack);
int *replacement = malloc((size_t)nbase * sizeof *replacement);
if (order == NULL || stack == NULL || replacement == NULL) {
sep_fail_nomem();
free(replacement); free(stack); free(order);
return -1;
}
for (int i = 0; i < nbase; i++) replacement[i] = i;
replacement[product->production_root] = product->ptest;
int norder = 0;
for (int i = 0; i < nbase; i++) g->pkg[i].color = 0;
if (sep_topo_visit(g, product->root, order, &norder, stack, 0) < 0) {
free(replacement); free(stack); free(order);
return -1;
}
const struct seppkg *ownerpkg = &g->pkg[product->ptest];
char *owner = sep_sprintf("%s#%s", ownerpkg->import_base,
ownerpkg->canon);
if (owner == NULL) {
free(replacement); free(stack); free(order);
return -1;
}
for (int oi = 0; oi < norder; oi++) {
int pi = order[oi];
if (pi == product->production_root) continue;
int changed = 0;
for (int k = 0; k < g->pkg[pi].ndeps; k++) {
int dep = g->pkg[pi].deps[k];
if (dep >= 0 && dep < nbase && replacement[dep] != dep) {
changed = 1;
break;
}
}
if (!changed) continue;
if (pi == product->ptest || pi == product->pxtest
|| pi == product->root) {
if (sep_rewrite_action_deps(g, pi, replacement, nbase) < 0) {
free(owner); free(replacement); free(stack); free(order);
return -1;
}
} else {
int copy = sep_clone_for_test(g, pi, owner, replacement, nbase);
if (copy < 0) {
free(owner); free(replacement); free(stack); free(order);
return -1;
}
replacement[pi] = copy;
}
}
free(owner); free(replacement); free(stack); free(order);
return 0;
}
/* 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;
}
for (int i = 0; i < g->pkg[pi].nsources && rc == 0; i++)
rc = sep_register_file_fold(g, g->pkg[pi].canon,
g->pkg[pi].sources[i]);
}
}
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;
}
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;
if (g->pkg[a].role != g->pkg[b].role)
return g->pkg[a].role - g->pkg[b].role;
int rcanon = strcmp(g->pkg[a].canon, g->pkg[b].canon);
if (rcanon != 0) return rcanon;
if (g->pkg[a].for_test == NULL || g->pkg[b].for_test == NULL)
return g->pkg[a].for_test == NULL
? (g->pkg[b].for_test == NULL ? 0 : -1) : 1;
return strcmp(g->pkg[a].for_test, g->pkg[b].for_test);
}
/* 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 **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;
active[root] = 1;
todo[ntodo++] = root;
while (ntodo > 0) {
int pi = todo[--ntodo];
for (int k = 0; k < g->pkg[pi].ndeps; k++) {
int dep = 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 = 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, &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) {
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;
}
/* Go's BuildInstallFunc installs linked test binaries with 0777 filtered by
* the caller's umask. Keep the retained copy byte-identical to the temporary
* runnable while giving the new staging inode that executable mode. */
static int
copy_executable_stage(const char *src, const char *dst)
{
FILE *in = fopen(src, "rb");
if (in == NULL) return -1;
int fd = open(dst, O_WRONLY | O_CREAT | O_TRUNC, 0777);
if (fd < 0) { fclose(in); return -1; }
FILE *out = fdopen(fd, "wb");
if (out == NULL) { close(fd); fclose(in); (void)unlink(dst); 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_publish);
free(products[i].stage_out);
products[i].stage_status = NULL;
products[i].stage_iface = NULL;
products[i].stage_publish = 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;
}
static int
sep_product_paths_overlap(const char *a, const char *b)
{
if (a == NULL || b == NULL) return 0;
if (strcmp(a, b) == 0) return 1;
size_t an = strlen(a), bn = strlen(b);
return (an == bn + sizeof ".new" - 1
&& memcmp(a, b, bn) == 0 && strcmp(a + bn, ".new") == 0)
|| (bn == an + sizeof ".new" - 1
&& memcmp(b, a, an) == 0 && strcmp(b + an, ".new") == 0);
}
static int
sep_validate_product_path_pair(const struct sepproduct *a,
const struct sepproduct *b)
{
const char *ap[] = {
a->stage_status != NULL ? a->status : NULL,
a->stage_out != NULL ? a->out : NULL,
a->stage_publish != NULL ? a->publish : NULL,
a->stage_status, a->stage_out, a->stage_publish, a->stage_iface };
const char *bp[] = {
b->stage_status != NULL ? b->status : NULL,
b->stage_out != NULL ? b->out : NULL,
b->stage_publish != NULL ? b->publish : NULL,
b->stage_status, b->stage_out, b->stage_publish, b->stage_iface };
for (size_t i = 0; i < nelem(ap); i++) {
if (ap[i] == NULL) continue;
for (size_t j = 0; j < nelem(bp); j++) {
if (!sep_product_paths_overlap(ap[i], bp[j])) continue;
fprintf(stderr, "ww: product path collision: %s\n", bp[j]);
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 (products[i].publish != NULL && !products[i].no_tests
&& sep_prepare_product_stage(&products[i].stage_publish,
products[i].publish) < 0)
return -1;
if (emit_asm) continue;
int owns_output = root_package ? publish_package
: is_test ? !products[i].no_tests
: 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;
}
}
for (int i = 0; i < nproducts; i++)
for (int j = 0; j < i; j++)
if (sep_validate_product_path_pair(&products[j],
&products[i]) < 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 ? 19 : 18, 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_publish, 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].production_root = -1;
products[i].ptest = -1;
products[i].pxtest = -1;
products[i].stage_out = NULL;
products[i].stage_publish = 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 *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;
}
if (products[i].directory_product) {
if (products[i].production_package != NULL) {
products[i].production_root = sep_find_or_add_variant(g,
rootpath, entry, 1, SEP_VARIANT_PRODUCTION, NULL,
SEP_ROLE_NORMAL, NULL, 1);
if (products[i].production_root < 0) {
free(inferred_path);
return 1;
}
}
if (products[i].internal_package != NULL) {
products[i].ptest = sep_find_or_add_variant(g, rootpath,
entry, 1, SEP_VARIANT_SAME_TEST,
products[i].internal_package, SEP_ROLE_NORMAL, NULL, 1);
if (products[i].ptest < 0) {
free(inferred_path);
return 1;
}
} else if (is_test && !products[i].no_tests) {
products[i].ptest = products[i].production_root;
}
if (products[i].external_package != NULL) {
products[i].pxtest = sep_find_or_add_variant(g, rootpath,
entry, 1, SEP_VARIANT_EXTERNAL,
products[i].external_package, SEP_ROLE_NORMAL, NULL, 1);
if (products[i].pxtest < 0) {
free(inferred_path);
return 1;
}
}
products[i].root = !is_test || products[i].no_tests
? products[i].production_root
: products[i].ptest >= 0 ? products[i].ptest
: products[i].pxtest;
products[i].variant_root = products[i].ptest;
} else {
const char *selector = products[i].variant
== SEP_VARIANT_PRODUCTION ? NULL : products[i].test_package;
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);
products[i].variant_root = products[i].root;
}
free(inferred_path);
if (products[i].root < 0) return 1;
}
for (int i = 0; i < nproducts; i++) {
if (!products[i].directory_product) continue;
int a = products[i].production_root >= 0
? products[i].production_root
: products[i].ptest >= 0 ? products[i].ptest : products[i].pxtest;
for (int j = 0; j < i; j++) {
if (!products[j].directory_product) continue;
int b = products[j].production_root >= 0
? products[j].production_root
: products[j].ptest >= 0 ? products[j].ptest
: products[j].pxtest;
int duplicate = a >= 0 && b >= 0 && a == b;
if (!duplicate && a >= 0 && b >= 0) {
const char *aid = g->pkg[a].import_base;
const char *bid = g->pkg[b].import_base;
if (aid != NULL && bid != NULL)
duplicate = strcmp(aid, bid) == 0;
else if (aid == NULL && bid == NULL)
duplicate = strcmp(g->pkg[a].canon,
g->pkg[b].canon) == 0;
}
if (duplicate) {
fprintf(stderr,
"ww test: duplicate --ww-package-test product for canonical directory\n");
return 1;
}
}
}
const char *test_support_module = "test";
int have_runnable_tests = 0;
for (int i = 0; i < nproducts; i++)
if (is_test && !products[i].no_tests) have_runnable_tests = 1;
/* -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 (have_runnable_tests) {
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++) {
if (products[i].no_tests) continue;
int target = products[i].ptest >= 0
? products[i].ptest : products[i].pxtest;
int root_is_support = target >= 0 && tc != NULL
&& strcmp(g->pkg[target].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++) {
if (products[i].no_tests) continue;
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++) {
if (products[i].no_tests) continue;
int target = products[i].ptest >= 0
? products[i].ptest : products[i].pxtest;
int root_is_support = target >= 0 && tc != NULL
&& strcmp(g->pkg[target].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].support = target;
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 roots[3] = { products[i].production_root,
products[i].ptest, products[i].pxtest };
const char *selectors[3] = { products[i].production_package,
products[i].internal_package, products[i].external_package };
int nroots = products[i].directory_product ? 3 : 1;
if (!products[i].directory_product) {
roots[0] = products[i].variant_root;
selectors[0] = products[i].variant == SEP_VARIANT_PRODUCTION
? NULL : products[i].test_package;
}
/* 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 != roots[0]
&& sep_add_dep(g, roots[0], support) < 0)
return 1;
g->pkg[roots[0]].link_entry = 1;
}
for (int ri = 0; ri < nroots; ri++) {
int root = roots[ri];
if (root < 0) continue;
if (ri > 0 && products[i].production_root >= 0
&& g->pkg[products[i].production_root].failed) {
g->pkg[root].failed = 1;
continue;
}
int duplicate = 0;
for (int rj = 0; rj < ri; rj++)
if (roots[rj] == root) duplicate = 1;
if (duplicate) continue;
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 (selectors[ri] != NULL
&& strcmp(g->pkg[root].name, selectors[ri]) != 0) {
fprintf(stderr,
"ww: package-test selector does not match loaded package\n");
g->pkg[root].failed = 1;
}
}
}
if (g->identity_failed) return 1;
if (have_runnable_tests) {
for (int i = 0; i < nproducts; i++) {
if (products[i].no_tests) continue;
int variant = products[i].ptest >= 0
? products[i].ptest : products[i].pxtest;
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].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++) {
if (products[i].no_tests) continue;
int variant = products[i].ptest >= 0
? products[i].ptest : products[i].pxtest;
int support = products[i].support;
int failed = g->pkg[variant].failed;
if (products[i].production_root >= 0
&& g->pkg[products[i].production_root].failed) failed = 1;
if (products[i].ptest >= 0
&& g->pkg[products[i].ptest].failed) failed = 1;
if (products[i].pxtest >= 0
&& g->pkg[products[i].pxtest].failed) failed = 1;
if (failed
|| (support >= 0 && g->pkg[support].failed)) {
products[i].root = variant;
g->pkg[variant].failed = 1;
continue;
}
int mainpkg = sep_add_generated_main(g, &products[i], i,
support);
if (mainpkg < 0) return 1;
products[i].root = mainpkg;
}
for (int i = 0; i < nproducts; i++) {
if (products[i].no_tests
|| g->pkg[products[i].root].failed) continue;
if (sep_recompile_for_test(g, &products[i]) < 0) return 1;
}
}
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].publish != NULL
&& validate_command_output_path(products[i].publish) < 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, &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, is_test ? 0777 : 0700,
&created_output) != 0) {
fprintf(stderr, is_test
? "ww: cannot create test output directory %s\n"
: "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].ngenerated_targets
> ((size_t)-1 - cargvcap) / 2) {
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 += 2 * (size_t)g->pkg[pi].ngenerated_targets;
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) {
for (int k = 0; k < g->pkg[pi].ngenerated_targets; k++) {
cargv[cpos++] = "--test-target-package";
cargv[cpos++] = g->pkg[
g->pkg[pi].generated_targets[k]].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 exact reverse-topological archive closure: root
* `.a` first, then every transitively reachable package `.a`, then
* libwwrt.a. Test substitution is already explicit in the graph. */
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;
if (is_test && products[i].no_tests) {
if (sep_stage_product_status(&products[i]) != 0) {
fprintf(stderr, "ww: cannot stage package-test product\n");
goto request_fail;
}
continue;
}
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];
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 (products[i].publish != NULL
&& (products[i].stage_publish == NULL
|| copy_executable_stage(products[i].stage_out,
products[i].stage_publish) != 0)) {
fprintf(stderr, "ww: cannot stage test binary %s\n",
products[i].publish);
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_publish != NULL
&& sep_txn_add(&tx, products[i].stage_publish,
products[i].publish) < 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,
.publish = 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 + 9 >= argc) {
fprintf(stderr,
"ww test: --ww-package-test needs kind, package, production, internal, external, directory, output, publication, and status\n");
return 2;
}
const char *kind = argv[++i];
const char *name = argv[++i];
const char *production = argv[++i];
const char *internal = argv[++i];
const char *external = argv[++i];
const char *dir = argv[++i];
const char *output = argv[++i];
const char *publish = argv[++i];
const char *status = argv[++i];
size_t pn = strlen(name);
int build_product = strcmp(kind, "build") == 0;
int test_product = strcmp(kind, "test") == 0;
int has_production = strcmp(production, "-") != 0;
int has_internal = strcmp(internal, "-") != 0;
int has_external = strcmp(external, "-") != 0;
if ((!build_product && !test_product)
|| pn == 0
|| dir[0] == '\0' || output[0] == '\0'
|| publish[0] == '\0'
|| status[0] == '\0'
|| (has_production && strcmp(production, name) != 0)
|| (has_internal && strcmp(internal, name) != 0)
|| (has_external && (strlen(external) != pn + 5
|| strncmp(external, name, pn) != 0
|| strcmp(external + pn, "_test") != 0))
|| (build_product && (!has_production
|| has_internal || has_external
|| strcmp(publish, "-") != 0))
|| (test_product && !has_production
&& !has_internal && !has_external)
|| (test_product && !has_internal && !has_external
&& strcmp(publish, "-") != 0)) {
fprintf(stderr,
"ww test: invalid --ww-package-test product\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].production_package = has_production
? production : NULL;
products[nproducts].internal_package = has_internal
? internal : NULL;
products[nproducts].external_package = has_external
? external : NULL;
products[nproducts].status = status;
products[nproducts].publish = strcmp(publish, "-") == 0
? NULL : publish;
products[nproducts].artifact = NULL;
products[nproducts].variant = build_product
? SEP_VARIANT_PRODUCTION : SEP_VARIANT_TEST_MAIN;
products[nproducts].directory_product = 1;
products[nproducts].no_tests = test_product
&& !has_internal && !has_external;
products[nproducts].root = -1;
products[nproducts].variant_root = -1;
products[nproducts].production_root = -1;
products[nproducts].ptest = -1;
products[nproducts].pxtest = -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) {
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) {
fprintf(stderr,
"ww test: duplicate --ww-package-test product 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 && nproducts == 0) {
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;
}
}
} else {
for (int i = 0; i < nproducts; i++) {
if (products[i].directory_product
&& products[i].variant == SEP_VARIANT_PRODUCTION) {
fprintf(stderr,
"ww test: package-test products must use test kind\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;
}
/* The coordinator independently wires -o retention and -c run
* suppression after it has loaded the complete package set. */
/* -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 (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 (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;
}