/* * 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 #include #include #include #include #include #include #include #include #include #include #include #ifndef PATH_MAX #define PATH_MAX 4096 #endif static const char *usage = "usage: ww [-V] [args...]\n" " -V print version and exit\n" " build [-S] [-w DIR] [-I DIR] [-o FILE|DIR] [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 FILE publishes a non-main archive FILE + FILE.wwi\n" " -o DIR publishes each selected command beneath DIR\n" " lib/... every eligible package under lib, recursively\n" " . current directory package (default when no path is given)\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; } extern char **environ; struct test_environment { char **values; char *path; }; /* Pinned cmd/go appends PATH=$GOROOT/bin:$PATH to each test command and * os/exec keeps the appended duplicate. The selected WW driver's sibling * directory is the local toolchain-bin analogue. Build tools retain the * caller's environment; only the test binary receives this array. */ static int make_test_environment(const char *toolbin, struct test_environment *out) { const char *oldpath = getenv("PATH"); if (oldpath == NULL) oldpath = ""; size_t dirn = strlen(toolbin), oldn = strlen(oldpath); if (oldn > SIZE_MAX - 7 || dirn > SIZE_MAX - oldn - 7) return -1; size_t pathn = 5 + dirn + (oldn != 0 ? 1 + oldn : 0); char *path = malloc(pathn + 1); if (path == NULL) return -1; memcpy(path, "PATH=", 5); memcpy(path + 5, toolbin, dirn); size_t at = 5 + dirn; if (oldn != 0) { path[at++] = ':'; memcpy(path + at, oldpath, oldn); at += oldn; } path[at] = '\0'; size_t nenv = 0; while (environ[nenv] != NULL) { if (nenv == SIZE_MAX - 2) { free(path); return -1; } nenv++; } if (nenv > SIZE_MAX / sizeof(char *) - 2) { free(path); return -1; } char **values = calloc(nenv + 2, sizeof *values); if (values == NULL) { free(path); return -1; } size_t n = 0; int inserted = 0; for (size_t i = 0; i < nenv; i++) { if (strncmp(environ[i], "PATH=", 5) == 0) { if (!inserted) { values[n++] = path; inserted = 1; } } else { values[n++] = environ[i]; } } if (!inserted) values[n++] = path; values[n] = NULL; out->values = values; out->path = path; return 0; } static void free_test_environment(struct test_environment *env) { free(env->values); free(env->path); } /* 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) { char toolbin[PATH_MAX]; if (realpath(self_dir, toolbin) == NULL) { fputs("ww: cannot prepare test environment\n", stderr); return -1; } struct test_environment env = {0}; if (make_test_environment(toolbin, &env) != 0) { fputs("ww: cannot prepare test environment\n", stderr); return -1; } pid_t pid = fork(); if (pid < 0) { perror("ww: fork"); free_test_environment(&env); 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; } execve(bin, xargv, env.values); 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); free_test_environment(&env); 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 - 11) { free(fallback); sep_fail_size(); return 1; } char **xargv = calloc((size_t)argc + 11, 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 (!build_only && !add_dot) xargv[n++] = "--ww-explicit-test-target"; 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 * /.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 // only. A decoy * /.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 /.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; } /* Named-file loading is allowed for every non-directory Stat result. Unlike * sep_slurp, this reader must not require seekability: a finite FIFO is still * a named source stream, just as it is for GoFilesPackage. */ static int source_header_slurp(const char *path, char **out, u64 *len) { int fd = open(path, O_RDONLY); if (fd < 0) return -1; size_t cap = 4096, n = 0; char *buf = malloc(cap); if (buf == NULL) { (void)sep_fail_nomem(); (void)close(fd); return -1; } for (;;) { if (n == cap) { if (cap >= INT_MAX) { (void)sep_fail_size(); free(buf); (void)close(fd); return -1; } size_t nextcap = cap > (size_t)INT_MAX / 2 ? (size_t)INT_MAX : cap * 2; char *next = realloc(buf, nextcap); if (next == NULL) { (void)sep_fail_nomem(); free(buf); (void)close(fd); return -1; } buf = next; cap = nextcap; } ssize_t got = read(fd, buf + n, cap - n); if (got < 0) { if (errno == EINTR) continue; free(buf); (void)close(fd); return -1; } if (got == 0) break; n += (size_t)got; } if (close(fd) != 0) { free(buf); return -1; } if (n == cap) { /* The loop normally grows before the read that can fill cap, but * retain an explicit terminator guard for future reader changes. */ if (cap >= INT_MAX) { (void)sep_fail_size(); free(buf); return -1; } char *next = realloc(buf, cap + 1); if (next == NULL) { (void)sep_fail_nomem(); free(buf); return -1; } buf = next; } buf[n] = '\0'; *out = buf; *len = (u64)n; return 0; } /* Validate only the package/import header before a named test source is * omitted from an ordinary build. Imports are syntax, not graph edges here. */ static int source_build_header(const char *path) { char *buf; u64 len; if (source_header_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 *header = parsepackageheader(&p); if (l.errs || p.errs) { freearena(a); free(buf); return -1; } if (header->module == NULL) { Pos pp = { path, 1, 1 }; errorf(pp, "invalid or missing package clause"); 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 : ∏ 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 action; /* reached by this request's semantic action list */ 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 build_action; /* loaded product retained in the action list */ int public_out; 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 || !g->pkg[i].action) 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 || !g->pkg[i].action) continue; for (int j = i + 1; j < g->n; j++) { if (g->pkg[j].failed || !g->pkg[j].loaded || !g->pkg[j].action || 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 || !g->pkg[i].action) 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; /* A test copy is a product-scoped action even when its replaced source * node is not in the final action closure. Preserve its established * complete-action locator without relying on a collision with that * inactive source node during artifact validation. */ p->storage = sep_storage_digest(p); p->storage_hashed = 1; if (p->storage == NULL) goto fail; 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; } /* Each physical source owns its own first-codepoint position. The * compiler sees one synthetic aggregate, so replace the optional UTF-8 * BOM with position-preserving whitespace rather than turning a later * source's marker into a mid-unit BOM. */ u64 off = len >= 3 && (unsigned char)buf[0] == 0xef && (unsigned char)buf[1] == 0xbb && (unsigned char)buf[2] == 0xbf ? 3 : 0; if (off != 0 && fwrite(" ", 1, 3, out) != 3) bad = 1; if (fwrite(buf + off, 1, (size_t)(len - off), out) != (size_t)(len - off) || 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("!\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; int public_output; }; struct septxn { struct septxnentry *v; int n, cap; }; static int sep_txn_add_mode(struct septxn *tx, const char *stage, const char *dst, int public_output) { 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()); e->public_output = public_output; 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 int sep_txn_add(struct septxn *tx, const char *stage, const char *dst) { return sep_txn_add_mode(tx, stage, dst, 0); } static int sep_txn_add_public(struct septxn *tx, const char *stage, const char *dst) { return sep_txn_add_mode(tx, stage, dst, 1); } 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); } struct sep_output_magic { const unsigned char *bytes; size_t n; }; #define SEP_OUTPUT_MAGIC(s) { (const unsigned char *)(s), sizeof(s) - 1 } /* Go 1.26.5 work.objectMagic. WW interfaces are an additional public output * kind and always begin with the compiler-owned module directive. */ static const struct sep_output_magic sep_output_magic[] = { SEP_OUTPUT_MAGIC("!\n"), SEP_OUTPUT_MAGIC("\n"), SEP_OUTPUT_MAGIC("\x7f" "ELF"), SEP_OUTPUT_MAGIC("\xfe\xed\xfa\xce"), SEP_OUTPUT_MAGIC("\xfe\xed\xfa\xcf"), SEP_OUTPUT_MAGIC("\xce\xfa\xed\xfe"), SEP_OUTPUT_MAGIC("\xcf\xfa\xed\xfe"), SEP_OUTPUT_MAGIC("\x4d\x5a\x90\x00\x03\x00"), SEP_OUTPUT_MAGIC("\x4d\x5a\x78\x00\x01\x00"), SEP_OUTPUT_MAGIC("\x00\x00\x01\xeb"), SEP_OUTPUT_MAGIC("\x00\x00\x8a\x97"), SEP_OUTPUT_MAGIC("\x00\x00\x06\x47"), SEP_OUTPUT_MAGIC("\x00\x61\x73\x6d"), SEP_OUTPUT_MAGIC("\x01\xdf"), SEP_OUTPUT_MAGIC("\x01\xf7"), SEP_OUTPUT_MAGIC("//ww:module "), }; #undef SEP_OUTPUT_MAGIC static int sep_is_object_output(const char *path) { unsigned char buf[64] = {0}; int fd = open(path, O_RDONLY); if (fd < 0) return 0; size_t got = 0; int bad = 0; while (got < sizeof buf) { ssize_t n = read(fd, buf + got, sizeof buf - got); if (n > 0) got += (size_t)n; else if (n == 0) break; else if (errno != EINTR) { bad = 1; break; } } (void)close(fd); if (bad) return 0; for (size_t i = 0; i < nelem(sep_output_magic); i++) if (got >= sep_output_magic[i].n && memcmp(buf, sep_output_magic[i].bytes, sep_output_magic[i].n) == 0) return 1; return 0; } /* Pinned Go's checkDstOverwrite follows symlinks with stat and protects only * directories and nonempty regular non-object files. Other file kinds remain * eligible for the install operation itself to replace. */ static int sep_check_dst_overwrite(const char *dst) { struct stat st; if (stat(dst, &st) != 0) return 0; if (S_ISDIR(st.st_mode)) { fputs("ww: build output ", stderr); sep_put_quoted(dst); fputs(" already exists and is a directory\n", stderr); return -1; } if (S_ISREG(st.st_mode) && st.st_size != 0 && !sep_is_object_output(dst)) { fputs("ww: build output ", stderr); sep_put_quoted(dst); fputs(" already exists and is not an object file\n", stderr); return -1; } return 0; } /* One request-wide rollback group: producers and linkers finish first; only * then are public destinations checked, 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 (tx->v[i].public_output && sep_check_dst_overwrite(tx->v[i].dst) != 0) 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 || !g->pkg[pi].action) 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++) { if (!g->pkg[pi].action) continue; 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; } /* A retained running test is built and executed from request-private storage. * Its Go-like install action runs only after that test succeeds, but reuses * the same guarded publisher as build and compile-only test products. */ static int sep_install_test_output(const char *stage, const char *dst) { char *install_stage = sep_sprintf("%s.install", stage); if (install_stage == NULL) return 1; if (strlen(install_stage) + 1 > (size_t)PATH_MAX || path_exists_nofollow(install_stage) != 0 || copy_executable_stage(stage, install_stage) != 0) { fputs("ww: cannot stage retained test output\n", stderr); free(install_stage); return 1; } char *parent = sep_parent_path(dst); if (parent == NULL) { (void)unlink(install_stage); free(install_stage); return 1; } struct sep_created_dirs created = {0}; if (sep_mkdirs(parent, 0777, &created) != 0) { fprintf(stderr, "ww: cannot create test output directory %s\n", parent); free(parent); (void)unlink(install_stage); free(install_stage); return 1; } free(parent); struct septxn tx = {0}; if (sep_txn_add_public(&tx, install_stage, dst) != 0 || sep_txn_commit(&tx) != 0) { sep_txn_discard(&tx); sep_txn_free(&tx); sep_rollback_dirs(&created); (void)unlink(install_stage); free(install_stage); return 1; } sep_txn_free(&tx); free(install_stage); 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 `.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, const char *default_output_dir, int output_path_error, const char *output_collision_base, const char *output_collision_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]); 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 (!is_test && create_output_dir != NULL) { int actions = 0; for (int i = 0; i < nproducts; i++) { int root = products[i].root; if (g->pkg[root].failed) { free(stack); free(order); return 1; } products[i].build_action = sep_root_is_command(&g->pkg[root]); if (products[i].build_action) actions++; } if (actions == 0) { fputs("ww: no main packages to build\n", stderr); 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 (products[i].build_action && !g->pkg[root].failed && sep_topo_visit(g, root, order, &norder, stack, 0) < 0) { free(stack); free(order); return 1; } } free(stack); for (int pi = 0; pi < g->n; pi++) g->pkg[pi].action = 0; for (int oi = 0; oi < norder; oi++) g->pkg[order[oi]].action = 1; if (sep_validate_artifact_paths(g, scratch) < 0) { free(order); return 1; } if (warm && workdir_exists && sep_validate_workdir_owners(g, scratch) < 0) { free(order); return 1; } /* 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 (products[i].build_action && !g->pkg[products[i].root].failed) viable_product = 1; if (!viable_product) { free(order); return 1; } if (!is_test && !emit_asm && output_path_error) { fputs("ww: command output path is too long\n", stderr); free(order); return 1; } if (!is_test && !emit_asm && output_collision_base != NULL) { fputs("ww: multiple commands produce output basename ", stderr); sep_put_quoted(output_collision_base); fputs(" in directory ", stderr); sep_put_quoted(output_collision_dir); fputc('\n', stderr); free(order); return 1; } if (!is_test && !emit_asm && default_output_dir != NULL && nproducts == 1 && sep_root_is_command(&g->pkg[products[0].root])) { fprintf(stderr, "ww: build output \"%s\" already exists and is a directory\n", default_output_dir); 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 (!emit_asm && create_output_dir != NULL && create_output_dir[0] != '\0' && sep_mkdirs(create_output_dir, 0777, &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 && (products[i].public_out ? sep_txn_add_public(&tx, products[i].stage_out, products[i].out) : sep_txn_add(&tx, products[i].stage_out, products[i].out)) < 0) goto request_fail; if (products[i].stage_publish != NULL && sep_txn_add_public(&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_public(&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 public_output, int keepscratch, const char *workdir, const char *create_output_dir, const char *default_output_dir, int output_path_error) { 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, .build_action = 1, .public_out = public_output, .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, create_output_dir, default_output_dir, output_path_error, NULL, 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, const char *default_output_dir, int output_path_error, const char *output_collision_base, const char *output_collision_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, default_output_dir, output_path_error, output_collision_base, output_collision_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 : : 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 source_operand_named(const char *path) { size_t n = strlen(path); return n >= 3 && strcmp(path + n - 3, ".ww") == 0; } /* Go's named-file package still applies matchFile's unconditional basename * exclusion before UseAllFiles, platform suffixes, source parsing, or test-file * classification. Keep this predicate on the public operand spelling: a * hidden parent does not hide a visible named file, while a hidden symlink * spelling remains hidden regardless of its non-directory target kind. */ static int source_operand_ignored(const char *path) { struct stat st; if (!source_operand_named(path)) return 0; if (stat(path, &st) != 0 || S_ISDIR(st.st_mode)) return 0; const char *base = strrchr(path, '/'); base = base ? base + 1 : path; return base[0] == '.' || base[0] == '_'; } static void source_operand_no_sources(const char *path) { const char *slash = strrchr(path, '/'); fputs("ww: ", stderr); if (slash == NULL) { fputc('.', stderr); } else if (slash == path) { fputc('/', stderr); } else { (void)fwrite(path, 1, (size_t)(slash - path), stderr); } fputs(": directory contains no WW package sources\n", stderr); } /* Go's build -o directory branch follows an existing destination through * stat, and a trailing platform separator declares a directory which the * request may need to create. WW's platform separator is '/'. */ static int build_output_dir(const char *path) { struct stat st; size_t n = strlen(path); if (n != 0 && path[n - 1] == '/') return 1; return stat(path, &st) == 0 && S_ISDIR(st.st_mode); } static int build_output_path(const char *dir, const char *src, char *out, size_t outsz) { char base[PATH_MAX]; size_t n = strlen(dir); int written; basename_no_ext(src, base, sizeof base); written = snprintf(out, outsz, "%s%s%s", dir, n != 0 && dir[n - 1] == '/' ? "" : "/", base); if (written < 0 || (size_t)written >= outsz) return -1; return 0; } static void build_import_leaf(const char *identity, char *out, size_t outsz) { const char *leaf = strrchr(identity, '.'); leaf = leaf ? leaf + 1 : identity; snprintf(out, outsz, "%s", leaf); } static int build_current_dir_spelling(const char *path) { if (path[0] == '\0' || path[0] == '/') return 0; const char *p = path; int found = 0; for (;;) { while (*p == '/') p++; if (*p == '\0') return found; if (*p++ != '.' || (*p != '\0' && *p != '/')) return 0; found = 1; } } static void build_local_dir_leaf(const char *resolved, char *out, size_t outsz) { char canonical[PATH_MAX]; const char *selected = resolved; if (build_current_dir_spelling(resolved) && realpath(resolved, canonical) != NULL) selected = canonical; char tmp[PATH_MAX]; memcpy(tmp, selected, strlen(selected) + 1); size_t n = strlen(tmp); while (n > 1 && tmp[n - 1] == '/') tmp[--n] = '\0'; const char *base = strrchr(tmp, '/'); snprintf(out, outsz, "%s", base ? base + 1 : tmp); } 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) && source_operand_named(name)) { 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 || (strcmp(cmd, "build") == 0 && 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 (strcmp(cmd, "build") == 0 && strncmp(argv[i], "-o=", 3) == 0) { if (cli_copy(cmd, "-o", outpath, outsz, argv[i] + 3) < 0) return -1; } else if (strcmp(cmd, "build") == 0 && strncmp(argv[i], "--o=", 4) == 0) { if (cli_copy(cmd, "-o", outpath, outsz, argv[i] + 4) < 0) return -1; } else if (strcmp(cmd, "run") == 0 && 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; } int add_dot = src == NULL; 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 && (S_ISDIR(requested.st_mode) || source_operand_named(src)); int requested_nondirectory = literal && !S_ISDIR(requested.st_mode); if (source_operand_ignored(src)) { source_operand_no_sources(src); free(incs); return 1; } const char *requested_base = strrchr(src, '/'); requested_base = requested_base ? requested_base + 1 : src; size_t requested_baselen = strlen(requested_base); if (requested_nondirectory && requested_baselen >= 8 && strcmp(requested_base + requested_baselen - 8, "_test.ww") == 0) { if (source_build_header(src) < 0) { free(incs); return 1; } free(incs); if (!outflag[0] || strcmp(outflag, "/dev/null") == 0) return 0; if (build_output_dir(outflag)) fputs("ww: no main packages to build\n", stderr); else fputs("ww: no packages to build\n", stderr); return 1; } 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; int discard_output = strcmp(outflag, "/dev/null") == 0; int output_dir = outflag[0] && !discard_output && build_output_dir(outflag); const char *root_identity = !literal && is_dir ? src : NULL; if (output_dir && is_dir) { free(incs); return exec_package_command(argc, argv, src, add_dot ? NULL : resolved, root_identity, add_dot, 1); } const char *create_output_dir = NULL; int output_path_error = 0; if (outflag[0] && !discard_output) { /* -o sets both the binary path and the intermediate stem so * artifacts land beside the requested output (T3). */ if (output_dir) { if (build_output_path(outflag, resolved, out, sizeof out) < 0) { output_path_error = 1; basename_no_ext(resolved, out, sizeof out); } create_output_dir = outflag; } else { memcpy(out, outflag, strlen(outflag) + 1); } objstem = out; } else if (is_dir && root_identity != NULL) { build_import_leaf(root_identity, out, sizeof out); } else if (is_dir) { build_local_dir_leaf(resolved, out, sizeof out); } else { basename_no_ext(resolved, out, sizeof out); } if (!outflag[0] && is_dir && build_current_dir_spelling(resolved)) objstem = out; const char *default_output_dir = !outflag[0] && build_output_dir(out) ? out : NULL; if (discard_output) { char tmpdir[PATH_MAX], tmp[PATH_MAX]; int dn = snprintf(tmpdir, sizeof tmpdir, "/tmp/ww_build_%d", getpid()); if (dn < 0 || (size_t)dn >= sizeof tmpdir || mkdir(tmpdir, 0700) != 0) { fputs("ww: cannot create temporary directory\n", stderr); free(incs); return 1; } int tn = snprintf(tmp, sizeof tmp, "%s/main", tmpdir); if (tn < 0 || (size_t)tn >= sizeof tmp) { (void)rmdir(tmpdir); free(incs); return 1; } /* Go's null output removes installation, while command linking and * library compilation still need request-private product paths. */ int rc = build_one_sep(resolved, is_dir, root_identity, tmp, tmp, incs, &linkflags, 0, 0, 0, SEP_VARIANT_PRODUCTION, NULL, emit_asm, 0, 0, workdir, NULL, NULL, 0); int cleanfail = 0; if (unlink(tmp) != 0 && errno != ENOENT) { fputs("ww: cannot remove temporary output\n", stderr); cleanfail = 1; } if (rmdir(tmpdir) != 0) { fputs("ww: cannot remove temporary directory\n", stderr); cleanfail = 1; } free(incs); if (cleanfail && rc == 0) rc = 1; return rc; } 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, 1, workdir, create_output_dir, default_output_dir, output_path_error); 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 && (S_ISDIR(requested.st_mode) || source_operand_named(src)); if (literal && S_ISDIR(requested.st_mode)) { size_t n = strlen(src); if (source_operand_named(src)) { if (n >= 8 && strcmp(src + n - 8, "_test.ww") == 0) fprintf(stderr, "ww: cannot run *_test.ww files (%s)\n", src); else fprintf(stderr, "%s is a directory, should be a WW file\n", src); free(incs); return 1; } } 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, 0, NULL, NULL, NULL, 0); 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) { if (argc == 3 && strcmp(argv[0], "--ww-install-test-output") == 0) return sep_install_test_output(argv[1], argv[2]); 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 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; int package_output_path_error = 0; const char *package_default_output_dir = NULL; const char *package_output_collision_base = NULL; const char *package_output_collision_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-command-output-path-error") == 0) { if (package_output_path_error) { fprintf(stderr, "ww test: invalid --ww-command-output-path-error\n"); return 2; } package_output_path_error = 1; } else if (strcmp(argv[i], "--ww-default-output-dir") == 0) { if (i + 1 >= argc || package_default_output_dir != NULL || argv[i + 1][0] == '\0') { fprintf(stderr, "ww test: invalid --ww-default-output-dir\n"); return 2; } package_default_output_dir = argv[++i]; } else if (strcmp(argv[i], "--ww-command-output-collision") == 0) { if (i + 2 >= argc || package_output_collision_base != NULL || argv[i + 1][0] == '\0' || argv[i + 2][0] == '\0') { fprintf(stderr, "ww test: invalid --ww-command-output-collision\n"); return 2; } package_output_collision_base = argv[++i]; package_output_collision_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 || strcmp(kind, "build-public") == 0; int public_build_product = strcmp(kind, "build-public") == 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].build_action = 1; products[nproducts].public_out = public_build_product; 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 || 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 (strncmp(argv[i], "-o=", 3) == 0) { if (cli_copy("test", "-o", outstem, sizeof outstem, argv[i] + 3) < 0) return 2; } else if (strncmp(argv[i], "--o=", 4) == 0) { if (cli_copy("test", "-o", outstem, sizeof outstem, argv[i] + 4) < 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_output_path_error || package_default_output_dir != NULL || package_output_collision_base != NULL) && (!package_build || nproducts == 0)) { fprintf(stderr, "ww test: invalid private output preflight\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; } } } int discard_output = strcmp(outstem, "/dev/null") == 0; if (strstr(target, "...") == NULL && source_operand_ignored(target)) { if (nproducts != 0 && outstem[0]) { fprintf(stderr, "ww test: package-test products reject -o\n"); return 2; } 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; } source_operand_no_sources(target); if (src != NULL && !compileonly && !emit_asm) fputs("FAIL\n", stdout); free(products); free(incs); return 1; } if (pattern != NULL) { struct stat first; int first_found = stat(target, &first) == 0; int first_logical = !first_found || (!S_ISDIR(first.st_mode) && !source_operand_named(target)); if (!first_found || !S_ISREG(first.st_mode) || !source_operand_named(target)) { char first_resolved[PATH_MAX]; int first_is_dir = 0; if (!first_logical || !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 || (!S_ISDIR(st.st_mode) && !source_operand_named(target))) { /* 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); if (src != NULL && !compileonly && !emit_asm) fputs("FAIL\n", stdout); 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, package_default_output_dir, package_output_path_error, package_output_collision_base, package_output_collision_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 retain_output = outstem[0] && !discard_output; int deferred_install = retain_output && !compileonly && !emit_asm; int owntmp = deferred_install || ((!outstem[0] || discard_output) && !workdir[0]); if (retain_output && !deferred_install) outp = outstem; else if (workdir[0] && !deferred_install) { /* 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 retained output keeps scratch private; exact /dev/null follows * Go's no-install test action rather than naming an artifact stem. */ int br = build_one_sep(resolved, is_dir, NULL, outp, outstem[0] && !discard_output ? outstem : tmp, incs, NULL, 0, 0, 1, SEP_VARIANT_PRODUCTION, NULL, emit_asm, retain_output && !deferred_install, retain_output ? 1 : 0, workdir, NULL, NULL, 0); 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); if (!compileonly && !emit_asm) fputs("FAIL\n", stdout); 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); int test_failed = rc != 0; if (rc == 0 && deferred_install && sep_install_test_output(outp, outstem) != 0) rc = 1; 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; } if (test_failed) fputs("FAIL\n", stdout); return rc; } if (S_ISREG(st.st_mode) && source_operand_named(target)) { 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 retain_output = outstem[0] && !discard_output; int deferred_install = retain_output && !compileonly && !emit_asm; int owntmp = deferred_install || ((!outstem[0] || discard_output) && !workdir[0]); if (retain_output && !deferred_install) outp = outstem; else if (workdir[0] && !deferred_install) { 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: an unretained output uses private scratch. */ int br = build_one_sep(target, 0, NULL, outp, outstem[0] && !discard_output ? outstem : tmp, incs, NULL, 0, 0, 1, SEP_VARIANT_PRODUCTION, NULL, emit_asm, retain_output && !deferred_install, retain_output ? 1 : 0, workdir, NULL, NULL, 0); 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); if (!compileonly && !emit_asm) fputs("FAIL\n", stdout); 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); int test_failed = rc != 0; if (rc == 0 && deferred_install && sep_install_test_output(outp, outstem) != 0) rc = 1; 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; } if (test_failed) fputs("FAIL\n", stdout); 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, package_default_output_dir, package_output_path_error, package_output_collision_base, package_output_collision_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; }