/* * 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 static const char *usage = "usage: ww [-V] [args...]\n" " -V print version and exit\n" " build [-p] [-S] [-w DIR] [-I DIR] [-o FILE] [path] build a local package graph\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" " -p emits a non-main archive FILE + FILE.wwi\n" " lib/... every package under lib, recursively (test only)\n" " . build the cwd's .ww\n"; static char *self_dir; static const char *self_path; 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[1024]; snprintf(buf, sizeof buf, "%s/%s", self_dir, name); return strdup(buf); } static int run_argv(const char *prog, char *const argv[]) { pid_t pid = fork(); if (pid < 0) { perror("ww: fork"); return -1; } if (pid == 0) { execv(prog, argv); static const char msg[] = "ww: execve failed\n"; (void)write(2, msg, sizeof msg - 1); _exit(127); } int status = 0; pid_t got; do { got = waitpid(pid, &status, 0); } while (got < 0 && errno == EINTR); if (got < 0) { perror("ww: waitpid"); return -1; } if (WIFEXITED(status)) return WEXITSTATUS(status); return 1; } /* run_test_bin — exec the built test binary with an optional name-filter * pattern as argv[1] (lib/test run() reads it via os.args). fork+execv * (not system()) so glob metacharacters in the pattern reach the binary * verbatim instead of being expanded by the shell. Mirrors the wwstage * twin (selfhost/cmd/ww/main.ww runsingletest, which passes the same * argv to os.exec.runstdio). #17 fnmatch filter. */ static int run_test_bin(const char *bin, const char *pattern) { pid_t pid = fork(); if (pid < 0) { perror("ww: fork"); return -1; } if (pid == 0) { char *xargv[3]; xargv[0] = (char *)bin; if (pattern) { xargv[1] = (char *)pattern; xargv[2] = NULL; } else { xargv[1] = NULL; } execv(bin, xargv); perror("ww: exec"); _exit(127); } int status = 0; /* the do_run twin's EINTR discipline: an interrupted wait left * status==0, so WIFEXITED(0)/WEXITSTATUS(0) reported a false * test PASS. */ pid_t got; do { got = waitpid(pid, &status, 0); } while (got < 0 && errno == EINTR); if (got < 0) { perror("ww: waitpid"); return -1; } if (WIFEXITED(status)) return WEXITSTATUS(status); return 1; } /* Delegate package/directory testing to the native WW coordinator. Keep the * old single-file path in this driver: wwtest itself builds each generated * package root through `ww test -c ... package.ww`, so that file boundary also * prevents delegation recursion. */ static int exec_package_tests(int argc, char **argv, const char *target, const char *resolved, const char *root_identity, int add_dot) { const char *override = getenv("WW_WWTEST"); char fallback[1024]; const char *prog = override && override[0] ? override : fallback; if (prog == fallback) snprintf(fallback, sizeof fallback, "%s/wwtest", self_dir); char **xargv = calloc((size_t)argc + 8, sizeof *xargv); if (xargv == NULL) { fputs("ww test: cannot allocate package coordinator arguments\n", stderr); return 1; } int n = 0, dotted = 0; xargv[n++] = (char *)prog; xargv[n++] = "package"; xargv[n++] = "--ww-driver"; xargv[n++] = (char *)self_path; if (root_identity != NULL) { xargv[n++] = "--ww-root-identity"; xargv[n++] = (char *)root_identity; } for (int i = 0; i < argc; i++) { if (add_dot && !dotted && strcmp(argv[i], "--") == 0) { xargv[n++] = "."; dotted = 1; } xargv[n++] = (resolved && argv[i] == target) ? (char *)resolved : argv[i]; } if (add_dot && !dotted) xargv[n++] = "."; xargv[n] = NULL; execv(prog, xargv); /* inherit the caller's environment */ fputs("ww test: cannot exec package test coordinator\n", stderr); free(xargv); return 1; } /* Breaks cycles in `use` resolution. Linear because typical imports are * a handful per build. */ struct ImportSet { char **paths; int n, cap; }; #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 void import_add(struct ImportSet *s, const char *path) { if (s->n + 1 > s->cap) { s->cap = s->cap ? s->cap * 2 : 8; s->paths = realloc(s->paths, s->cap * sizeof *s->paths); } s->paths[s->n++] = strdup(path); } /* `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 void import_path_form(const char *name, char *out, size_t outsz) { size_t i; for (i = 0; i + 1 < outsz && name[i] != '\0'; i++) out[i] = (name[i] == '.') ? '/' : name[i]; out[i] = '\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; snprintf(out, outsz, "%s/%s", dir, path_form); 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(const char *dirs, const char *path_form, char *out, size_t outsz) { const char *p = dirs; while (*p) { const char *e = strchr(p, ':'); size_t n = e ? (size_t)(e - p) : strlen(p); if (n > 0 && n < outsz) { char dir[1024]; if (n >= sizeof dir) n = sizeof dir - 1; memcpy(dir, p, n); dir[n] = '\0'; if (locate_import_in(dir, path_form, out, outsz)) return 1; } if (!e) 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 && n < outsz) { char dir[1024]; if (n >= sizeof dir) n = sizeof dir - 1; memcpy(dir, p, n); dir[n] = '\0'; snprintf(out, outsz, "%s/%s.ww", dir, path_form); if (access(out, 0) == 0) { *is_dir = 0; return 1; } } if (!e) break; p = e + 1; } return 0; } /* Byte-wise total order is locale-independent; rule-10 byte-id requires * the two stages sort the same way. strcmp diverges from Hare's memcmp * (ref/hare/sort/cmp/cmp.ha:9); the order is identical for NUL-free * filenames. */ static int strs_cmp(const void *a, const void *b) { const char *sa = *(const char *const *)a; const char *sb = *(const char *const *)b; return strcmp(sa, sb); } static int sep_slurp(const char*, char**, u64*); /* Go's contract: only *_test.ww is a test source. Ask the compiler parser, * rather than a textual attribute scan, whether a production source contains * @test; otherwise valid whitespace/comments could silently drop a test. */ static int source_has_test_decl(const char *path) { char *buf; u64 len; if (sep_slurp(path, &buf, &len) < 0) return -1; /* Keep directory-loader package-clause diagnostics stable. The full * parser reports its language-level "missing package clause" first; * the imports-only pass owns the package-loader wording and also avoids * stage-specific recovery diagnostics for a malformed clause. */ Arena *ia = newarena(); Lex il; Parser ip; lexinit(&il, ia, path, buf, len); parserinit(&ip, ia, &il); Node *imports = parseimports(&ip); if (il.errs || ip.errs) { freearena(ia); free(buf); return -1; } if (imports->module == NULL) { Pos pp = { path, 1, 1 }; errorf(pp, "invalid or missing package clause"); freearena(ia); free(buf); return -1; } freearena(ia); Arena *a = newarena(); Lex l; Parser p; lexinit(&l, a, path, buf, len); parserinit(&p, a, &l); Node *file = parsefile(&p); if (l.errs || p.errs) { freearena(a); free(buf); return -1; } int found = 0; for (Node *d = file->list; d != NULL && !found; d = d->next) if (d->kind == N_FNDECL) for (Node *at = d->attr; at != NULL; at = at->next) if (at->str != NULL && strcmp(at->str, "test") == 0) { found = 1; break; } freearena(a); free(buf); return found; } #define SEP_VARIANT_PRODUCTION 0 #define SEP_VARIANT_SAME_TEST 1 #define SEP_VARIANT_EXTERNAL 2 #define SEP_VARIANT_TEST_MAIN 3 #define SEP_ROLE_NORMAL 0 #define SEP_ROLE_TEST_SUPPORT 1 #define SEP_ROLE_GENERATED_MAIN 2 #define SEP_TEST_SUPPORT_MODULE "__wwtest" #define SEP_IMPORT_PATH_MAX 256 #define SEP_MAXPRODUCT 256 #define SEP_MAXCONTEXT (SEP_MAXPRODUCT + 1) /* 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, size_t outsz) { 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; } if (strlen(imports->module) >= outsz) { errorf(imports->pos, "package name is too long"); freearena(a); free(buf); return -1; } snprintf(out, outsz, "%s", imports->module); freearena(a); free(buf); return 0; } static int source_list_add(char ***list, int *n, int *cap, const char *path) { if (*n + 1 > *cap) { int ncap = *cap ? *cap * 2 : 8; char **next = realloc(*list, (size_t)ncap * sizeof *next); if (next == NULL) return -1; *list = next; *cap = ncap; } char *copy = strdup(path); if (copy == NULL) return -1; (*list)[(*n)++] = copy; return 0; } static void source_list_free(char **list, int n) { for (int i = 0; i < n; i++) free(list[i]); free(list); } /* Production packages select production files only. A same-package test root * selects production files followed by matching same-package test files; an * external root selects only matching external-test files. Each partition is * byte-sorted so compiler test discovery is deterministic without generated * package amalgamation. */ static int enumerate_dir_ww(const char *dirpath, int variant, const char *test_package, char ***out_files) { DIR *d = opendir(dirpath); if (d == NULL) { *out_files = NULL; return -1; } char **prod = NULL, **tests = NULL; int nprod = 0, capprod = 0, ntests = 0, captests = 0; struct dirent *ent; while ((ent = readdir(d)) != NULL) { const char *nm = ent->d_name; size_t nl = strlen(nm); if (nl <= 3) continue; if (strcmp(nm + nl - 3, ".ww") != 0) continue; int is_test = nl >= 8 && strcmp(nm + nl - 8, "_test.ww") == 0; if (variant == SEP_VARIANT_PRODUCTION && is_test) continue; if (variant == SEP_VARIANT_EXTERNAL && !is_test) continue; char path[2048]; snprintf(path, sizeof path, "%s/%s", dirpath, nm); struct stat st; if (lstat(path, &st) != 0 || !S_ISREG(st.st_mode)) { fprintf(stderr, "ww: %s: package source is not a regular file\n", path); source_list_free(prod, nprod); source_list_free(tests, ntests); closedir(d); *out_files = NULL; return -2; } int has_test = !is_test ? source_has_test_decl(path) : 0; if (has_test < 0) { source_list_free(prod, nprod); source_list_free(tests, ntests); closedir(d); *out_files = NULL; return -2; } if (has_test > 0) { fprintf(stderr, "ww: %s: @test declaration outside *_test.ww\n", path); source_list_free(prod, nprod); source_list_free(tests, ntests); closedir(d); *out_files = NULL; return -2; } if (is_test) { char package[256]; if (source_package_name(path, package, sizeof package) < 0) { source_list_free(prod, nprod); source_list_free(tests, ntests); closedir(d); *out_files = NULL; return -2; } if (test_package == NULL || strcmp(package, test_package) != 0) continue; } 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(prod, nprod); source_list_free(tests, ntests); closedir(d); *out_files = NULL; return -2; } } closedir(d); if (nprod > 1) qsort(prod, (size_t)nprod, sizeof *prod, strs_cmp); if (ntests > 1) qsort(tests, (size_t)ntests, sizeof *tests, strs_cmp); int total = nprod + ntests; char **all = total ? malloc((size_t)total * sizeof *all) : NULL; if (total && all == NULL) { 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. */ #define SEP_MAXPKG 256 struct seppkg { char path[512]; /* compiler/import identity; derived from import_base */ char import_base[SEP_IMPORT_PATH_MAX]; /* canonical directory import identity */ char entry[1024]; /* resolved package dir (or file, for a file root) */ char canon[1024]; /* canonical location; never package identity */ char artifact[512]; /* stable non-importable variant artifact key */ char name[256]; /* validated declared name; directory packages only */ char test_package[256]; /* selected test package; root variants only */ char **sources; /* owned, byte-sorted selected paths; dirs only */ int nsources; int is_dir; int variant; /* SEP_VARIANT_*; dependencies are production */ int role; /* normal, reserved test support, or generated main */ int root; /* requested usage; never package-action identity */ int link_entry; /* package supplies the executable's bare main */ int generated_main; /* compiler-owned generated test-main package */ int failed; /* discovery/compile failure reaches this action */ int test_support; /* compiler-generated -T support package */ int loaded; /* directory membership/name loaded exactly once */ int export_changed; /* staged export differs from committed export */ int emit_context; /* first verified resolution context */ unsigned char context_state[SEP_MAXCONTEXT]; /* 0 new, 1 active, 2 checked */ struct ImportSet bindings; /* first context's canonical import bindings */ int deps[SEP_MAXPKG]; /* direct-dep indices into sepgraph.pkg */ int ndeps; int color; /* tri-color DFS: 0 white, 1 gray, 2 black */ }; struct sepcontext { char root[1024]; /* selected entry directory; diagnostic identity */ char searchpath[8192]; /* root : explicit -I roots : toolchain source */ }; struct sepgraph { struct seppkg pkg[SEP_MAXPKG]; int n; struct sepcontext context[SEP_MAXCONTEXT]; int ncontext; int support_context; int identity_failed; /* command-global canonical identity collision */ }; struct sepproduct { const char *dir; const char *out; const char *identity; /* explicit canonical lookup identity, if any */ const char *test_package; const char *status; char artifact[64]; int variant; int context; int root; int variant_root; /* production-plus-test or external test package */ }; #define SEP_MAXLFLAGS 32 #define SEP_ARTIFACT_MAX 1024 struct seplinkflags { const char *libdirs[SEP_MAXLFLAGS]; int nlibdirs; const char *libs[SEP_MAXLFLAGS]; int nlibs; }; static int sep_directory_variant(int variant) { return variant == SEP_VARIANT_PRODUCTION || variant == SEP_VARIANT_SAME_TEST || variant == SEP_VARIANT_EXTERNAL; } static int sep_variant_path(int variant, const char *base, char *out, size_t outsz) { int n; if (variant == SEP_VARIANT_EXTERNAL) n = snprintf(out, outsz, "%s_test", base); else n = snprintf(out, outsz, "%s", base); return n >= 0 && (size_t)n < outsz ? 0 : -1; } 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; } /* 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' || strlen(base) >= SEP_IMPORT_PATH_MAX) { fprintf(stderr, "ww: package path is too long (limit %d bytes)\n", SEP_IMPORT_PATH_MAX - 1); 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[0] != '\0') { if (strcmp(p->import_base, base) == 0) return 0; g->identity_failed = 1; sep_diag_directory_identities(p->entry, p->import_base, base); return -1; } char path[sizeof p->path]; if (sep_variant_path(p->variant, base, path, sizeof path) < 0) { fprintf(stderr, "ww: package variant path is too long\n"); 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[0] != '\0' && strcmp(g->pkg[i].import_base, base) == 0) { g->identity_failed = 1; sep_diag_path_locations(base, g->pkg[i].entry, p->entry); return -1; } if (same_location && !support_alias && g->pkg[i].import_base[0] != '\0' && strcmp(g->pkg[i].import_base, base) != 0) { g->identity_failed = 1; sep_diag_directory_identities(p->entry, g->pkg[i].import_base, base); return -1; } if (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); 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); return -1; } } } snprintf(p->import_base, sizeof p->import_base, "%s", base); snprintf(p->path, sizeof p->path, "%s", 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) { if (path != NULL && strlen(path) >= SEP_IMPORT_PATH_MAX) { fprintf(stderr, "ww: package path is too long (limit %d bytes)\n", SEP_IMPORT_PATH_MAX - 1); return -1; } char *canon = realpath(entry, NULL); if (canon == NULL) { fprintf(stderr, "ww: cannot canonicalize package %s\n", entry); return -1; } if (strlen(canon) >= sizeof g->pkg[0].canon) { fprintf(stderr, "ww: canonical package path is too long\n"); free(canon); return -1; } const char *base = path ? path : ""; char incoming_path[sizeof g->pkg[0].path]; incoming_path[0] = '\0'; if (is_dir && base[0] != '\0' && sep_variant_path(variant, base, incoming_path, sizeof incoming_path) < 0) { fprintf(stderr, "ww: package variant path is too long\n"); 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[0] != '\0' && strcmp(base, q->import_base) == 0) { g->identity_failed = 1; sep_diag_path_locations(base, q->entry, entry); free(canon); return -1; } if (incoming_path[0] != '\0' && 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(canon); return -1; } if (same_location && q->variant == variant && q->role == role) { if (variant != SEP_VARIANT_PRODUCTION && strcmp(q->test_package, test_package ? test_package : "") != 0) { fprintf(stderr, "ww: incompatible package-test roots %s\n", entry); free(canon); return -1; } if (base[0] != '\0' && sep_bind_import_base(g, i, base) < 0) { free(canon); return -1; } q->root = q->root || root; free(canon); return i; } if (same_location) { if (support_alias) continue; if (q->import_base[0] != '\0' && base[0] != '\0' && strcmp(q->import_base, base) != 0) { g->identity_failed = 1; sep_diag_directory_identities(entry, q->import_base, base); 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(canon); return -1; } continue; } if (same_location && strcmp(q->path, base) == 0 && q->variant == variant && q->role == role) { q->root = q->root || root; free(canon); return i; } } if (g->n >= SEP_MAXPKG) { fprintf(stderr, "ww: too many packages (limit %d)\n", SEP_MAXPKG); free(canon); return -1; } int ni = g->n++; struct seppkg *p = &g->pkg[ni]; memset(p, 0, sizeof *p); snprintf(p->entry, sizeof p->entry, "%s", entry); snprintf(p->canon, sizeof p->canon, "%s", canon); free(canon); p->is_dir = is_dir; p->variant = variant; p->role = role; p->root = root; p->emit_context = -1; if (test_package != NULL) snprintf(p->test_package, sizeof p->test_package, "%s", test_package); 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[0] != '\0') { inherited = g->pkg[i].import_base; break; } if (inherited[0] != '\0' && sep_bind_import_base(g, ni, inherited) < 0) { g->n--; return -1; } } else { snprintf(p->path, sizeof p->path, "%s", base); if (artifact != NULL) snprintf(p->artifact, sizeof p->artifact, "%s", artifact); } 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); } /* 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++) { for (int j = 0; j < g->pkg[i].nsources; j++) free(g->pkg[i].sources[j]); free(g->pkg[i].sources); for (int j = 0; j < g->pkg[i].bindings.n; j++) free(g->pkg[i].bindings.paths[j]); free(g->pkg[i].bindings.paths); } free(g); } /* One selected directory owns one import-resolution context. Same/external * variants of that directory share it; different directories never acquire * precedence merely from their request order. */ static int sep_context_for(struct sepgraph *g, const char *root, const char *extra_includes, const char *toolsrcdir) { char searchpath[8192]; int n; if (extra_includes != NULL && extra_includes[0] != '\0') n = snprintf(searchpath, sizeof searchpath, "%s:%s:%s", root, extra_includes, toolsrcdir); else n = snprintf(searchpath, sizeof searchpath, "%s:%s", root, toolsrcdir); if (n < 0 || (size_t)n >= sizeof searchpath) { fprintf(stderr, "ww: package import search path is too long\n"); return -1; } for (int i = 0; i < g->ncontext; i++) if (strcmp(g->context[i].searchpath, searchpath) == 0) return i; if (g->ncontext >= SEP_MAXCONTEXT) { fprintf(stderr, "ww: too many package import contexts\n"); return -1; } struct sepcontext *c = &g->context[g->ncontext]; if (snprintf(c->root, sizeof c->root, "%s", root) >= (int)sizeof c->root) { fprintf(stderr, "ww: package root path is too long\n"); return -1; } snprintf(c->searchpath, sizeof c->searchpath, "%s", searchpath); return g->ncontext++; } /* Dots stay (legal in filenames). Product roots have distinct artifact names * even though each compiler unit resets to the bare executable namespace. */ static void sep_fname(const struct sepgraph *g, int pi, const char *scratch, const char *suffix, char *out, size_t outsz) { const char *base = g->pkg[pi].path; if (g->pkg[pi].artifact[0] != '\0') { base = g->pkg[pi].artifact; } else if (g->pkg[pi].root) { if (base[0] == '\0') base = "__root"; } else if (base[0] == '\0') { base = "__root"; } snprintf(out, outsz, "%s/%s%s", scratch, base, suffix); } /* All later artifact construction uses fixed SEP_ARTIFACT_MAX buffers. Check * the longest suffix once, before any file is opened, so truncation can never * collapse two command-owned action identities onto one path. */ static int sep_validate_artifact_paths(const struct sepgraph *g, const char *scratch) { for (int i = 0; i < g->n; i++) { if (g->pkg[i].failed || !g->pkg[i].loaded) continue; const char *base = g->pkg[i].artifact[0] != '\0' ? g->pkg[i].artifact : (g->pkg[i].path[0] != '\0' ? g->pkg[i].path : "__root"); size_t need = strlen(scratch) + 1 + strlen(base) + strlen(".unit.new") + 1; if (need > SEP_ARTIFACT_MAX) { fprintf(stderr, "ww: package artifact path is too long\n"); return -1; } for (int j = i + 1; j < g->n; j++) { if (g->pkg[j].failed || !g->pkg[j].loaded) continue; const char *other = g->pkg[j].artifact[0] != '\0' ? g->pkg[j].artifact : (g->pkg[j].path[0] != '\0' ? g->pkg[j].path : "__root"); if (strcmp(base, other) == 0) { fprintf(stderr, "ww: package actions share artifact identity %s\n", base); 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) { 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->usepath ? x->usepath : x->str; const char *yp = y->usepath ? y->usepath : y->str; int r = strcmp(xp, yp); 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 *path, int leaf_only) { if (pkg->variant != SEP_VARIANT_EXTERNAL || pkg->test_package[0] == '\0') return 0; const char *name = path; if (leaf_only) { const char *dot = strrchr(path, '.'); if (dot != NULL) name = dot + 1; } 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; } /* Canonical bindings make a shared package independent of which selected * root reaches it first. Directory bindings and the raw-file inline * compatibility binding are part of the package action's source meaning. */ static int sep_binding_add(struct ImportSet *bindings, char kind, const char *name, const char *target) { size_t nn = strlen(name), tn = target ? strlen(target) : 0; char *binding = malloc(nn + tn + 4); if (binding == NULL) return -1; binding[0] = kind; binding[1] = ':'; memcpy(binding + 2, name, nn); binding[2 + nn] = ':'; if (target != NULL) memcpy(binding + 3 + nn, target, tn); binding[3 + nn + tn] = '\0'; if (import_seen(bindings, binding)) { free(binding); return 0; } if (bindings->n == bindings->cap) { int cap = bindings->cap ? bindings->cap * 2 : 8; char **paths = realloc(bindings->paths, (size_t)cap * sizeof *paths); if (paths == NULL) { free(binding); return -1; } bindings->paths = paths; bindings->cap = cap; } bindings->paths[bindings->n++] = binding; return 0; } /* 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, const char *searchpath, struct ImportSet *filevisit, struct ImportSet *bindings, int owned_source) { if (import_seen(filevisit, file)) return 0; import_add(filevisit, file); 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[0] == '\0')) { const char *declared = imports->module; if (strlen(declared) >= sizeof g->pkg[pi].name) { errorf(imports->pos, "package name is too long"); freearena(a); free(buf); return -1; } struct seppkg *pkg = &g->pkg[pi]; if (pkg->name[0] == '\0') snprintf(pkg->name, sizeof pkg->name, "%s", declared); 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) nuse++; Node **uses = nuse ? malloc((size_t)nuse * sizeof *uses) : NULL; if (nuse && uses == NULL) { 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; const char *previous = NULL; for (int i = 0; i < nuse && rc == 0; i++) { Node *u = uses[i]; const char *name = u->usepath ? u->usepath : u->str; if (previous && strcmp(previous, name) == 0) continue; previous = name; if (reserved_import_path(name)) { errorf(u->pos, "package path %s is reserved", name); rc = -1; break; } if (strlen(name) >= sizeof g->pkg[0].path) { errorf(u->pos, "import path is too long (limit %zu bytes)", sizeof g->pkg[0].path - 1); rc = -1; break; } char path_form[1024]; import_path_form(name, path_form, sizeof path_form); if (strlen(name) + 1 > sizeof path_form) { errorf(u->pos, "import path is too long"); rc = -1; break; } char ipath[1024]; int external_production = 0; /* A selected external logical root is already an exact resolved * path/directory pair. Its source import of that same full ordinary * identity reuses the colocated production action. No declaration leaf * or unrelated cached package can override normal context lookup. */ const char *bound = g->pkg[pi].variant == SEP_VARIANT_EXTERNAL && g->pkg[pi].import_base[0] != '\0' && strcmp(g->pkg[pi].import_base, name) == 0 ? g->pkg[pi].canon : NULL; int located = bound != NULL; if (located) snprintf(ipath, sizeof ipath, "%s", bound); else located = locate_import(searchpath, path_form, ipath, sizeof ipath); if (!located) { const char *dot = strrchr(name, '.'); const char *leaf = dot ? dot + 1 : name; int inline_package = 0; if (!g->pkg[pi].is_dir) for (Node *package = imports->body; package; package = package->next) if (strcmp(package->module, leaf) == 0) { inline_package = 1; break; } if (inline_package) { if (sep_binding_add(bindings, 'I', name, NULL) < 0) rc = -1; continue; } errorf(u->pos, "cannot find package %s", name); rc = -1; break; } { char *canon = realpath(ipath, NULL); if (canon == NULL) { errorf(u->pos, "cannot canonicalize package '%s'", name); rc = -1; break; } if (sep_binding_add(bindings, 'D', name, canon) < 0) { free(canon); rc = -1; break; } int self = strcmp(canon, g->pkg[pi].canon) == 0; free(canon); if (self && sep_external_production_name(&g->pkg[pi], name, 1)) 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)"); rc = -1; break; } /* The external package's self-production import is the ordinary * canonical production action for this directory. Discovery role and * the external product's artifact name never create another action. */ int di = sep_find_or_add(g, name, ipath, 1); if (di < 0) { rc = -1; break; } int seen = 0; for (int k = 0; k < g->pkg[pi].ndeps; k++) if (g->pkg[pi].deps[k] == di) { seen = 1; break; } if (!seen) { if (g->pkg[pi].ndeps >= SEP_MAXPKG) { rc = -1; break; } g->pkg[pi].deps[g->pkg[pi].ndeps++] = di; } } } free(uses); freearena(a); free(buf); return rc; } static void sep_import_set_free(struct ImportSet *s) { for (int i = 0; i < s->n; i++) free(s->paths[i]); free(s->paths); s->paths = NULL; s->n = s->cap = 0; } static int sep_dep_cmp(const struct sepgraph *g, int a, int b) { int r = strcmp(g->pkg[a].path, g->pkg[b].path); if (r != 0) return r; if (g->pkg[a].variant != g->pkg[b].variant) return g->pkg[a].variant - g->pkg[b].variant; if (g->pkg[a].role != g->pkg[b].role) return g->pkg[a].role - g->pkg[b].role; return strcmp(g->pkg[a].canon, g->pkg[b].canon); } /* Add the compiler-owned test main as a real package action. Its identity is a * pure function of the selected variant (and its one command-global support * edge), never a product ordinal. Equivalent products therefore reuse it. */ static int sep_add_generated_main(struct sepgraph *g, struct sepproduct *product, int ordinal, int support) { (void)ordinal; int variant = product->variant_root; if (variant < 0 || variant >= g->n) return -1; const char *kind = "production"; if (g->pkg[variant].variant == SEP_VARIANT_SAME_TEST) kind = "internal"; else if (g->pkg[variant].variant == SEP_VARIANT_EXTERNAL) kind = "external"; char path[sizeof g->pkg[0].path]; char artifact[sizeof g->pkg[0].artifact]; char canon[sizeof g->pkg[0].canon]; char entry[sizeof g->pkg[0].entry]; snprintf(entry, sizeof entry, "%s", g->pkg[variant].entry); const char *variant_artifact = g->pkg[variant].artifact[0] ? g->pkg[variant].artifact : g->pkg[variant].path; int pn = snprintf(path, sizeof path, "__wwtestmain.%s.%s.main", g->pkg[variant].path, kind); int an = snprintf(artifact, sizeof artifact, "%s-main", variant_artifact); int cn = snprintf(canon, sizeof canon, "%s#%s-test-main", g->pkg[variant].canon, kind); if (pn < 0 || (size_t)pn >= sizeof path || an < 0 || (size_t)an >= sizeof artifact || cn < 0 || (size_t)cn >= sizeof canon) { fprintf(stderr, "ww: generated test-main identity is too long\n"); return -1; } for (int i = 0; i < g->n; i++) { if (strcmp(g->pkg[i].path, path) != 0) continue; if (g->pkg[i].generated_main) { int wants_support = support >= 0 && support != variant; int has_variant = 0, has_support = 0; for (int k = 0; k < g->pkg[i].ndeps; k++) { if (g->pkg[i].deps[k] == variant) has_variant = 1; if (wants_support && g->pkg[i].deps[k] == support) has_support = 1; } if (has_variant && has_support == wants_support && g->pkg[i].ndeps == 1 + wants_support) return i; } fprintf(stderr, "ww: generated test-main package identity collides with source import %s\n", path); return -1; } if (g->n >= SEP_MAXPKG) { fprintf(stderr, "ww: too many packages (limit %d)\n", SEP_MAXPKG); return -1; } struct seppkg *p = &g->pkg[g->n]; memset(p, 0, sizeof *p); snprintf(p->path, sizeof p->path, "%s", path); snprintf(p->artifact, sizeof p->artifact, "%s", artifact); snprintf(p->canon, sizeof p->canon, "%s", canon); snprintf(p->entry, sizeof p->entry, "%s", entry); snprintf(p->name, sizeof p->name, "main"); 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; p->context_state[product->context] = 2; p->deps[p->ndeps++] = variant; if (support >= 0 && support != variant) p->deps[p->ndeps++] = support; 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++; } /* 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 (context < 0 || context >= g->ncontext) return -1; if (g->pkg[pi].test_support && g->support_context >= 0) context = g->support_context; if (g->pkg[pi].context_state[context] == 2) return g->pkg[pi].failed ? -1 : 0; if (g->pkg[pi].context_state[context] == 1) return 0; g->pkg[pi].context_state[context] = 1; const char *searchpath = g->context[context].searchpath; struct ImportSet fv = {0}, 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[0] ? g->pkg[pi].test_package : NULL; g->pkg[pi].nsources = enumerate_dir_ww(g->pkg[pi].entry, g->pkg[pi].variant, test_package, &g->pkg[pi].sources); if (g->pkg[pi].nsources == -2) { rc = -1; /* diagnosed in enumerate_dir_ww */ } else if (g->pkg[pi].nsources < 0) { fprintf(stderr, "ww: cannot read directory %s\n", g->pkg[pi].entry); rc = -1; } else if (g->pkg[pi].nsources == 0) { fprintf(stderr, "ww: %s: directory contains no WW package sources\n", g->pkg[pi].entry); rc = -1; } } } if (g->pkg[pi].is_dir) { for (int i = 0; i < g->pkg[pi].nsources && rc == 0; i++) rc = sep_scan_file(g, pi, g->pkg[pi].sources[i], searchpath, &fv, &bindings, 1); if (rc == 0 && g->pkg[pi].path[0] != '\0' && !g->pkg[pi].test_support) { const char *dot = strrchr(g->pkg[pi].path, '.'); const char *leaf = dot ? dot + 1 : g->pkg[pi].path; if (strcmp(g->pkg[pi].name, leaf) != 0) { fprintf(stderr, "ww: package %s does not match import path %s\n", g->pkg[pi].name, g->pkg[pi].path); rc = -1; } } } else if (rc == 0) { rc = sep_scan_file(g, pi, g->pkg[pi].entry, searchpath, &fv, &bindings, 0); } sep_import_set_free(&fv); if (bindings.n > 1) qsort(bindings.paths, (size_t)bindings.n, sizeof *bindings.paths, strs_cmp); if (rc == 0 && g->pkg[pi].emit_context < 0) { g->pkg[pi].bindings = bindings; bindings.paths = NULL; bindings.n = bindings.cap = 0; g->pkg[pi].emit_context = context; } else if (rc == 0) { struct ImportSet *want = &g->pkg[pi].bindings; if (want->n != bindings.n) rc = -1; for (int i = 0; i < want->n && rc == 0; i++) if (strcmp(want->paths[i], bindings.paths[i]) != 0) rc = -1; if (rc < 0) { 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, g->context[g->pkg[pi].emit_context].root, g->context[context].root); } } sep_import_set_free(&bindings); if (rc < 0) { g->pkg[pi].context_state[context] = 2; g->pkg[pi].failed = 1; return -1; } 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; for (int k = 0; k < g->pkg[pi].ndeps; k++) if (sep_load_pkg(g, g->pkg[pi].deps[k], context) < 0) { g->pkg[pi].failed = 1; return -1; } return 0; } 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 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 -1; memcpy(root, p, n); root[n] = '\0'; char *canon = n == 0 ? NULL : realpath(root, NULL); free(root); 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) { fprintf(stderr, "ww: package path is too long (limit %d bytes)\n", SEP_IMPORT_PATH_MAX - 1); free(canon); return -1; } if (ir > 0 && reserved_import_path(out)) ir = 0; if (ir > 0) { char located[1024]; if (locate_import(searchpath, rel, located, sizeof located)) { char *selected = realpath(located, NULL); int same = selected != NULL && strcmp(selected, pkg->canon) == 0; free(selected); if (same) { free(canon); return 1; } } } } free(canon); } if (!e) break; p = e + 1; } return 0; } static int sep_ordinary_declared_name(const struct seppkg *p, char *out, size_t outsz) { if (p->name[0] == '\0') return -1; size_t n = strlen(p->name); if (p->variant == SEP_VARIANT_EXTERNAL) { if (n <= 5 || strcmp(p->name + n - 5, "_test") != 0) { fprintf(stderr, "ww: package-test selector does not name an external package\n"); return -1; } n -= 5; } if (n + 1 > outsz) return -1; memcpy(out, p->name, n); out[n] = '\0'; return 0; } /* The reserved local namespace is reversible, so filesystem identity never * depends on a hash, request order, output name, or another selected package. */ static int sep_local_import_base(const struct seppkg *p, char *out, size_t outsz) { char leaf[sizeof p->name]; if (sep_ordinary_declared_name(p, leaf, sizeof leaf) < 0) return -1; size_t off = 0; int n = snprintf(out, outsz, "%s.p", SEP_LOCAL_IMPORT_PREFIX); if (n < 0 || (size_t)n >= outsz) return -1; 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) return -1; out[off++] = (char)c; } else if (c == '_' || c == '/') { if (off + 2 >= outsz) return -1; out[off++] = '_'; out[off++] = c == '_' ? 'u' : 's'; } else { if (off + 4 >= outsz) return -1; out[off++] = '_'; out[off++] = 'x'; out[off++] = hex[c >> 4]; out[off++] = hex[c & 15]; } } size_t ln = strlen(leaf); if (off + 1 + ln + 1 > outsz) return -1; out[off++] = '.'; memcpy(out + off, leaf, ln + 1); return 0; } /* 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[0] != '\0') continue; for (int ci = 0; ci < g->ncontext; ci++) { if (p->context_state[ci] != 2) continue; char candidate[SEP_IMPORT_PATH_MAX]; int found = sep_reverse_import_base(g, p, ci, candidate, sizeof candidate); if (found < 0) return -1; if (found > 0 && sep_bind_import_base(g, pi, candidate) < 0) return -1; } } 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[0] != '\0') 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[0] == '\0') continue; base = g->pkg[i].import_base; break; } char local[SEP_IMPORT_PATH_MAX]; if (base == NULL) { if (sep_local_import_base(p, local, sizeof local) < 0) { fprintf(stderr, "ww: local package identity is too long (limit %d bytes)\n", SEP_IMPORT_PATH_MAX - 1); return -1; } base = local; } if (sep_bind_import_base(g, pi, base) < 0) return -1; } 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; const char *dot = strrchr(p->path, '.'); const char *leaf = dot ? dot + 1 : p->path; int support_alias = p->role == SEP_ROLE_TEST_SUPPORT && strcmp(p->path, SEP_TEST_SUPPORT_MODULE) == 0 && strcmp(p->name, "test") == 0; if (!support_alias && strcmp(p->name, leaf) != 0) { fprintf(stderr, "ww: package %s does not match import path %s\n", p->name, p->path); return -1; } p->artifact[0] = '\0'; int n = 0; char action_path[sizeof p->path]; snprintf(action_path, sizeof action_path, "%s", p->path); if (p->variant == SEP_VARIANT_SAME_TEST) n = snprintf(p->artifact, sizeof p->artifact, "%s-internal-test", action_path); else if (p->variant == SEP_VARIANT_EXTERNAL) n = snprintf(p->artifact, sizeof p->artifact, "%s-external-test", action_path); if (n < 0 || (size_t)n >= sizeof p->artifact) { fprintf(stderr, "ww: package variant artifact identity is too long\n"); return -1; } } return 0; } /* DFS post-order over the dep DAG → reverse-topo (deps before importer), * cite Hare gather (deps.ha:123). Tri-color: a gray back-edge is a loud * dep-cycle reject naming the chain (Hare deps.ha:243); `stack[0..depth)` * is the live DFS path, so the cycle runs from pi's first occurrence on * it to the top, closing back on pi. */ static int sep_topo_visit(struct sepgraph *g, int pi, int *order, int *no, int *stack, int depth) { if (g->pkg[pi].color == 2) return 0; if (g->pkg[pi].color == 1) { int j = 0; while (j < depth && stack[j] != pi) j++; fprintf(stderr, "ww: dependency cycle: "); for (int s = j; s < depth; s++) fprintf(stderr, "%s -> ", g->pkg[stack[s]].path[0] ? g->pkg[stack[s]].path : "(root)"); fprintf(stderr, "%s\n", g->pkg[pi].path[0] ? g->pkg[pi].path : "(root)"); return -1; } g->pkg[pi].color = 1; stack[depth] = pi; for (int k = 0; k < g->pkg[pi].ndeps; k++) if (sep_topo_visit(g, g->pkg[pi].deps[k], order, no, stack, depth + 1) < 0) return -1; g->pkg[pi].color = 2; order[(*no)++] = pi; return 0; } /* Internal test variants replace their colocated production action in the * corresponding test link closure. Canonical package identity has already * made every remaining compiler qualifier globally unambiguous. */ static int sep_internal_replaces_production(const struct sepgraph *g, int a, int b) { const struct seppkg *internal = &g->pkg[a]; const struct seppkg *production = &g->pkg[b]; if (internal->variant != SEP_VARIANT_SAME_TEST) { internal = &g->pkg[b]; production = &g->pkg[a]; } return internal->variant == SEP_VARIANT_SAME_TEST && production->variant == SEP_VARIANT_PRODUCTION && production->role != SEP_ROLE_TEST_SUPPORT && strcmp(internal->canon, production->canon) == 0; } static int sep_validate_module_closure(struct sepgraph *g, const int *order, int n, int include_root) { for (int i = 0; i < n; i++) { int a = order[i]; if ((!include_root && g->pkg[a].root) || g->pkg[a].path[0] == '\0') continue; for (int j = i + 1; j < n; j++) { int b = order[j]; if (!include_root && g->pkg[b].root) continue; if (strcmp(g->pkg[a].path, g->pkg[b].path) == 0 && !sep_internal_replaces_production(g, a, b)) { fprintf(stderr, "ww: product closure contains multiple packages named %s\n", g->pkg[a].path); return -1; } } } return 0; } /* Emit one of pi's own source files into the sep-unit under the * //ww:module-reset primary boundary. No source or export outside pi's * sorted owned-source set may enter this unit. */ static int sep_emit_body(FILE *out, const char *path, const char *modpath) { char *buf; u64 len; if (sep_slurp(path, &buf, &len) < 0) { fprintf(stderr, "ww: cannot read %s\n", path); return -1; } /* #57: tag the primary body by its full dotted import path so the * definer mangles == the importer reference; a root build (path "") * stays a bare reset (keeps bare main). */ int bad = 0; if (modpath != NULL && modpath[0] != '\0') { if (fprintf(out, "//ww:module-reset %s\n", modpath) < 0) bad = 1; } else if (fputs("//ww:module-reset\n", out) == EOF) { bad = 1; } if (fwrite(buf, 1, (size_t)len, out) != (size_t)len || fputc('\n', out) == EOF) bad = 1; free(buf); if (bad) { fprintf(stderr, "ww: cannot write package unit\n"); return -1; } return 0; } /* 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 *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); } if (fclose(u) != 0) { fprintf(stderr, "ww: cannot close package unit %s\n", unitf); return -1; } return bodyrc; } /* archive_o — write a deterministic single-member SysV ar archive at * `apath` wrapping the object at `objpath`. 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, * one 60-byte member header, and the `.o` bytes (newline-padded to even). * Zeroed mtime/uid/gid + fixed mode + a fixed member name make the bytes * a pure function of the `.o` content → cstage `.a` == wwstage `.a` * (rule 10). The wwstage twin is archiveo (selfhost/cmd/ww/main.ww). */ static int archive_o(const char *objpath, const char *apath) { 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; } unsigned char *buf = malloc((size_t)n); if (buf == NULL) { fclose(in); return -1; } if (fread(buf, 1, (size_t)n, in) != (size_t)n) { free(buf); fclose(in); return -1; } if (fclose(in) != 0) { free(buf); return -1; } FILE *out = fopen(apath, "wb"); if (out == NULL) { fprintf(stderr, "ww: cannot open %s\n", apath); free(buf); return -1; } int bad = fwrite("!\n", 1, 8, out) != 8; /* 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, "pkg.o/", 6); /* GNU short-name '/' terminator */ 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 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 || fwrite(buf, 1, (size_t)n, out) != (size_t)n) bad = 1; if ((n & 1) && fputc('\n', out) == EOF) bad = 1; if (fclose(out) != 0) bad = 1; free(buf); 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 commit * via temp + rename with the unit renamed last, so a killed build can * never leave a committed unit vouching for uncommitted artifacts. The * caller serializes invocations per workdir (Make target = one workdir) * and `make clean` reclaims the state; the wwstage twin is the * fileequal/copyfileatomic/workdirstamp 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 stat(path, &st) == 0 && S_ISREG(st.st_mode); } static int file_size_nonzero(const char *path) { struct stat st; return stat(path, &st) == 0 && S_ISREG(st.st_mode) && st.st_size > 0; } /* 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[65536], bb[65536]; 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; } /* Replace dst with src's bytes via temp + rename, so a torn write can * never masquerade as a committed tool copy. */ static int copy_file_atomic(const char *src, const char *dst) { char tmp[1100]; snprintf(tmp, sizeof tmp, "%s.new", dst); FILE *in = fopen(src, "rb"); if (in == NULL) return -1; FILE *out = fopen(tmp, "wb"); if (out == NULL) { fclose(in); return -1; } static char buf[65536]; size_t n; while ((n = fread(buf, 1, sizeof buf, in)) > 0) if (fwrite(buf, 1, n, out) != n) { fclose(in); fclose(out); return -1; } int bad = ferror(in); fclose(in); if (fclose(out) != 0 || bad) return -1; return rename(tmp, dst); } /* A coordinator-private completion marker distinguishes a newly linked * product from a caller-owned binary left behind by an earlier invocation. */ static int record_product_status(const char *path) { if (path == NULL) return 0; char tmp[1100]; snprintf(tmp, sizeof tmp, "%s.new", path); FILE *f = fopen(tmp, "wb"); if (f == NULL) return -1; int bad = fputs("ok\n", f) == EOF; if (fclose(f) != 0) bad = 1; if (bad) return -1; return rename(tmp, path); } /* The stamp pins the non-content build inputs a unit compare cannot see: * the -T/-S 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 ? 9 : 8, is_test ? "test" : "build", emit_asm); } /* A stale global builder identity invalidates every committed unit voucher in * this driver-owned workdir before compilation starts. Artifacts may remain, * but without their unit they cannot be reused. That makes it safe to record * the new identity after a partial multi-root pass: successful actions have * current units, while failed and no-longer-requested actions do not. */ static int invalidate_workdir_units(const char *scratch) { DIR *d = opendir(scratch); if (d == NULL) return -1; struct dirent *de; int rc = 0; while ((de = readdir(d)) != NULL) { size_t n = strlen(de->d_name); if (n < 8 || strcmp(de->d_name + n - 8, ".unit.ww") != 0) continue; char path[SEP_ARTIFACT_MAX]; int pn = snprintf(path, sizeof path, "%s/%s", scratch, de->d_name); if (pn < 0 || (size_t)pn >= sizeof path || (unlink(path) != 0 && errno != ENOENT)) { rc = -1; break; } } if (closedir(d) != 0) rc = -1; if (rc != 0) fprintf(stderr, "ww: cannot invalidate stale package units\n"); return rc; } /* 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 package_only, int is_test, struct sepproduct *products, int nproducts, int emit_asm, const char *workdir, char *scratchout, size_t scratchoutsz, struct sepgraph **graphout) { if (nproducts < 1 || nproducts > SEP_MAXPRODUCT) return 1; for (int i = 0; i < nproducts; i++) if (products[i].status != NULL && unlink(products[i].status) != 0 && errno != ENOENT) 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[1024]; snprintf(libbuf, sizeof libbuf, "%s/../lib", self_dir); libdir = libbuf; } const char *srcdir = envpath("WW_SRCLIB"); static char srcbuf[1024]; if (srcdir == NULL) { snprintf(srcbuf, sizeof srcbuf, "%s/../../lib", self_dir); if (access(srcbuf, 0) == 0) srcdir = srcbuf; else if (access("lib", 0) == 0) srcdir = "lib"; else srcdir = libdir; } const char *toolsrcdir = srcdir; char srcd[1024]; if (entry_is_dir) { snprintf(srcd, sizeof srcd, "%s", src); 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[1024]; if (entry_is_dir) { const char *b = strrchr(srcd, '/'); const char *base = b ? b + 1 : srcd; snprintf(stem, sizeof stem, "%s/%s", srcd, base); } else { snprintf(stem, sizeof stem, "%s", src); char *dot = strrchr(stem, '.'); if (dot && strcmp(dot, ".ww") == 0) *dot = '\0'; } const char *ostem = (objstem && objstem[0]) ? objstem : stem; int warm = workdir != NULL && workdir[0] != 0; char scratch[1100]; if (warm) { struct stat wst; if (stat(workdir, &wst) != 0 || !S_ISDIR(wst.st_mode)) { 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. */ snprintf(scratch, sizeof scratch, "%s", workdir); } else { snprintf(scratch, sizeof scratch, "%s.sepwork", ostem); if (mkdir(scratch, 0755) != 0) { fprintf(stderr, "ww: cannot create scratch %s\n", scratch); return 1; } /* Hand the scratch path back only after mkdir succeeds. The * wrapper therefore never removes a pre-existing path that this * build failed to acquire. */ if (scratchout) snprintf(scratchout, scratchoutsz, "%s", scratch); } int stale_all = 0, stampok = 0; char toolw[1200] = {0}, toolc[1200] = {0}, toola[1200] = {0}; char stampf[1200] = {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; } snprintf(toolw, sizeof toolw, "%s/.wwtool.ww", scratch); snprintf(toolc, sizeof toolc, "%s/.wwtool.w6c", scratch); snprintf(toola, sizeof toola, "%s/.wwtool.w6a", scratch); snprintf(stampf, sizeof stampf, "%s/.wwtool.stamp", scratch); 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; if (stale_all && invalidate_workdir_units(scratch) != 0) return 1; } struct sepgraph *g = calloc(1, sizeof *g); if (g == NULL) return 1; g->support_context = -1; if (graphout) *graphout = g; int support_for[SEP_MAXPRODUCT]; for (int i = 0; i < nproducts; i++) support_for[i] = -1; for (int i = 0; i < nproducts; i++) { const char *entry = products[i].dir != NULL ? products[i].dir : src; char contextdir[1024]; 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; } products[i].context = sep_context_for(g, contextroot, extra_includes, toolsrcdir); if (products[i].context < 0) return 1; const char *selector = products[i].variant == SEP_VARIANT_PRODUCTION ? NULL : products[i].test_package; const char *rootpath = products[i].identity != NULL ? products[i].identity : ""; 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); if (products[i].root < 0) return 1; products[i].variant_root = products[i].root; if (!is_test && !package_only) g->pkg[products[i].root].link_entry = 1; } const char *test_support_module = "test"; /* -T generates a dispatcher whose support qualifier is selected by the * command. Represent that compiler-generated requirement as a direct edge * of the generated-main action. It normally coalesces with an explicit * toolchain `import test`; * when user source occupies that identity, the reserved graph alias keeps * it distinct. The linker receives the same support archive closure. */ if (is_test) { char tpath[1024]; int tdir = 0; if (locate_import(toolsrcdir, "test", tpath, sizeof tpath)) { tdir = 1; g->support_context = sep_context_for(g, toolsrcdir, NULL, toolsrcdir); if (g->support_context < 0) return 1; char *tc = realpath(tpath, NULL); int collision = 0; for (int i = 0; i < nproducts; i++) { int root_is_support = tc != NULL && strcmp(g->pkg[products[i].root].canon, tc) == 0; const char *name = products[i].test_package; if (!root_is_support && name != NULL && (strcmp(name, "test") == 0 || strcmp(name, "test_test") == 0)) collision = 1; } for (int i = 0; i < nproducts && !collision; i++) { char userpath[1024]; int userdir = 0; if (locate_import(g->context[products[i].context].searchpath, "test", userpath, sizeof userpath)) { userdir = 1; (void)userdir; char *uc = realpath(userpath, NULL); if (tc != NULL && uc != NULL && strcmp(tc, uc) != 0) collision = 1; free(uc); } } if (collision) test_support_module = SEP_TEST_SUPPORT_MODULE; for (int i = 0; i < nproducts; i++) { int root = products[i].root; int root_is_support = tc != NULL && strcmp(g->pkg[root].canon, tc) == 0; /* A same-test build of the runtime package already owns run * and its source imports. An external test still needs the * colocated production node, which is also its support dep. */ if (root_is_support && strcmp(test_support_module, "test") == 0 && products[i].variant != SEP_VARIANT_EXTERNAL) { support_for[i] = root; continue; } int ti; if (strcmp(test_support_module, SEP_TEST_SUPPORT_MODULE) == 0) ti = sep_find_or_add_role(g, test_support_module, tpath, tdir, SEP_ROLE_TEST_SUPPORT, NULL); else ti = sep_find_or_add(g, test_support_module, tpath, tdir); if (ti < 0) return 1; g->pkg[ti].test_support = 1; support_for[i] = ti; } free(tc); } } for (int i = 0; i < nproducts; i++) { int root = products[i].variant_root; /* Raw single-file test fixtures are the one retained non-directory * exception: keep compiler-owned test-main synthesis in that action. * Its support export is still an exact direct input. */ if (is_test && !entry_is_dir) { int support = support_for[i]; if (support >= 0 && support != root) { int seen = 0; for (int k = 0; k < g->pkg[root].ndeps; k++) if (g->pkg[root].deps[k] == support) seen = 1; if (!seen) g->pkg[root].deps[g->pkg[root].ndeps++] = support; } g->pkg[root].link_entry = 1; } if (sep_load_pkg(g, root, products[i].context) < 0) { g->pkg[root].failed = 1; continue; } if (products[i].test_package != NULL && strcmp(g->pkg[root].name, products[i].test_package) != 0) { fprintf(stderr, "ww: package-test selector does not match loaded package\n"); g->pkg[root].failed = 1; } } if (is_test && entry_is_dir) { for (int i = 0; i < nproducts; i++) { int variant = products[i].variant_root; int support = support_for[i]; if (support >= 0 && support != variant && sep_load_pkg(g, support, products[i].context) < 0) g->pkg[variant].failed = 1; } } if (g->identity_failed) return 1; if (sep_finalize_directory_identities(g) < 0) return 1; if (is_test && entry_is_dir) { for (int i = 0; i < nproducts; i++) { int variant = products[i].variant_root; int support = support_for[i]; if (g->pkg[variant].failed || (support >= 0 && g->pkg[support].failed)) { products[i].root = variant; continue; } int mainpkg = sep_add_generated_main(g, &products[i], i, support); if (mainpkg < 0) return 1; products[i].root = mainpkg; } } if (sep_validate_artifact_paths(g, scratch) < 0) return 1; int root_package = package_only; if (root_package && !g->pkg[products[0].root].failed && strcmp(g->pkg[products[0].root].name, "main") == 0) { fprintf(stderr, "ww: -p requires a non-main package\n"); return 1; } int *order = calloc((size_t)g->n, sizeof *order); int *stack = calloc((size_t)g->n, sizeof *stack); int norder = 0; if (order == NULL || stack == NULL) { free(stack); free(order); return 1; } /* Diagnose cycles per product before constructing the shared union. A * variant-local cycle must not suppress an independent sibling root. */ 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; if (sep_topo_visit(g, root, order, &ignored, stack, 0) < 0 || sep_validate_module_closure(g, order, ignored, 1) < 0) g->pkg[root].failed = 1; } for (int pi = 0; pi < g->n; pi++) g->pkg[pi].color = 0; for (int i = 0; i < nproducts; i++) { int root = products[i].root; if (!g->pkg[root].failed && sep_topo_visit(g, root, order, &norder, stack, 0) < 0) { free(stack); free(order); return 1; } } free(stack); 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]; 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); /* 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; if (sep_compose_unit(g, pi, cu) < 0) { 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; if (warm && !stale_all && !deps_changed && file_equal(unitnew, unitf) && file_is_reg(asmf) && file_is_reg(wwi) && (emit_asm || (file_size_nonzero(obj) && file_size_nonzero(apath)))) { if (unlink(unitnew) != 0) { fprintf(stderr, "ww: cannot remove %s\n", unitnew); g->pkg[pi].failed = 1; any_failed = 1; } continue; } size_t cargvcap = (size_t)(12 + 3 * g->pkg[pi].ndeps); 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"); free(importfiles); free(cargv); free(order); return 1; } 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; } else { if (g->pkg[pi].variant == SEP_VARIANT_SAME_TEST || g->pkg[pi].variant == SEP_VARIANT_EXTERNAL) cargv[cpos++] = "--test-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++] = "-c"; for (int k = 0; k < g->pkg[pi].ndeps; k++) { int dj = g->pkg[pi].deps[k]; sep_fname(g, dj, scratch, ".wwi", importfiles[k], sizeof importfiles[k]); cargv[cpos++] = "--import"; cargv[cpos++] = g->pkg[dj].path; cargv[cpos++] = importfiles[k]; } 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; 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; continue; } } /* Every package action, including executable and generated-test roots, * produces the existing deterministic single-member archive. */ if (!emit_asm) { if (archive_o(co, 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; continue; } } /* Commit order: artifacts before the unit that vouches for * them, unit strictly last. */ if (warm) { if (rename(wwinew, wwi) != 0 || rename(asmnew, asmf) != 0 || (!emit_asm && rename(objnew, obj) != 0) || (!emit_asm && rename(anew, apath) != 0) || rename(unitnew, unitf) != 0) { fprintf(stderr, "ww: cannot commit %s\n", g->pkg[pi].path[0] ? g->pkg[pi].path : "(root)"); g->pkg[pi].failed = 1; any_failed = 1; continue; } } } /* Stale passes removed every old unit voucher before compiling. Current * successful units are therefore safe to vouch for even when a sibling * root failed; a killed pass leaves the old identity and forces another * invalidating pass, never false reuse. */ if (warm) { if (!file_equal(toolw, self_path) && copy_file_atomic(self_path, toolw) != 0) { fprintf(stderr, "ww: cannot record %s\n", toolw); free(order); return 1; } if (!file_equal(toolc, c6) && copy_file_atomic(c6, toolc) != 0) { fprintf(stderr, "ww: cannot record %s\n", toolc); free(order); return 1; } if (!emit_asm && !file_equal(toola, a6) && copy_file_atomic(a6, toola) != 0) { fprintf(stderr, "ww: cannot record %s\n", toola); free(order); return 1; } if (!stampok) { char stampnew[1300]; snprintf(stampnew, sizeof stampnew, "%s.new", stampf); FILE *sf = fopen(stampnew, "wb"); int bad = sf == NULL || fputs(stampwant, sf) == EOF; if (sf != NULL && fclose(sf) != 0) bad = 1; if (bad || rename(stampnew, stampf) != 0) { fprintf(stderr, "ww: cannot record %s\n", stampf); free(order); return 1; } } } if (emit_asm) { free(order); return any_failed ? 1 : 0; } if (root_package) { int root = products[0].root; if (g->pkg[root].failed) { free(order); return 1; } char archive[SEP_ARTIFACT_MAX], iface[SEP_ARTIFACT_MAX]; char outiface[SEP_ARTIFACT_MAX]; sep_fname(g, root, scratch, ".a", archive, sizeof archive); sep_fname(g, root, scratch, ".wwi", iface, sizeof iface); snprintf(outiface, sizeof outiface, "%s.wwi", out); if (copy_file_atomic(archive, out) != 0 || copy_file_atomic(iface, outiface) != 0) { fprintf(stderr, "ww: cannot write package artifact %s\n", out); free(order); return 1; } free(order); return 0; } free(order); /* Each product gets its own reverse-topological archive closure: root `.a` * first, then every transitively reachable package `.a`, then libwwrt.a. An * internal test variant already contains production sources, so its * colocated production archive is omitted without dropping dependencies. */ char rtpaths[2][1024]; int nrt = 1; snprintf(rtpaths[0], sizeof rtpaths[0], "%s/libwwrt.a", libdir); if (access(rtpaths[0], 0) != 0) { nrt = 2; snprintf(rtpaths[0], sizeof rtpaths[0], "%s/../obj/rt/start.o", self_dir); snprintf(rtpaths[1], sizeof rtpaths[1], "%s/../obj/rt/syscall.o", self_dir); } int nlibdirs = linkflags ? linkflags->nlibdirs : 0; int nlibs = linkflags ? linkflags->nlibs : 0; for (int i = 0; i < nproducts; i++) { int root = products[i].root; int variant_root = products[i].variant_root; if (g->pkg[root].failed) { any_failed = 1; continue; } 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 || sep_topo_visit(g, root, linkorder, &nlink, linkstack, 0) < 0) { free(linkstack); free(linkorder); return 1; } free(linkstack); size_t largvcap = (size_t)(3 + nlink + nrt + 2 * nlibdirs + 2 * nlibs + 1); char **largv = calloc(largvcap, sizeof *largv); char (*linkpaths)[SEP_ARTIFACT_MAX] = calloc((size_t)nlink, sizeof *linkpaths); if (largv == NULL || linkpaths == NULL) { free(linkpaths); free(largv); free(linkorder); return 1; } int pos = 0, npath = 0; largv[pos++] = "w6l"; largv[pos++] = "-o"; largv[pos++] = (char *)products[i].out; for (int oi = nlink - 1; oi >= 0; oi--) { int pi = linkorder[oi]; if (variant_root >= 0 && g->pkg[variant_root].variant == SEP_VARIANT_SAME_TEST && pi != variant_root && g->pkg[pi].variant == SEP_VARIANT_PRODUCTION && g->pkg[pi].role != SEP_ROLE_TEST_SUPPORT && strcmp(g->pkg[pi].canon, g->pkg[variant_root].canon) == 0) continue; sep_fname(g, pi, scratch, ".a", 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; any_failed = 1; continue; } if (record_product_status(products[i].status) != 0) { fprintf(stderr, "ww: cannot record package-test product\n"); g->pkg[root].failed = 1; any_failed = 1; } } return any_failed ? 1 : 0; } /* 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 package_only, int is_test, int root_variant, const char *test_package, int emit_asm, int keepscratch, const char *workdir) { char scratch[1100] = {0}; struct sepgraph *g = NULL; struct sepproduct product = { .dir = src, .out = out, .identity = root_identity, .test_package = test_package, .status = NULL, .artifact = {0}, .variant = root_variant, .root = -1, .variant_root = -1, }; if (!package_only && !entry_is_dir) snprintf(product.artifact, sizeof product.artifact, "__root"); int r = build_one_sep_impl(src, entry_is_dir, out, objstem, extra_includes, linkflags, package_only, is_test, &product, 1, emit_asm, workdir, 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) { char scratch[1100] = {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, NULL, 0, 1, products, nproducts, 0, workdir, 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 const char * search_path(const char *extra, char *buf, size_t bufsz) { const char *libdir = envpath("WW_SRCLIB"); static char libbuf[1024]; if (libdir == NULL) { libdir = envpath("WW_LIB"); } if (libdir == NULL) { snprintf(libbuf, sizeof libbuf, "%s/../../lib", self_dir); if (access(libbuf, 0) == 0) libdir = libbuf; else if (access("lib", 0) == 0) libdir = "lib"; else { snprintf(libbuf, sizeof libbuf, "%s/../lib", self_dir); libdir = libbuf; } } if (extra && extra[0]) snprintf(buf, bufsz, ".:%s:%s", extra, libdir); else snprintf(buf, bufsz, ".:%s", libdir); return buf; } static void basename_no_ext(const char *path, char *out, size_t outsz) { const char *base = strrchr(path, '/'); base = base ? base + 1 : path; snprintf(out, outsz, "%s", base); char *dot = strrchr(out, '.'); if (dot && strcmp(dot, ".ww") == 0) *dot = '\0'; } static int resolve_module(const char *name, const char *incs, char *out, size_t outsz, int *is_dir) { struct stat st; if (stat(name, &st) == 0) { if (S_ISREG(st.st_mode)) { snprintf(out, outsz, "%s", name); *is_dir = 0; return 1; } if (S_ISDIR(st.st_mode)) { snprintf(out, outsz, "%s", name); *is_dir = 1; return 1; } } if (reserved_import_path(name)) return 0; char sp[4096]; search_path(incs, sp, sizeof sp); char path_form[256]; import_path_form(name, path_form, sizeof path_form); return locate_module(sp, path_form, out, outsz, is_dir); } /* 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 *package_out) { *src_out = NULL; if (emit_asm_out) *emit_asm_out = 0; if (package_out) *package_out = 0; int i = 0; for (; i < argc; i++) { 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], "-p") == 0) { if (package_out == NULL) { fprintf(stderr, "ww %s: unknown flag\n", cmd); return -1; } *package_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; } snprintf(workdir, workdirsz, "%s", argv[++i]); } else if (strncmp(argv[i], "-w", 2) == 0 && argv[i][2]) { if (workdir == NULL) { fprintf(stderr, "ww %s: unknown flag\n", cmd); return -1; } snprintf(workdir, workdirsz, "%s", argv[i] + 2); } 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; } size_t n = strlen(incs); snprintf(incs + n, incsz - n, "%s%s", n ? ":" : "", argv[++i]); } else if (strncmp(argv[i], "-I", 2) == 0 && argv[i][2]) { size_t n = strlen(incs); snprintf(incs + n, incsz - n, "%s%s", n ? ":" : "", argv[i] + 2); } else if (strcmp(argv[i], "-o") == 0) { if (i + 1 >= argc) { fprintf(stderr, "ww %s: -o needs an argument\n", cmd); return -1; } snprintf(outpath, outsz, "%s", argv[++i]); } else if (strncmp(argv[i], "-o", 2) == 0 && argv[i][2]) { snprintf(outpath, outsz, "%s", argv[i] + 2); } else if (argv[i][0] == '-') { fprintf(stderr, "ww %s: unknown flag\n", cmd); return -1; } else if (*src_out == NULL) { *src_out = argv[i]; } 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}; char incs[2048] = {0}; char outflag[1024] = {0}; char workdir[1024] = {0}; int emit_asm = 0; int package_only = 0; if (parse_build_flags("build", argc, argv, incs, sizeof incs, &linkflags, outflag, sizeof outflag, workdir, sizeof workdir, &src, &emit_asm, &package_only) < 0) return 2; if (src == NULL) src = "."; if (package_only && emit_asm) { fprintf(stderr, "ww build: -p and -S cannot be combined\n"); return 2; } struct stat requested; int literal = stat(src, &requested) == 0; char resolved[1024]; int is_dir = 0; if (!resolve_module(src, incs, resolved, sizeof resolved, &is_dir)) { fprintf(stderr, "ww build: cannot find module %s\n", src); return 1; } if (package_only && !is_dir) { fprintf(stderr, "ww build: -p needs a package directory\n"); return 2; } char out[1024]; const char *objstem = NULL; if (outflag[0]) { /* -o sets both the binary path and the intermediate stem so * artifacts land beside the requested output (T3). */ snprintf(out, sizeof out, "%s", outflag); objstem = out; } else if (is_dir) { char tmp[1024]; snprintf(tmp, sizeof tmp, "%s", resolved); size_t n = strlen(tmp); while (n > 1 && tmp[n-1] == '/') tmp[--n] = '\0'; const char *b = strrchr(tmp, '/'); snprintf(out, sizeof out, "%s", b ? b + 1 : tmp); } else { basename_no_ext(resolved, out, sizeof out); } const char *root_identity = !literal && is_dir ? src : NULL; return build_one_sep(resolved, is_dir, root_identity, out, objstem, incs, &linkflags, package_only, 0, SEP_VARIANT_PRODUCTION, NULL, emit_asm, 1, workdir); } static int do_run(int argc, char **argv) { const char *src = NULL; struct seplinkflags linkflags = {0}; char incs[2048] = {0}; char outflag[1024] = {0}; /* -o accepted+ignored: run always uses the temp */ int next = parse_build_flags("run", argc, argv, incs, sizeof incs, &linkflags, outflag, sizeof outflag, NULL, 0, &src, NULL, NULL); if (next < 0) return 2; if (src == NULL) src = "."; struct stat requested; int literal = stat(src, &requested) == 0; char resolved[1024]; int is_dir = 0; if (!resolve_module(src, incs, resolved, sizeof resolved, &is_dir)) { fprintf(stderr, "ww run: cannot find module %s\n", src); return 1; } char tmpdir[1024], tmp[1024]; snprintf(tmpdir, sizeof tmpdir, "/tmp/ww_run_%d", getpid()); if (mkdir(tmpdir, 0700) != 0) { fprintf(stderr, "ww: cannot create temporary directory %s\n", tmpdir); return 1; } snprintf(tmp, sizeof tmp, "%s/main", tmpdir); /* 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; if (build_one_sep(resolved, is_dir, root_identity, tmp, tmp, incs, &linkflags, 0, 0, SEP_VARIANT_PRODUCTION, NULL, 0, 0, NULL) != 0) { if (unlink(tmp) != 0 && errno != ENOENT) fputs("ww: cannot remove temporary output\n", stderr); if (rmdir(tmpdir) != 0) fputs("ww: cannot remove temporary directory\n", stderr); return 1; } pid_t pid = fork(); if (pid < 0) { perror("ww: fork"); if (unlink(tmp) != 0 && errno != ENOENT) fputs("ww: cannot remove temporary output\n", stderr); if (rmdir(tmpdir) != 0) fputs("ww: cannot remove temporary directory\n", stderr); return 1; } if (pid == 0) { int n_extra = argc - next; char **xargv = calloc((size_t)n_extra + 2, sizeof *xargv); xargv[0] = tmp; for (int i = 0; i < n_extra; i++) xargv[i+1] = argv[next + i]; xargv[n_extra+1] = NULL; execv(tmp, xargv); perror("ww: exec"); _exit(127); } int status = 0; pid_t got; do { got = waitpid(pid, &status, 0); } while (got < 0 && errno == EINTR); int rc = got == pid && WIFEXITED(status) ? WEXITSTATUS(status) : 1; if (got != pid) perror("ww: waitpid"); if (unlink(tmp) != 0 && errno != ENOENT) { fputs("ww: cannot remove temporary output\n", stderr); if (rc == 0) rc = 1; } if (rmdir(tmpdir) != 0) { fputs("ww: cannot remove temporary directory\n", stderr); if (rc == 0) rc = 1; } return rc; } static int do_test(int argc, char **argv) { const char *src = NULL; struct sepproduct products[SEP_MAXPRODUCT]; int nproducts = 0; char incs[2048] = {0}; /* -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[1024] = {0}; char workdir[1024] = {0}; int packageopts = 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]; } size_t n = strlen(incs); snprintf(incs + n, sizeof incs - n, "%s%s", n ? ":" : "", dir); } 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-test") == 0) { if (i + 5 >= argc || nproducts >= SEP_MAXPRODUCT) { fprintf(stderr, "ww test: --ww-package-test needs kind, package, directory, output, and status\n"); return 2; } const char *kind = argv[++i]; const char *name = argv[++i]; const char *dir = argv[++i]; const char *output = argv[++i]; const char *status = argv[++i]; size_t pn = strlen(name); int variant = SEP_VARIANT_EXTERNAL; if (strcmp(kind, "production") == 0) variant = SEP_VARIANT_PRODUCTION; else if (strcmp(kind, "same") == 0) variant = SEP_VARIANT_SAME_TEST; if ((strcmp(kind, "production") != 0 && strcmp(kind, "same") != 0 && strcmp(kind, "external") != 0) || pn == 0 || pn >= sizeof ((struct seppkg *)0)->name || dir[0] == '\0' || output[0] == '\0' || status[0] == '\0' || (strcmp(kind, "external") == 0 && (pn <= 5 || strcmp(name + pn - 5, "_test") != 0))) { fprintf(stderr, "ww test: invalid --ww-package-test variant\n"); return 2; } products[nproducts].dir = dir; products[nproducts].out = output; products[nproducts].test_package = name; products[nproducts].status = status; products[nproducts].artifact[0] = '\0'; products[nproducts].variant = variant; products[nproducts].root = -1; products[nproducts].variant_root = -1; nproducts++; } else if (strcmp(argv[i], "-S") == 0) { emit_asm = 1; } else if (strcmp(argv[i], "-list") == 0) { packageopts = 1; } else if (strcmp(argv[i], "-j") == 0 || strcmp(argv[i], "-run") == 0 || strcmp(argv[i], "-filter") == 0) { if (i + 1 >= argc) { fprintf(stderr, "ww test: %s needs an argument\n", argv[i]); return 2; } packageopts = 1; i++; } else if (strncmp(argv[i], "-timeout-ms=", 12) == 0 && argv[i][12] != '\0') { packageopts = 1; } else if (strcmp(argv[i], "-o") == 0) { if (i + 1 >= argc) { fprintf(stderr, "ww test: -o needs an argument\n"); return 2; } snprintf(outstem, sizeof outstem, "%s", argv[++i]); } else if (argv[i][1] == 'o' && argv[i][2]) { snprintf(outstem, sizeof outstem, "%s", argv[i] + 2); } else if (strcmp(argv[i], "-w") == 0) { if (i + 1 >= argc) { fprintf(stderr, "ww test: -w needs an argument\n"); return 2; } snprintf(workdir, sizeof workdir, "%s", argv[++i]); } else if (argv[i][1] == 'w' && argv[i][2]) { snprintf(workdir, sizeof workdir, "%s", argv[i] + 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]) { fprintf(stderr, "ww test: -S needs -o\n"); return 2; } for (int i = 1; i < nproducts; i++) { struct sepproduct p = products[i]; int j = i; while (j > 0 && (strcmp(products[j - 1].dir, p.dir) > 0 || (strcmp(products[j - 1].dir, p.dir) == 0 && products[j - 1].variant > p.variant))) { products[j] = products[j - 1]; j--; } products[j] = p; } for (int i = 0; i < nproducts; i++) { if (i > 0 && strcmp(products[i - 1].dir, products[i].dir) == 0 && products[i - 1].variant == products[i].variant) { fprintf(stderr, "ww test: duplicate --ww-package-test variant for directory\n"); return 2; } /* Artifact identity is derived from canonical package identity after * discovery; product position is deliberately not an action key. */ products[i].artifact[0] = '\0'; } if (nproducts != 0 && packageopts) { fprintf(stderr, "ww test: package-test variant rejects package options\n"); return 2; } if (nproducts != 0 && outstem[0]) { fprintf(stderr, "ww test: package-test products reject -o\n"); return 2; } /* Go's ./... form: a trailing "..." element is a package-tree * request for the coordinator, never a literal path — recognized * before stat, with the directory-mode rejects. */ size_t tlen = strlen(target); if (strcmp(target, "...") == 0 || (tlen >= 4 && strcmp(target + tlen - 4, "/...") == 0)) { if (nproducts != 0) { fprintf(stderr, "ww test: package-test variant needs one directory\n"); return 2; } if (emit_asm) { fprintf(stderr, "ww test: -S needs a single test file\n"); return 2; } /* -c -o forwards: the coordinator names the single * package's artifact and rejects a multi-package fan-out. */ if (outstem[0] && !compileonly) { fprintf(stderr, "ww test: -o needs -c for a package target\n"); return 2; } if (pattern) { fprintf(stderr, "ww test: pattern needs a single test file\n"); return 2; } /* -w forwards: the coordinator keys one persistent driver * workdir for the complete selected test request. */ return exec_package_tests(argc, argv, src, NULL, NULL, 0); } struct stat st; if (stat(target, &st) != 0) { /* not a literal path — try module resolution and run as * a single test program. */ char resolved[1024]; int is_dir = 0; if (!resolve_module(target, incs, resolved, sizeof resolved, &is_dir)) { fprintf(stderr, "ww test: cannot find %s\n", target); return 1; } if (is_dir) { if (emit_asm) { fprintf(stderr, "ww test: -S needs a single test file\n"); return 2; } if (outstem[0] && !compileonly) { fprintf(stderr, "ww test: -o needs -c for a package target\n"); return 2; } if (pattern) { fprintf(stderr, "ww test: pattern 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; } return build_package_tests(resolved, request_identity, incs, workdir, products, nproducts); } return exec_package_tests(argc, argv, src, resolved, request_identity != NULL ? request_identity : target, 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[1024] = {0}, tmp[1024]; const char *outp; int owntmp = !outstem[0] && !workdir[0]; if (outstem[0]) outp = outstem; else if (workdir[0]) { /* The workdir owns the persistent test binary the same * way it owns the package artifacts. */ snprintf(tmp, sizeof tmp, "%s/main", workdir); 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; } snprintf(tmp, sizeof tmp, "%s/main", tmpdir); outp = tmp; } /* No-o redirects internal scratch to /tmp rather than beside the * source. An explicit -o names the caller-owned artifact stem. */ int br = build_one_sep(resolved, is_dir, NULL, outp, outstem[0] ? outstem : tmp, incs, NULL, 0, 1, SEP_VARIANT_PRODUCTION, NULL, emit_asm, outstem[0] ? 1 : 0, workdir); if (br != 0) { if (owntmp && unlink(outp) != 0 && errno != ENOENT) fputs("ww: cannot remove temporary output\n", stderr); if (owntmp && rmdir(tmpdir) != 0) fputs("ww: cannot remove temporary directory\n", stderr); return 1; } if (compileonly || emit_asm) { int cleanfail = 0; if (owntmp && unlink(outp) != 0 && errno != ENOENT) { fputs("ww: cannot remove temporary output\n", stderr); cleanfail = 1; } if (owntmp && rmdir(tmpdir) != 0) { fputs("ww: cannot remove temporary directory\n", stderr); cleanfail = 1; } return cleanfail ? 1 : 0; } int rc = run_test_bin(outp, pattern); if (owntmp && unlink(outp) != 0 && errno != ENOENT) { fputs("ww: cannot remove temporary output\n", stderr); if (rc == 0) rc = 1; } if (owntmp && rmdir(tmpdir) != 0) { fputs("ww: cannot remove temporary directory\n", stderr); if (rc == 0) rc = 1; } return rc; } if (S_ISREG(st.st_mode)) { if (nproducts != 0) { fprintf(stderr, "ww test: package-test variant needs one directory\n"); return 2; } if (packageopts) { fprintf(stderr, "ww test: package options need a directory\n"); return 2; } char tmpdir[1024] = {0}, tmp[1024]; const char *outp; int owntmp = !outstem[0] && !workdir[0]; if (outstem[0]) outp = outstem; else if (workdir[0]) { snprintf(tmp, sizeof tmp, "%s/main", workdir); 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; } snprintf(tmp, sizeof tmp, "%s/main", tmpdir); outp = tmp; } /* See module-mode note: no-o scratch is redirected to /tmp. */ int br = build_one_sep(target, 0, NULL, outp, outstem[0] ? outstem : tmp, incs, NULL, 0, 1, SEP_VARIANT_PRODUCTION, NULL, emit_asm, outstem[0] ? 1 : 0, workdir); if (br != 0) { if (owntmp && unlink(outp) != 0 && errno != ENOENT) fputs("ww: cannot remove temporary output\n", stderr); if (owntmp && rmdir(tmpdir) != 0) fputs("ww: cannot remove temporary directory\n", stderr); return 1; } if (compileonly || emit_asm) { int cleanfail = 0; if (owntmp && unlink(outp) != 0 && errno != ENOENT) { fputs("ww: cannot remove temporary output\n", stderr); cleanfail = 1; } if (owntmp && rmdir(tmpdir) != 0) { fputs("ww: cannot remove temporary directory\n", stderr); cleanfail = 1; } return cleanfail ? 1 : 0; } int rc = run_test_bin(outp, pattern); if (owntmp && unlink(outp) != 0 && errno != ENOENT) { fputs("ww: cannot remove temporary output\n", stderr); if (rc == 0) rc = 1; } if (owntmp && rmdir(tmpdir) != 0) { fputs("ww: cannot remove temporary directory\n", stderr); if (rc == 0) rc = 1; } return rc; } if (!S_ISDIR(st.st_mode)) { fprintf(stderr, "ww test: %s is neither file nor directory\n", target); return 1; } if (emit_asm) { fprintf(stderr, "ww test: -S needs a single test file\n"); return 2; } if (outstem[0] && !compileonly) { fprintf(stderr, "ww test: -o needs -c for a package target\n"); return 2; } /* The second bare positional remains the legacy single-file filter form; * package filtering uses explicit -run/-filter options. */ if (pattern) { fprintf(stderr, "ww test: pattern 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; } return build_package_tests(target, request_identity, incs, workdir, products, nproducts); } return exec_package_tests(argc, argv, src, NULL, request_identity, src == NULL); } int main(int argc, char **argv) { if (argc >= 1) { self_path = argv[0]; char buf[1024]; snprintf(buf, sizeof buf, "%s", argv[0]); 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; }