/* * 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. */ #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 [-S] [-w DIR] [-o FILE] [path] compile module; -S stops after package asm\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" " lib/... every package under lib, recursively (test only)\n" " . build the cwd's .ww\n"; static char *self_dir; static const char * toolpath(const char *envvar, const char *name) { const char *p = getenv(envvar); if (p && p[0]) return p; static char buf[1024]; snprintf(buf, sizeof buf, "%s/%s", self_dir, name); return strdup(buf); } static int run(const char *cmd) { int rc = system(cmd); if (rc == -1) return -1; if (WIFEXITED(rc)) return WEXITSTATUS(rc); 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, 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 builder[1024]; snprintf(builder, sizeof builder, "%s/ww", self_dir); char **xargv = calloc((size_t)argc + 6, 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++] = builder; 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; }; 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'; } /* Symmetric with wwstage locatein for byte-id driver output (rule 10). * The legacy //.ww form was dropped in task #22 — * directory-as-module enumeration replaces it, mirroring * ref/hare/hare/module/srcs.ha (Hare has no fallback matching * `foo/foo.ha`; a module IS the directory). */ static int locate_import_in(const char *dir, const char *path_form, char *out, size_t outsz, int *is_dir, int want_dir) { struct stat st; if (want_dir) { snprintf(out, outsz, "%s/%s", dir, path_form); if (stat(out, &st) == 0 && S_ISDIR(st.st_mode)) { *is_dir = 1; return 1; } return 0; } snprintf(out, outsz, "%s/%s.ww", dir, path_form); if (access(out, 0) == 0) { *is_dir = 0; return 1; } return 0; } /* #98: "a module IS the directory" — a directory-package on ANY entry * wins over a same-named sibling FILE on an EARLIER entry. The driver * builds the searchpath srcd-first; a co-located `lib//_test.ww` * entry makes srcd = lib/, so a self-named `import ` would * else file-hit the sibling lib//.ww and fold it * inline under the wrong module-reset → "package does not match * import path ". Two passes — directories first, files only * if no directory matches anywhere — let lib// resolve as the dir * while a genuine leaf package with no directory (e.g. lib/encoding/hex * imported bare as `hex`, reachable only via its file in srcd) still * resolves in the file pass. Latent: a dir-package now beats an * earlier-entry same-named sibling FILE — loud-failing, none in the * corpus; tracked as #101. */ static int locate_import(const char *dirs, const char *path_form, char *out, size_t outsz, int *is_dir) { for (int want_dir = 1; want_dir >= 0; want_dir--) { 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, is_dir, want_dir)) 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); } /* Go's contract: only *_test.ww is a test source. A line-leading @test * declaration anywhere else would be silently dropped by a non-T build * (#6, the Hare model), so directory enumeration rejects it loudly. */ static int file_has_line_test(const char *path) { char line[4096]; FILE *f = fopen(path, "rb"); if (f == NULL) return 0; int found = 0; while (fgets(line, sizeof line, f) != NULL) { char *p = line; while (*p == ' ' || *p == '\t' || *p == '\r') p++; if (strncmp(p, "@test", 5) == 0 && (p[5] == ' ' || p[5] == '\t')) { found = 1; break; } } fclose(f); return found; } /* A line-leading @test in a production source is diagnosed here and * returns -2. This is the sole directory-membership discovery path; the * owning seppkg retains the returned list. */ static int enumerate_dir_ww(const char *dirpath, char ***out_files) { DIR *d = opendir(dirpath); if (d == NULL) { *out_files = NULL; return -1; } char **arr = NULL; int n = 0, cap = 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; char path[2048]; snprintf(path, sizeof path, "%s/%s", dirpath, nm); if (nl >= 8 && strcmp(nm + nl - 8, "_test.ww") == 0) continue; if (file_has_line_test(path)) { fprintf(stderr, "ww: %s: @test declaration outside *_test.ww\n", path); for (int i = 0; i < n; i++) free(arr[i]); free(arr); closedir(d); *out_files = NULL; return -2; } if (n + 1 > cap) { cap = cap ? cap * 2 : 8; arr = realloc(arr, cap * sizeof *arr); } arr[n++] = strdup(path); } closedir(d); if (n > 1) qsort(arr, n, sizeof *arr, strs_cmp); *out_files = arr; return n; } /* 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 dep `.wwi` as import scope) AND * producer (writes this package's `.wwi` for its importers via -I), so * a package's interface is materialized as a side effect of compiling * it. Reverse-topo order guarantees a package's deps' `.wwi` exist * before it compiles. * * The load-bearing rule (ken #56, rob-resolved): every dep is tagged by * its FULL DOTTED import path on prepend (`//ww:module `), so the * definer's qualified symbol (#53) equals the consumer's qualified * reference (#40) and the sep `.o`s link. A dep is NEVER bare-embedded. * * The prepend is the TRANSITIVE closure of a package's deps (lead- * ratified, superseding rob-c3-spec §1.3 "direct deps"): a dep's public * interface can name a transitive dep's type (os exposes time.instant), * so the consuming unit needs the whole closure for name RESOLUTION — * direct-deps-only does not type-check. This mirrors harec reading the * transitive `.td` closure and is consistent with the current flat-unit * transitive-namespace model (the visibility tighten is task #45, * deferred post-M4). */ #define SEP_MAXPKG 256 struct seppkg { char path[256]; /* dotted import path; "" == root/primary */ char entry[1024]; /* resolved package dir (or file, for a file root) */ char name[256]; /* validated declared name; directory packages only */ char **sources; /* owned, byte-sorted production paths; dirs only */ int nsources; int is_dir; int deps[SEP_MAXPKG]; /* direct-dep indices into sepgraph.pkg */ int ndeps; int color; /* tri-color DFS: 0 white, 1 gray, 2 black */ }; struct sepgraph { struct seppkg pkg[SEP_MAXPKG]; int n; }; static int sep_find_or_add(struct sepgraph *g, const char *path, const char *entry, int is_dir) { for (int i = 0; i < g->n; i++) if (strcmp(g->pkg[i].path, path) == 0) return i; 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]; snprintf(p->path, sizeof p->path, "%s", path); snprintf(p->entry, sizeof p->entry, "%s", entry); p->is_dir = is_dir; p->name[0] = '\0'; p->sources = NULL; p->nsources = 0; p->ndeps = 0; p->color = 0; return g->n++; } /* 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); } free(g); } /* Dots stay (legal in filenames); the root's empty path becomes "__root". */ 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[0] ? g->pkg[pi].path : "__root"; snprintf(out, outsz, "%s/%s%s", scratch, base, suffix); } /* unit_has_package — does `path` declare `package ;` ANYWHERE? * #16 ENFORCE-driver (rob A): distinguishes a genuinely-missing import * from one satisfied by an INLINE package in the same unit. Unlike a * first-package-decl scan, this checks every line — single-file * multi-package fixtures carry several `package` decls. The * comment-skip line scan + name match are uncapped. The * wwstage twin unithaspackage must stay byte-identical (rule 10). */ static int unit_has_package(const char *path, const char *leaf) { FILE *in = fopen(path, "rb"); if (in == NULL) return 0; char line[2048]; int found = 0; while (fgets(line, sizeof line, in)) { const char *p = line; while (*p == ' ' || *p == '\t') p++; if (p[0] == '/' && p[1] == '/') continue; if (strncmp(p, "package ", 8) != 0 && strncmp(p, "package\t", 8) != 0) continue; p += 8; while (*p == ' ' || *p == '\t') p++; size_t i = 0; while (leaf[i] != '\0' && leaf[i] == p[i]) i++; if (leaf[i] == '\0') { char c = p[i]; if (c == ';' || c == ' ' || c == '\t' || c == '\n' || c == '\0') { found = 1; break; } } } fclose(in); return found; } static int sep_ident_start(int c) { return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || c == '_'; } static int sep_ident_continue(int c) { return sep_ident_start(c) || (c >= '0' && c <= '9'); } /* Deliberately only the loader's small header grammar for the leading * package clause, not a second compiler lexer. */ static int sep_skip_space(const char *src, size_t n, size_t *off) { size_t i = *off; for (;;) { while (i < n && (src[i] == ' ' || src[i] == '\t' || src[i] == '\r' || src[i] == '\n')) i++; if (i + 1 < n && src[i] == '/' && src[i + 1] == '/') { i += 2; while (i < n && src[i] != '\n') i++; continue; } if (i + 1 < n && src[i] == '/' && src[i + 1] == '*') { i += 2; while (i + 1 < n && !(src[i] == '*' && src[i + 1] == '/')) i++; if (i + 1 >= n) return -1; i += 2; continue; } break; } *off = i; return 0; } static int sep_package_clause(const char *src, size_t n, char *name, size_t namesz) { static const char kw[] = "package"; size_t i = 0; if (sep_skip_space(src, n, &i) < 0 || i + sizeof kw - 1 >= n || memcmp(src + i, kw, sizeof kw - 1) != 0) return -1; i += sizeof kw - 1; if (i >= n || (src[i] != ' ' && src[i] != '\t' && src[i] != '\r' && src[i] != '\n')) return -1; if (sep_skip_space(src, n, &i) < 0 || i >= n || !sep_ident_start((unsigned char)src[i])) return -1; size_t begin = i++; while (i < n && sep_ident_continue((unsigned char)src[i])) i++; size_t len = i - begin; if (len + 1 > namesz || sep_skip_space(src, n, &i) < 0 || i >= n || src[i] != ';') return -1; memcpy(name, src + begin, len); name[len] = '\0'; 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, int owned_source) { if (import_seen(filevisit, file)) return 0; import_add(filevisit, file); FILE *in = fopen(file, "rb"); if (in == NULL) { fprintf(stderr, "ww: cannot read %s\n", file); return -1; } if (fseek(in, 0, SEEK_END) != 0) { fclose(in); return -1; } long flen = ftell(in); if (flen < 0 || fseek(in, 0, SEEK_SET) != 0) { fclose(in); return -1; } char *buf = malloc((size_t)flen + 1); if (buf == NULL) { fclose(in); return -1; } if (fread(buf, 1, (size_t)flen, in) != (size_t)flen) { free(buf); fclose(in); return -1; } buf[flen] = '\0'; fclose(in); if (owned_source) { char declared[256]; if (sep_package_clause(buf, (size_t)flen, declared, sizeof declared) < 0) { fprintf(stderr, "ww: %s: invalid or missing package clause\n", file); 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) { fprintf(stderr, "ww: %s: conflicting package names %s and %s\n", pkg->entry, pkg->name, declared); free(buf); return -1; } } int rc = 0; for (size_t off = 0; off < (size_t)flen && rc == 0;) { size_t end = off; while (end < (size_t)flen && buf[end] != '\n') end++; const char *p = buf + off; const char *lineend = buf + end; while (p < lineend && (*p == ' ' || *p == '\t')) p++; if ((size_t)(lineend - p) < 7 || (memcmp(p, "import ", 7) != 0 && memcmp(p, "import\t", 7) != 0)) { off = end < (size_t)flen ? end + 1 : end; continue; } p += 7; while (p < lineend && (*p == ' ' || *p == '\t')) p++; char name[256] = {0}; int j = 0; while (p < lineend && ((*p >= 'a' && *p <= 'z') || (*p >= 'A' && *p <= 'Z') || *p == '_' || *p == '.' || (*p >= '0' && *p <= '9'))) if (j + 1 < (int)sizeof name) name[j++] = *p++; if (j == 0) { off = end < (size_t)flen ? end + 1 : end; continue; } char path_form[256]; import_path_form(name, path_form, sizeof path_form); char ipath[1024]; int is_dir = 0; /* #16 ENFORCE-driver: an unresolvable import is a hard error, * not a silent skip — EXCEPT when the package is defined INLINE * in the same unit (single-file multi-package; leaf = the last * dotted component). E3-C1 (#87): the legacy amalgamator that * used to own the genuine-missing case is gone, so the sep * producer enforces it here (INV-2, by construction). */ if (!locate_import(searchpath, path_form, ipath, sizeof ipath, &is_dir)) { const char *dot = strrchr(name, '.'); const char *leaf = dot ? dot + 1 : name; if (unit_has_package(file, leaf)) goto next_line; /* inline-satisfied */ fprintf(stderr, "ww: cannot find package %s\n", name); rc = -1; goto next_line; } if (is_dir) { 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; } } else if (sep_scan_file(g, pi, ipath, searchpath, filevisit, 0) < 0) { rc = -1; break; } next_line: off = end < (size_t)flen ? end + 1 : end; } free(buf); return rc; } /* Load package pi once: a directory node takes ownership of its sorted * production paths, then the same stored list supplies package-name * validation and dependency scanning. Recurse over the resulting edges. * `color` doubles as a loaded marker here (2 == loaded); it is reset to * white before the topo pass. */ static int sep_load_pkg(struct sepgraph *g, int pi, const char *searchpath) { if (g->pkg[pi].color == 2) return 0; g->pkg[pi].color = 2; struct ImportSet fv = {0}; int rc = 0; if (g->pkg[pi].is_dir) { g->pkg[pi].nsources = enumerate_dir_ww(g->pkg[pi].entry, &g->pkg[pi].sources); if (g->pkg[pi].nsources == -2) { rc = -1; /* diagnosed in enumerate_dir_ww */ } else if (g->pkg[pi].nsources < 0) { fprintf(stderr, "ww: cannot read directory %s\n", g->pkg[pi].entry); rc = -1; } else if (g->pkg[pi].nsources == 0) { fprintf(stderr, "ww: %s: directory contains no WW package sources\n", g->pkg[pi].entry); rc = -1; } for (int i = 0; i < g->pkg[pi].nsources && rc == 0; i++) rc = sep_scan_file(g, pi, g->pkg[pi].sources[i], searchpath, &fv, 1); if (rc == 0 && g->pkg[pi].path[0] != '\0') { 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 { rc = sep_scan_file(g, pi, g->pkg[pi].entry, searchpath, &fv, 0); } for (int i = 0; i < fv.n; i++) free(fv.paths[i]); free(fv.paths); if (rc < 0) return rc; /* recurse into freshly-added deps (sep_find_or_add may have grown * g->n during the scan; iterate by index). */ for (int k = 0; k < g->pkg[pi].ndeps; k++) if (sep_load_pkg(g, g->pkg[pi].deps[k], searchpath) < 0) 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; } static void sep_mark_deps(struct sepgraph *g, int pi, char *inset) { for (int k = 0; k < g->pkg[pi].ndeps; k++) { int di = g->pkg[pi].deps[k]; if (!inset[di]) { inset[di] = 1; sep_mark_deps(g, di, inset); } } } /* Emit one of pi's own source files into the sep-unit under the * //ww:module-reset primary boundary (so -c emits its decls, imported * ==0). DIRECTORY imports are skipped (provided as `.wwi` ahead of the * body); FILE imports fold in (intra-package split). */ static void sep_emit_body(FILE *out, const char *path, struct ImportSet *visited, const char *searchpath, const char *modpath) { if (import_seen(visited, path)) return; import_add(visited, path); FILE *in = fopen(path, "rb"); if (in == NULL) { fprintf(stderr, "ww: cannot read %s\n", path); return; } char line[2048]; while (fgets(line, sizeof line, in)) { const char *p = line; while (*p == ' ' || *p == '\t') p++; if (strncmp(p, "import ", 7) != 0 && strncmp(p, "import\t", 7) != 0) continue; p += 7; while (*p == ' ' || *p == '\t') p++; char name[256] = {0}; int j = 0; while ((*p >= 'a' && *p <= 'z') || (*p >= 'A' && *p <= 'Z') || *p == '_' || *p == '.' || (*p >= '0' && *p <= '9')) if (j + 1 < (int)sizeof name) name[j++] = *p++; if (j == 0) continue; char path_form[256]; import_path_form(name, path_form, sizeof path_form); char ipath[1024]; int is_dir = 0; if (!locate_import(searchpath, path_form, ipath, sizeof ipath, &is_dir)) continue; if (!is_dir) sep_emit_body(out, ipath, visited, searchpath, modpath); } /* #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). */ if (modpath != NULL && modpath[0] != '\0') fprintf(out, "//ww:module-reset %s\n", modpath); else fputs("//ww:module-reset\n", out); rewind(in); int ch; while ((ch = fgetc(in)) != EOF) fputc(ch, out); fputc('\n', out); fclose(in); } /* Compose pi's sep-unit at `unitf`: the transitive-closure `.wwi`s * (reverse-topo order, each tagged by its dotted path), then pi's own * body under //ww:module-reset. Directory membership comes only from the * package node loaded before planning. */ static int sep_compose_unit(struct sepgraph *g, int pi, const char *scratch, const int *order, int norder, const char *searchpath, const char *unitf) { FILE *u = fopen(unitf, "wb"); if (u == NULL) { fprintf(stderr, "ww: cannot open %s\n", unitf); return -1; } char inset[SEP_MAXPKG] = {0}; sep_mark_deps(g, pi, inset); for (int oi = 0; oi < norder; oi++) { int dj = order[oi]; if (dj == pi || !inset[dj]) continue; char wwi[1024]; sep_fname(g, dj, scratch, ".wwi", wwi, sizeof wwi); FILE *wf = fopen(wwi, "rb"); if (wf == NULL) { fprintf(stderr, "ww: missing %s\n", wwi); fclose(u); return -1; } fprintf(u, "//ww:module %s\n", g->pkg[dj].path); int ch; while ((ch = fgetc(wf)) != EOF) fputc(ch, u); fputc('\n', u); fclose(wf); } struct ImportSet bodyvisit = {0}; if (g->pkg[pi].is_dir) { for (int i = 0; i < g->pkg[pi].nsources; i++) sep_emit_body(u, g->pkg[pi].sources[i], &bodyvisit, searchpath, g->pkg[pi].path); } else { sep_emit_body(u, g->pkg[pi].entry, &bodyvisit, searchpath, g->pkg[pi].path); } for (int i = 0; i < bodyvisit.n; i++) free(bodyvisit.paths[i]); free(bodyvisit.paths); fclose(u); return 0; } /* 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; } fseek(in, 0, SEEK_END); long n = ftell(in); fseek(in, 0, SEEK_SET); if (n < 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; } fclose(in); FILE *out = fopen(apath, "wb"); if (out == NULL) { fprintf(stderr, "ww: cannot open %s\n", apath); free(buf); return -1; } fwrite("!\n", 1, 8, out); /* 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); memcpy(hdr + 48, sz, (size_t)szn); hdr[58] = 0x60; /* member-header magic byte */ hdr[59] = 0x0a; fwrite(hdr, 1, sizeof hdr, out); fwrite(buf, 1, (size_t)n, out); if (n & 1) fputc('\n', out); /* members are 2-byte aligned */ fclose(out); free(buf); 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 AND the tool copies * recorded in the dir byte-equal the live tools — 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); } /* 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 1 mode %s asm %d\n", is_test ? "test" : "build", emit_asm); } /* build_one_sep — discover_deps, reverse_topo, * the transitive producer loop (one `w6c -c -I` per package, dep-first, * each DEP `.o` wrapped in its own deterministic `.a`), then a * reverse-topo `w6l` of the root `.o` + dep `.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 char *extra_libs, const char *extra_libdirs, int is_test, int emit_asm, const char *workdir, char *scratchout, size_t scratchoutsz, struct sepgraph **graphout) { const char *c6 = toolpath("WW_W6C", "w6c"); const char *a6 = toolpath("WW_W6A", "w6a"); const char *l6 = toolpath("WW_W6L", "w6l"); const char *libdir = getenv("WW_LIB"); if (libdir == NULL || libdir[0] == 0) { static char libbuf[1024]; snprintf(libbuf, sizeof libbuf, "%s/../lib", self_dir); libdir = libbuf; } const char *srcdir = getenv("WW_SRCLIB"); static char srcbuf[1024]; if (srcdir == NULL || srcdir[0] == 0) { 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; } 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'; } } static char searchpath[4096]; if (extra_includes && extra_includes[0]) snprintf(searchpath, sizeof searchpath, "%s:%s:%s", srcd, extra_includes, srcdir); else snprintf(searchpath, sizeof searchpath, "%s:%s", srcd, srcdir); srcdir = searchpath; 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 toolc[1200] = {0}, toola[1200] = {0}, stampf[1200] = {0}; char stampwant[128]; if (warm) { 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(toolc, c6) || (!emit_asm && !file_equal(toola, a6))) stale_all = 1; } struct sepgraph *g = calloc(1, sizeof *g); if (g == NULL) return 1; if (graphout) *graphout = g; int root = sep_find_or_add(g, "", src, entry_is_dir); if (root < 0) return 1; /* #79 (-T): lib/test is the synth main's `test.run` callee but @test * files never `import test;`. Inject it as a direct dep of the root so * sep_load_pkg pulls test + its transitive deps; the producer adds -T * to the root and `test.run` links against test's `.a` — via the * sep_scan_file dedup-guarded dep append. */ if (is_test) { char tpath[1024]; int tdir = 0; if (locate_import(srcdir, "test", tpath, sizeof tpath, &tdir)) { int ti = sep_find_or_add(g, "test", tpath, tdir); if (ti < 0) return 1; int seen = 0; for (int k = 0; k < g->pkg[root].ndeps; k++) if (g->pkg[root].deps[k] == ti) { seen = 1; break; } if (!seen && g->pkg[root].ndeps < SEP_MAXPKG) g->pkg[root].deps[g->pkg[root].ndeps++] = ti; } } if (sep_load_pkg(g, root, srcdir) < 0) return 1; for (int i = 0; i < g->n; i++) g->pkg[i].color = 0; 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 || sep_topo_visit(g, root, order, &norder, stack, 0) < 0) { free(stack); free(order); return 1; } free(stack); for (int oi = 0; oi < norder; oi++) { int pi = order[oi]; char unitf[1024], wwi[1024], asmf[1024], obj[1024], apath[1024]; char unitnew[1024], wwinew[1024], asmnew[1024], objnew[1024]; char anew[1024], cmd[8192]; 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, scratch, order, norder, srcdir, cu) < 0) { free(order); return 1; } if (warm && !stale_all && file_equal(unitnew, unitf) && file_is_reg(asmf) && (pi == root || file_is_reg(wwi)) && (emit_asm || (file_size_nonzero(obj) && (pi == root || file_size_nonzero(apath))))) { if (unlink(unitnew) != 0) { fprintf(stderr, "ww: cannot remove %s\n", unitnew); free(order); return 1; } continue; } /* BUG-1 (#69): -I is purely the root's UNUSED * `.wwi` output path, but it triggers wwi_emit → * check_exported_type on the root. A terminal binary's * root legitimately has `export fn` over an unexported * LOCAL type (the root is never imported), which the * export-check rejects. Skip -I for the root; its `.wwi` * is never consumed. */ if (pi == root) /* #79: the root carries -T under `ww test` * so w6c synthesizes the test main. Deps never * get -T. */ snprintf(cmd, sizeof cmd, "%s %s-c -o %s %s", c6, is_test ? "-T " : "", cs, cu); else snprintf(cmd, sizeof cmd, "%s -c -I %s -o %s %s", c6, cw, cs, cu); if (run(cmd) != 0) { fprintf(stderr, "ww: w6c failed for %s\n", g->pkg[pi].path[0] ? g->pkg[pi].path : "(root)"); free(order); return 1; } if (!emit_asm) { snprintf(cmd, sizeof cmd, "%s -o %s %s", a6, co, cs); if (run(cmd) != 0) { fprintf(stderr, "ww: w6a failed for %s\n", g->pkg[pi].path[0] ? g->pkg[pi].path : "(root)"); free(order); return 1; } } /* wrap each DEP package's `.o` in its own deterministic `.a` * (5a). The ROOT stays a positional `.o` (force-loaded — it's * the build target, always fully linked), so `main` is defined * before any archive is processed. The link consumes `.o`/`.a`, * never `.wwi`. */ if (!emit_asm && pi != root) { if (archive_o(co, ca) != 0) { fprintf(stderr, "ww: archive failed for %s\n", g->pkg[pi].path[0] ? g->pkg[pi].path : "(root)"); free(order); return 1; } } /* Commit order: artifacts before the unit that vouches for * them, unit strictly last. */ if (warm) { if ((pi != root && rename(wwinew, wwi) != 0) || rename(asmnew, asmf) != 0 || (!emit_asm && rename(objnew, obj) != 0) || (!emit_asm && pi != root && 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)"); free(order); return 1; } } } /* Tool identity commits only after every package artifact it vouches * for is itself committed; a killed pass leaves the old identity and * forces a full recompile, never a false reuse. */ if (warm) { 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 0; } /* reverse-topo link: root `.o` first (order[norder-1], force-loaded), * then transitive dep `.a` in reverse-topo order, then libwwrt.a — * each archive selectively pulls only members satisfying a live * undef. */ char rtargs[2048] = {0}; char rtpath[1024]; snprintf(rtpath, sizeof rtpath, "%s/libwwrt.a", libdir); if (access(rtpath, 0) == 0) { snprintf(rtargs, sizeof rtargs, "%s", rtpath); } else { char a1[1024], a2[1024]; snprintf(a1, sizeof a1, "%s/../obj/rt/start.o", self_dir); snprintf(a2, sizeof a2, "%s/../obj/rt/syscall.o", self_dir); snprintf(rtargs, sizeof rtargs, "%s %s", a1, a2); } char objs[8192] = {0}; for (int oi = norder - 1; oi >= 0; oi--) { char path[1024]; sep_fname(g, order[oi], scratch, order[oi] == root ? ".o" : ".a", path, sizeof path); size_t n = strlen(objs); snprintf(objs + n, sizeof objs - n, "%s%s", n ? " " : "", path); } const char *libargs = (extra_libs && extra_libs[0]) ? extra_libs : ""; const char *libdirset = (extra_libdirs && extra_libdirs[0]) ? extra_libdirs : ""; char cmd[16384]; snprintf(cmd, sizeof cmd, "%s -o %s %s %s%s%s%s%s", l6, out, objs, rtargs, libdirset[0] ? " " : "", libdirset, libargs[0] ? " " : "", libargs); int rc = run(cmd); free(order); if (rc != 0) { fprintf(stderr, "ww: w6l failed\n"); return 1; } return 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 *out, const char *objstem, const char *extra_includes, const char *extra_libs, const char *extra_libdirs, int is_test, int emit_asm, int keepscratch, const char *workdir) { char scratch[1100] = {0}; struct sepgraph *g = NULL; int r = build_one_sep_impl(src, entry_is_dir, out, objstem, extra_includes, extra_libs, extra_libdirs, is_test, 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; } static int do_version(void) { printf("ww %s\n", WW_VERSION); return 0; } /* Compose the standard module search path: cwd : : $WW_LIB * source dir. 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 = getenv("WW_SRCLIB"); static char libbuf[1024]; if (libdir == NULL || libdir[0] == 0) { libdir = getenv("WW_LIB"); } if (libdir == NULL || libdir[0] == 0) { 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; } } char sp[4096]; search_path(incs, sp, sizeof sp); char path_form[256]; import_path_form(name, path_form, sizeof path_form); return locate_import(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, char *libdirs, size_t libdirsz, char *libs, size_t libsz, char *outpath, size_t outsz, char *workdir, size_t workdirsz, const char **src_out, int *emit_asm_out) { *src_out = NULL; if (emit_asm_out) *emit_asm_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], "-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]) { size_t n = strlen(libs); snprintf(libs + n, libsz - n, "%s%s", n ? " " : "", 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; } size_t n = strlen(libs); snprintf(libs + n, libsz - n, "%s-l%s", n ? " " : "", 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; } size_t n = strlen(libdirs); snprintf(libdirs + n, libdirsz - n, "%s-L%s", n ? " " : "", argv[++i]); } else if (strncmp(argv[i], "-L", 2) == 0 && argv[i][2]) { size_t n = strlen(libdirs); snprintf(libdirs + n, libdirsz - n, "%s%s", n ? " " : "", argv[i]); } 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; char libs[2048] = {0}; char libdirs[2048] = {0}; char incs[2048] = {0}; char outflag[1024] = {0}; char workdir[1024] = {0}; int emit_asm = 0; if (parse_build_flags("build", argc, argv, incs, sizeof incs, libdirs, sizeof libdirs, libs, sizeof libs, outflag, sizeof outflag, workdir, sizeof workdir, &src, &emit_asm) < 0) return 2; if (src == NULL) src = "."; 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; } 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); } return build_one_sep(resolved, is_dir, out, objstem, incs, libs, libdirs, 0, emit_asm, 1, workdir); } static int do_run(int argc, char **argv) { const char *src = NULL; char libs[2048] = {0}; char libdirs[2048] = {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, libdirs, sizeof libdirs, libs, sizeof libs, outflag, sizeof outflag, NULL, 0, &src, NULL); if (next < 0) return 2; if (src == NULL) src = "."; 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. */ if (build_one_sep(resolved, is_dir, tmp, tmp, incs, libs, libdirs, 0, 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; 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; /* #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], "-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; } /* 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 (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 per package group under the given root. */ return exec_package_tests(argc, argv, src, 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; } return exec_package_tests(argc, argv, src, resolved, 0); } 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]) { /* 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, outp, outstem[0] ? outstem : tmp, incs, "", "", 1, 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 (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, outp, outstem[0] ? outstem : tmp, incs, "", "", 1, 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; } return exec_package_tests(argc, argv, src, NULL, src == NULL); } int main(int argc, char **argv) { if (argc >= 1) { 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; }