diff --git a/cmd/ww/main.c b/cmd/ww/main.c index c41eb4eb..d51314ae 100644 --- a/cmd/ww/main.c +++ b/cmd/ww/main.c @@ -22,9 +22,9 @@ static const char *usage = "usage: ww [-V] [args...]\n" " -V print version and exit\n" -" build [-S] [-o FILE] [path] compile module; -S stops after package asm\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] [options] [path] build/run tests; -S emits package asm\n" +" test [-S -o STEM] [-w DIR] [options] [path] build/run tests; -S emits package asm\n" " fmt reformat ww source\n" " version print version and exit\n" "\n" @@ -868,16 +868,104 @@ archive_o(const char *objpath, const char *apath) 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. */ + * 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, char *scratchout, - size_t scratchoutsz, struct sepgraph **graphout) + 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"); @@ -930,16 +1018,50 @@ build_one_sep_impl(const char *src, int entry_is_dir, const char *out, 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]; - snprintf(scratch, sizeof scratch, "%s.sepwork", ostem); - if (mkdir(scratch, 0755) != 0) { - fprintf(stderr, "ww: cannot create scratch %s\n", scratch); - return 1; + 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; } - /* 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); struct sepgraph *g = calloc(1, sizeof *g); if (g == NULL) return 1; @@ -978,13 +1100,40 @@ build_one_sep_impl(const char *src, int entry_is_dir, const char *out, /* producer loop — dep-first, one `w6c -c -I` pass per package. */ for (int oi = 0; oi < norder; oi++) { int pi = order[oi]; - char unitf[1024], wwi[1024], asmf[1024], obj[1024], cmd[8192]; + 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, - unitf) < 0) { free(order); return 1; } + 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 @@ -997,17 +1146,17 @@ build_one_sep_impl(const char *src, int entry_is_dir, const char *out, * so w6c synthesizes the test main. Deps never * get -T. */ snprintf(cmd, sizeof cmd, "%s %s-c -o %s %s", - c6, is_test ? "-T " : "", asmf, unitf); + c6, is_test ? "-T " : "", cs, cu); else snprintf(cmd, sizeof cmd, "%s -c -I %s -o %s %s", - c6, wwi, asmf, unitf); + 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, obj, asmf); + 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)"); @@ -1020,14 +1169,53 @@ build_one_sep_impl(const char *src, int entry_is_dir, const char *out, * before any archive is processed. The link consumes `.o`/`.a`, * never `.wwi`. */ if (!emit_asm && pi != root) { - char apath[1024]; - sep_fname(g, pi, scratch, ".a", apath, sizeof apath); - if (archive_o(obj, apath) != 0) { + 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; } @@ -1077,13 +1265,14 @@ build_one_sep_impl(const char *src, int entry_is_dir, const char *out, 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 *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, - scratch, sizeof scratch, &g); + workdir, scratch, sizeof scratch, &g); sep_graph_free(g); if (!keepscratch && scratch[0]) { size_t sl = strlen(scratch); @@ -1197,6 +1386,7 @@ parse_build_flags(const char *cmd, int argc, char **argv, 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; @@ -1209,6 +1399,23 @@ parse_build_flags(const char *cmd, int argc, char **argv, 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, @@ -1277,10 +1484,12 @@ do_build(int argc, char **argv) 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, &src, &emit_asm) < 0) + outflag, sizeof outflag, workdir, sizeof workdir, + &src, &emit_asm) < 0) return 2; if (src == NULL) src = "."; /* default: build cwd */ char resolved[1024]; @@ -1307,7 +1516,7 @@ do_build(int argc, char **argv) basename_no_ext(resolved, out, sizeof out); } return build_one_sep(resolved, is_dir, out, objstem, incs, libs, - libdirs, 0, emit_asm, 1); + libdirs, 0, emit_asm, 1, workdir); } static int @@ -1320,7 +1529,7 @@ do_run(int argc, char **argv) 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, &src, NULL); + outflag, sizeof outflag, NULL, 0, &src, NULL); if (next < 0) return 2; if (src == NULL) src = "."; char resolved[1024]; @@ -1339,7 +1548,7 @@ do_run(int argc, char **argv) /* 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) != 0) { + 0, NULL) != 0) { if (unlink(tmp) != 0 && errno != ENOENT) fputs("ww: cannot remove temporary output\n", stderr); if (rmdir(tmpdir) != 0) @@ -1398,6 +1607,7 @@ do_test(int argc, char **argv) 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 @@ -1455,6 +1665,15 @@ do_test(int argc, char **argv) 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; @@ -1487,6 +1706,11 @@ do_test(int argc, char **argv) "ww test: -c/-S/-o need a single test file\n"); return 2; } + if (workdir[0]) { + fprintf(stderr, + "ww test: -w needs a single test file\n"); + return 2; + } if (pattern) { fprintf(stderr, "ww test: pattern needs a single test file\n"); @@ -1501,8 +1725,14 @@ do_test(int argc, char **argv) } char tmpdir[1024] = {0}, tmp[1024]; const char *outp; + int owntmp = !outstem[0] && !workdir[0]; if (outstem[0]) outp = outstem; - else { + 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", @@ -1516,32 +1746,32 @@ do_test(int argc, char **argv) * 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); + emit_asm, outstem[0] ? 1 : 0, workdir); if (br != 0) { - if (!outstem[0] && unlink(outp) != 0 && errno != ENOENT) + if (owntmp && unlink(outp) != 0 && errno != ENOENT) fputs("ww: cannot remove temporary output\n", stderr); - if (!outstem[0] && rmdir(tmpdir) != 0) + if (owntmp && rmdir(tmpdir) != 0) fputs("ww: cannot remove temporary directory\n", stderr); return 1; } if (compileonly || emit_asm) { int cleanfail = 0; - if (!outstem[0] && unlink(outp) != 0 && errno != ENOENT) { + if (owntmp && unlink(outp) != 0 && errno != ENOENT) { fputs("ww: cannot remove temporary output\n", stderr); cleanfail = 1; } - if (!outstem[0] && rmdir(tmpdir) != 0) { + 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 (!outstem[0] && unlink(outp) != 0 && errno != ENOENT) { + if (owntmp && unlink(outp) != 0 && errno != ENOENT) { fputs("ww: cannot remove temporary output\n", stderr); if (rc == 0) rc = 1; } - if (!outstem[0] && rmdir(tmpdir) != 0) { + if (owntmp && rmdir(tmpdir) != 0) { fputs("ww: cannot remove temporary directory\n", stderr); if (rc == 0) rc = 1; } @@ -1556,8 +1786,12 @@ do_test(int argc, char **argv) /* single .ww file — build, then run unless -c (compile-only). */ char tmpdir[1024] = {0}, tmp[1024]; const char *outp; + int owntmp = !outstem[0] && !workdir[0]; if (outstem[0]) outp = outstem; - else { + 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", @@ -1569,32 +1803,32 @@ do_test(int argc, char **argv) } /* 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); + incs, "", "", 1, emit_asm, outstem[0] ? 1 : 0, workdir); if (br != 0) { - if (!outstem[0] && unlink(outp) != 0 && errno != ENOENT) + if (owntmp && unlink(outp) != 0 && errno != ENOENT) fputs("ww: cannot remove temporary output\n", stderr); - if (!outstem[0] && rmdir(tmpdir) != 0) + if (owntmp && rmdir(tmpdir) != 0) fputs("ww: cannot remove temporary directory\n", stderr); return 1; } if (compileonly || emit_asm) { int cleanfail = 0; - if (!outstem[0] && unlink(outp) != 0 && errno != ENOENT) { + if (owntmp && unlink(outp) != 0 && errno != ENOENT) { fputs("ww: cannot remove temporary output\n", stderr); cleanfail = 1; } - if (!outstem[0] && rmdir(tmpdir) != 0) { + 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 (!outstem[0] && unlink(outp) != 0 && errno != ENOENT) { + if (owntmp && unlink(outp) != 0 && errno != ENOENT) { fputs("ww: cannot remove temporary output\n", stderr); if (rc == 0) rc = 1; } - if (!outstem[0] && rmdir(tmpdir) != 0) { + if (owntmp && rmdir(tmpdir) != 0) { fputs("ww: cannot remove temporary directory\n", stderr); if (rc == 0) rc = 1; } @@ -1608,6 +1842,10 @@ do_test(int argc, char **argv) fprintf(stderr, "ww test: -c/-S/-o need a single test file\n"); return 2; } + if (workdir[0]) { + fprintf(stderr, "ww test: -w needs a single test file\n"); + return 2; + } /* The second bare positional remains the legacy single-file filter form; * package filtering uses explicit -run/-filter options. */ if (pattern) { diff --git a/docs/test-system-v2.md b/docs/test-system-v2.md index 301427a5..ad1110d4 100644 --- a/docs/test-system-v2.md +++ b/docs/test-system-v2.md @@ -201,6 +201,22 @@ beneath one freshly acquired directory, remove both after every build result, and make cleanup failure fail the command. Make recipes build driver-produced tools in invocation-owned directories and apply the same exact cleanup rule. +`ww build -w DIR` and single-file `ww test -w DIR` replace that scratch with a +caller-owned persistent package-artifact workdir: the directory must already +exist, is never cleaned by the driver, and holds one committed unit, `.wwi`, +`.s`, `.o`, and dep `.a` per package plus byte copies of the compiler and +assembler and a small mode stamp. A package is reused only when its freshly +composed unit byte-equals the committed unit and the recorded tool copies +byte-equal the live tools — content identity only, no mtimes, no hashes, every +decision reproducible with `cmp` against plain files. Recompiled artifacts +land at staged `.new` names and commit by rename with the unit renamed last, +so an interrupted build forces a recompile rather than a false reuse; the +link always reruns. One workdir serves one invocation at a time and one +(root, mode) shape; both driver stages implement the identical contract. +This is build staleness in the Make/mk/Go sense, not a result cache: tests +always run, and the byte-identity and bootstrap gates keep building on fresh +scratch. `make clean` reclaims every workdir under `out/`. + `test/lang` currently uses one package per source file, so its complete gate still performs independent package builds. That remaining source layout is not hidden behind caching or concurrency; it is outside the small `test` target. diff --git a/selfhost/cmd/ww/main.ww b/selfhost/cmd/ww/main.ww index 00b74b57..4bbcb8c2 100644 --- a/selfhost/cmd/ww/main.ww +++ b/selfhost/cmd/ww/main.ww @@ -1294,9 +1294,174 @@ fn archiveo(objpath: *u8, apath: *u8) i32 = { // reverse-topo `w6l` of the root `.o` + dependency `.a` set + libwwrt.a. // Side files land in a cold `.sepwork` scratch dir. Twin of cstage // build_one_sep. +// ---- -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 and `make clean` reclaims +// the state. Cstage twin: cmd/ww/main.c file_equal/copy_file_atomic/ +// workdir_stamp_text group. + +// `.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. +fn fileisreg(path: *u8) bool = { + let fi: os.filestat; + let ok: bool = false; + match (os.stat(&fi, pathstr(path))) { + case void => { + let t: u32 = (fi.mode: u32) & 61440u32; // S_IFMT + if (t == os.mode.REG: u32) { ok = true; }; + }; + case let e: os.oserror => void; + }; + return ok; +}; + +fn filesizenonzero(path: *u8) bool = { + let fi: os.filestat; + let ok: bool = false; + match (os.stat(&fi, pathstr(path))) { + case void => { + let t: u32 = (fi.mode: u32) & 61440u32; // S_IFMT + if (t == os.mode.REG: u32) { + if (fi.sz > 0u64) { ok = true; }; + }; + }; + case let e: os.oserror => void; + }; + return ok; +}; + +// Byte equality of two files; absence or IO error is inequality. +fn fileequal(a: *u8, b: *u8) bool = { + let fa: i32 = os.open(pathstr(a), os.flag.RDONLY, 0i32); + if (fa < 0) { return false; }; + let fb: i32 = os.open(pathstr(b), os.flag.RDONLY, 0i32); + if (fb < 0) { os.close(fa); return false; }; + let bufa: []u8 = alloc([], 65536u64)!; + bufa.len = 65536; + let bufb: []u8 = alloc([], 65536u64)!; + bufb.len = 65536; + let eq: bool = true; + let done: bool = false; + for (!done) { + let na: i64 = os.read(fa, bufa.ptr, 65536u64); + let nb: i64 = os.read(fb, bufb.ptr, 65536u64); + if (na < 0 || na != nb) { eq = false; done = true; } + else { if (na == 0) { done = true; } + else { + let k: u64 = 0u64; + for (k < (na: u64)) { + if (bufa[k] != bufb[k]) { + eq = false; done = true; k = (na: u64); + }; + k += 1u64; + }; + }; }; + }; + os.close(fa); + os.close(fb); + return eq; +}; + +// Replace dst with src's bytes via temp + rename, so a torn write can +// never masquerade as a committed tool copy. +fn copyfileatomic(src: *u8, dst: *u8) i32 = { + let tmpp: *u8 = appendlit(dst, ".new"); + let in: i32 = os.open(pathstr(src), os.flag.RDONLY, 0i32); + if (in < 0) { return -1; }; + let out: i32 = os.open(pathstr(tmpp), + os.flag.WRONLY | os.flag.CREATE | os.flag.TRUNC, 420i32); // 0o644 + if (out < 0) { os.close(in); return -1; }; + let buf: []u8 = alloc([], 65536u64)!; + buf.len = 65536; + let bad: bool = false; + let done: bool = false; + for (!done) { + let n: i64 = os.read(in, buf.ptr, 65536u64); + if (n < 0) { bad = true; done = true; } + else { if (n == 0) { done = true; } + else { + match (os.writeall(out, buf.ptr, n: u64)) { + case let w: i64 => void; + case let e: os.oserror => { bad = true; done = true; }; + }; + }; }; + }; + os.close(in); + if (os.close(out) != 0) { bad = true; }; + if (bad) { return -1; }; + return os.rename(pathstr(tmpp), pathstr(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). +fn workdirstamptext(istest: i32, emitasm: i32) str = { + if (istest != 0) { + if (emitasm != 0) { + return "ww workdir fmt 1 mode test asm 1\n"; + }; + return "ww workdir fmt 1 mode test asm 0\n"; + }; + if (emitasm != 0) { + return "ww workdir fmt 1 mode build asm 1\n"; + }; + return "ww workdir fmt 1 mode build asm 0\n"; +}; + +fn stampmatches(path: *u8, want: str) bool = { + let fd: i32 = os.open(pathstr(path), os.flag.RDONLY, 0i32); + if (fd < 0) { return false; }; + let buf: []u8 = alloc([], 128u64)!; + buf.len = 128; + let n: i64 = os.read(fd, buf.ptr, 127u64); + os.close(fd); + if (n < 0) { return false; }; + if ((n: i32) != want.len) { return false; }; + let k: u64 = 0u64; + let eq: bool = true; + for (k < (n: u64)) { + if (buf[k] != want.ptr[k]) { eq = false; k = (n: u64); }; + k += 1u64; + }; + return eq; +}; + +fn writestampatomic(path: *u8, want: str) i32 = { + let tmpp: *u8 = appendlit(path, ".new"); + let fd: i32 = os.open(pathstr(tmpp), + os.flag.WRONLY | os.flag.CREATE | os.flag.TRUNC, 420i32); // 0o644 + if (fd < 0) { return -1; }; + let bad: bool = false; + match (os.writeall(fd, want.ptr, want.len: u64)) { + case let w: i64 => void; + case let e: os.oserror => { bad = true; }; + }; + if (os.close(fd) != 0) { bad = true; }; + if (bad) { return -1; }; + return os.rename(pathstr(tmpp), pathstr(path)); +}; + +// cerrpath — the "ww: \n" diagnostic shape shared by the +// workdir error sites; byte-identical wording to the cstage twin's +// fprintf(..., "%s", path) forms. +fn cerrpath(head: str, path: *u8, tail: str) void = { + cerr(head); + os.write(2, path, cstrlen(path)); + cerr(tail); +}; + fn buildonesepimpl(selfdir: *u8, src: *u8, entryisdir: i32, out: *u8, objstem: *u8, incs: *u8, lf: *lflags, istest: i32, emitasm: i32, - scratchout: **u8, graphout: **sepgraph) i32 = { + workdir: *u8, scratchout: **u8, graphout: **sepgraph) i32 = { let c6: *u8 = joinpathlit(selfdir, "w6c_ww"); let a6: *u8 = joinpathlit(selfdir, "w6a_ww"); let l6: *u8 = joinpathlit(selfdir, "w6l_ww"); @@ -1371,14 +1536,63 @@ fn buildonesepimpl(selfdir: *u8, src: *u8, entryisdir: i32, out: *u8, }; let effstem: *u8 = stem.ptr; if (objstem != nil) { effstem = objstem; }; - let scratch: *u8 = appendlit(effstem, ".sepwork"); - if (os.mkdir(pathstr(scratch), 493i32) != 0) { - cerr("ww: cannot create scratch\n"); - return 1; + let warm: bool = false; + if (workdir != nil) { + if (workdir[0u64] != 0u8) { warm = true; }; + }; + let scratch: *u8 = nil; + if (warm) { + let wfi: os.filestat; + let wok: bool = false; + match (os.stat(&wfi, pathstr(workdir))) { + case void => { + let wt: u32 = (wfi.mode: u32) & 61440u32; // S_IFMT + if (wt == os.mode.DIR: u32) { wok = true; }; + }; + case let e: os.oserror => void; + }; + if (!wok) { + cerrpath("ww: workdir ", workdir, + " is not a directory\n"); + return 1; + }; + // The workdir is caller-owned and persistent: no acquisition, + // no refusal, and scratchout stays nil so the wrapper never + // cleans it. + scratch = workdir; + } else { + scratch = appendlit(effstem, ".sepwork"); + if (os.mkdir(pathstr(scratch), 493i32) != 0) { + cerr("ww: cannot create scratch\n"); + return 1; + }; + // Hand the path back only after mkdir succeeds, so the wrapper + // never removes a pre-existing path that this invocation failed + // to acquire. + if (scratchout != nil) { *scratchout = scratch; }; + }; + let staleall: bool = false; + let stampok: bool = false; + let toolc: *u8 = nil; + let toola: *u8 = nil; + let stampf: *u8 = nil; + let stampwant: str = ""; + if (warm) { + toolc = joinpathlit(scratch, ".wwtool.w6c"); + toola = joinpathlit(scratch, ".wwtool.w6a"); + stampf = joinpathlit(scratch, ".wwtool.stamp"); + stampwant = workdirstamptext(istest, emitasm); + stampok = stampmatches(stampf, stampwant); + staleall = !stampok; + if (!staleall) { + if (!fileequal(toolc, c6)) { staleall = true; }; + }; + if (!staleall) { + if (emitasm == 0) { + if (!fileequal(toola, a6)) { staleall = true; }; + }; + }; }; - // Hand the path back only after mkdir succeeds, so the wrapper never - // removes a pre-existing path that this invocation failed to acquire. - if (scratchout != nil) { *scratchout = scratch; }; // libwwrt.a path: /../lib/libwwrt.a let libwwrt: []u8 = alloc([], (os.PATH_MAX: u64))!; @@ -1441,9 +1655,56 @@ fn buildonesepimpl(selfdir: *u8, src: *u8, entryisdir: i32, out: *u8, let wwi: *u8 = sepfname(g, pi, scratch, ".wwi"); let asmf: *u8 = sepfname(g, pi, scratch, ".s"); let objf: *u8 = sepfname(g, pi, scratch, ".o"); - if (sepcomposeunit(g, pi, scratch, order, norder, searchpath.ptr, unitf) < 0) { + let apath: *u8 = sepfname(g, pi, scratch, ".a"); + let unitnew: *u8 = sepfname(g, pi, scratch, ".unit.new"); + let wwinew: *u8 = sepfname(g, pi, scratch, ".wwi.new"); + let asmnew: *u8 = sepfname(g, pi, scratch, ".s.new"); + let objnew: *u8 = sepfname(g, pi, scratch, ".o.new"); + let anew: *u8 = sepfname(g, pi, scratch, ".a.new"); + // Warm mode compiles from staged `.new` paths and commits by + // rename; classic mode keeps its exact in-place paths. + let cu: *u8 = unitf; + let cw: *u8 = wwi; + let cs: *u8 = asmf; + let co: *u8 = objf; + let ca: *u8 = apath; + if (warm) { + cu = unitnew; cw = wwinew; cs = asmnew; + co = objnew; ca = anew; + }; + if (sepcomposeunit(g, pi, scratch, order, norder, searchpath.ptr, cu) < 0) { return 1; }; + let fresh: bool = false; + if (warm) { + if (!staleall) { + fresh = fileequal(unitnew, unitf); + if (fresh) { fresh = fileisreg(asmf); }; + if (fresh) { + if (pi != root) { + fresh = fileisreg(wwi); + }; + }; + if (fresh) { + if (emitasm == 0) { + fresh = filesizenonzero(objf); + }; + }; + if (fresh) { + if (emitasm == 0 && pi != root) { + fresh = filesizenonzero(apath); + }; + }; + }; + }; + if (fresh) { + if (os.remove(pathstr(unitnew)) != 0) { + cerrpath("ww: cannot remove ", unitnew, "\n"); + return 1; + }; + oi += 1; + continue; + }; { // BUG-1 (#69): -I is purely the root's UNUSED // `.wwi` output path, but it triggers wwiemit -> @@ -1463,11 +1724,11 @@ fn buildonesepimpl(selfdir: *u8, src: *u8, entryisdir: i32, out: *u8, append(argv, "-c"); if (pi != root) { append(argv, "-I"); - append(argv, pathstr(wwi)); + append(argv, pathstr(cw)); }; append(argv, "-o"); - append(argv, pathstr(asmf)); - append(argv, pathstr(unitf)); + append(argv, pathstr(cs)); + append(argv, pathstr(cu)); let env: []str = os.getenvs(); let result: exec.result; exec.runstdio(pathstr(c6), argv, env, &result); @@ -1485,8 +1746,8 @@ fn buildonesepimpl(selfdir: *u8, src: *u8, entryisdir: i32, out: *u8, let argv: []str = alloc([], 4u64)!; append(argv, "w6a"); append(argv, "-o"); - append(argv, pathstr(objf)); - append(argv, pathstr(asmf)); + append(argv, pathstr(co)); + append(argv, pathstr(cs)); let env: []str = os.getenvs(); let result: exec.result; exec.runstdio(pathstr(a6), argv, env, &result); @@ -1506,14 +1767,81 @@ fn buildonesepimpl(selfdir: *u8, src: *u8, entryisdir: i32, out: *u8, // processed. The link // consumes `.o`/`.a`, never `.wwi`. if (emitasm == 0 && pi != root) { - let apath: *u8 = sepfname(g, pi, scratch, ".a"); - if (archiveo(objf, apath) != 0) { + if (archiveo(co, ca) != 0) { cerr("ww: archive failed\n"); return 1; }; }; + // Commit order: artifacts before the unit that vouches for + // them, unit strictly last. + if (warm) { + let bad: bool = false; + if (pi != root) { + if (os.rename(pathstr(wwinew), pathstr(wwi)) != 0) { + bad = true; + }; + }; + if (!bad) { + if (os.rename(pathstr(asmnew), pathstr(asmf)) != 0) { + bad = true; + }; + }; + if (!bad) { + if (emitasm == 0) { + if (os.rename(pathstr(objnew), pathstr(objf)) != 0) { + bad = true; + }; + }; + }; + if (!bad) { + if (emitasm == 0 && pi != root) { + if (os.rename(pathstr(anew), pathstr(apath)) != 0) { + bad = true; + }; + }; + }; + if (!bad) { + if (os.rename(pathstr(unitnew), pathstr(unitf)) != 0) { + bad = true; + }; + }; + if (bad) { + if (g.pkg[pi].path[0u64] != 0u8) { + cerrpath("ww: cannot commit ", + g.pkg[pi].path, "\n"); + } else { + cerr("ww: cannot commit (root)\n"); + }; + return 1; + }; + }; oi += 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 (!fileequal(toolc, c6)) { + if (copyfileatomic(c6, toolc) != 0) { + cerrpath("ww: cannot record ", toolc, "\n"); + return 1; + }; + }; + if (emitasm == 0) { + if (!fileequal(toola, a6)) { + if (copyfileatomic(a6, toola) != 0) { + cerrpath("ww: cannot record ", toola, "\n"); + return 1; + }; + }; + }; + if (!stampok) { + if (writestampatomic(stampf, stampwant) != 0) { + cerrpath("ww: cannot record ", stampf, "\n"); + return 1; + }; + }; + }; if (emitasm != 0) { return 0; }; // Reverse-topo link: root `.o` first (order[norder-1]), dependency `.a` @@ -1591,11 +1919,11 @@ fn buildonesepimpl(selfdir: *u8, src: *u8, entryisdir: i32, out: *u8, // exact tree. Twin of the cstage build_one_sep wrapper. fn buildonesep(selfdir: *u8, src: *u8, entryisdir: i32, out: *u8, objstem: *u8, incs: *u8, lf: *lflags, istest: i32, emitasm: i32, - keepscratch: i32) i32 = { + keepscratch: i32, workdir: *u8) i32 = { let scratch: *u8 = nil; let g: *sepgraph = nil; let r: i32 = buildonesepimpl(selfdir, src, entryisdir, out, objstem, - incs, lf, istest, emitasm, &scratch, &g); + incs, lf, istest, emitasm, workdir, &scratch, &g); sepgraphfree(g); if (keepscratch == 0 && scratch != nil) { if (cstrendswithlit(scratch, ".sepwork")) { @@ -1716,7 +2044,7 @@ fn resolvemodule(selfdir: *u8, name: *u8, incs: *u8, isdir: *i32) *u8 = { // ---- Subcommand handlers ---------------------------------------------- fn writeusage(fd: i32) void = { - let s: str = "usage: ww [-V] [args...]\n -V print version and exit\n build [-S] [-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] [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 $WW_LIB-equiv for foo.ww or foo/foo.ww\n lib/foo directory: build lib/foo/foo.ww\n . build the cwd's .ww\n"; + let s: str = "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 $WW_LIB-equiv for foo.ww or foo/foo.ww\n lib/foo directory: build lib/foo/foo.ww\n . build the cwd's .ww\n"; os.write(fd, s.ptr, s.len: u64); }; @@ -1761,6 +2089,7 @@ fn defaultoutpath(src: *u8) *u8 = { fn dobuild(selfdir: *u8, argv: **u8, argc: i32, start: i32) i32 = { let src: *u8 = nil; let outflag: *u8 = nil; // -o target (binary + intermediate stem); T3 + let workdir: *u8 = nil; // -w persistent package-artifact workdir let emitasm: i32 = 0; let incs: []u8 = alloc([], (os.PATH_MAX: u64) * 2u64)!; incs.len = ((os.PATH_MAX: u64) * 2u64): i32; @@ -1846,10 +2175,21 @@ fn dobuild(selfdir: *u8, argv: **u8, argc: i32, start: i32) i32 = { i += 1; outflag = argv[i]; }; + } else { if (p[1u64] == 119u8) { // '-w' + if (p[2u64] != 0u8) { + workdir = p + 2u64; + } else { + if (i + 1 >= argc) { + cerr("ww build: -w needs an argument\n"); + return 2; + }; + i += 1; + workdir = argv[i]; + }; } else { cerr("ww build: unknown flag\n"); return 2; - }; }; }; }; }; + }; }; }; }; }; }; } else { if (src == nil) { src = p; }; }; @@ -1897,7 +2237,7 @@ fn dobuild(selfdir: *u8, argv: **u8, argc: i32, start: i32) i32 = { lf.libs = libs.ptr; lf.nlibs = nlibs; return buildonesep(selfdir, resolved, isdir, out, objstem, incs.ptr, &lf, - 0i32, emitasm, 1i32); + 0i32, emitasm, 1i32, workdir); }; // Format the owned driver workspace /tmp/ into buf. Pid is @@ -2063,7 +2403,7 @@ fn dorun(selfdir: *u8, argv: **u8, argc: i32, start: i32) i32 = { lf.nlibs = nlibs; // The freshly acquired directory owns both main and main.sepwork. if (buildonesep(selfdir, resolved, isdir, outp, outp, incs.ptr, &lf, - 0i32, 0i32, 0i32) != 0) { + 0i32, 0i32, 0i32, nil) != 0) { let cleanrc: i32 = os.remove(pathstr(outp)); if (cleanrc != 0 && cleanrc != -2i32) { cerr("ww: cannot remove temporary output\n"); @@ -2118,17 +2458,26 @@ fn dorun(selfdir: *u8, argv: **u8, argc: i32, start: i32) i32 = { // compatibility route; directory/default requests delegate to wwtest. fn runsingletest(selfdir: *u8, src: *u8, incs: *u8, compileonly: i32, - emitasm: i32, outstem: *u8, pattern: *u8) i32 = { + emitasm: i32, outstem: *u8, workdir: *u8, pattern: *u8) i32 = { let tmp: []u8 = alloc([], (os.PATH_MAX: u64))!; tmp.len = os.PATH_MAX; // -o redirects the binary + its caller-owned sepwork intermediates // (objstem, T3) to ; without -o both are driver-owned /tmp paths. let outp: *u8 = nil; let objstem: *u8 = nil; + // owntmp: the driver owns (and must clean) the /tmp workspace; with + // -o or -w the binary lands in a caller-owned location instead. + let owntmp: bool = false; if (outstem != nil) { outp = outstem; objstem = outstem; + } else { if (workdir != nil) { + // The workdir owns the persistent test binary the same way it + // owns the package artifacts. + outp = joinpathlit(workdir, "main"); + objstem = outp; } else { + owntmp = true; makedrivertmp(tmp.ptr, "ww_test_"); if (os.mkdir(pathstr(tmp.ptr), 448i32) != 0) { cerr("ww: cannot create temporary directory\n"); @@ -2136,7 +2485,7 @@ fn runsingletest(selfdir: *u8, src: *u8, incs: *u8, compileonly: i32, }; outp = joinpathlit(tmp.ptr, "main"); objstem = outp; - }; + }; }; // E3-C1: separate compilation is the sole build path (task #87). let lf: lflags; lf.libdirs = nil; @@ -2146,9 +2495,9 @@ fn runsingletest(selfdir: *u8, src: *u8, incs: *u8, compileonly: i32, let keep: i32 = 0; if (outstem != nil) { keep = 1; }; let bres: i32 = buildonesep(selfdir, src, 0, outp, objstem, incs, &lf, - 1i32, emitasm, keep); + 1i32, emitasm, keep, workdir); if (bres != 0) { - if (outstem == nil) { + if (owntmp) { let cleanrc: i32 = os.remove(pathstr(outp)); if (cleanrc != 0 && cleanrc != -2i32) { cerr("ww: cannot remove temporary output\n"); @@ -2160,7 +2509,7 @@ fn runsingletest(selfdir: *u8, src: *u8, incs: *u8, compileonly: i32, return 1; }; if (compileonly != 0 || emitasm != 0) { - if (outstem == nil) { + if (owntmp) { let cleanbad: bool = false; let cleanrc: i32 = os.remove(pathstr(outp)); if (cleanrc != 0 && cleanrc != -2i32) { @@ -2197,7 +2546,7 @@ fn runsingletest(selfdir: *u8, src: *u8, incs: *u8, compileonly: i32, rc = -1; }; }; }; - if (outstem == nil) { + if (owntmp) { let cleanrc: i32 = os.remove(pathstr(outp)); if (cleanrc != 0 && cleanrc != -2i32) { cerr("ww: cannot remove temporary output\n"); @@ -2232,6 +2581,7 @@ fn dotest(selfdir: *u8, argv: **u8, argc: i32, start: i32) i32 = { let compileonly: i32 = 0; let emitasm: i32 = 0; let outstem: *u8 = nil; + let workdir: *u8 = nil; let packageopts: bool = false; let afterdash: bool = false; let i: i32 = start; @@ -2293,6 +2643,19 @@ fn dotest(selfdir: *u8, argv: **u8, argc: i32, start: i32) i32 = { }; i += 1; continue; }; + if (p[1u64] == 119u8) { // '-w' + if (p[2u64] != 0u8) { + workdir = p + 2u64; + } else { + if (i + 1 >= argc) { + cerr("ww test: -w needs an argument\n"); + return 2; + }; + i += 1; + workdir = argv[i]; + }; + i += 1; continue; + }; cerr("ww test: unknown flag\n"); return 2; } else { if (target == nil) { target = p; targetindex = i; } @@ -2334,13 +2697,17 @@ fn dotest(selfdir: *u8, argv: **u8, argc: i32, start: i32) i32 = { cerr("ww test: package options need a directory\n"); return 2; }; return runsingletest(selfdir, resolved, incs.ptr, compileonly, - emitasm, outstem, patarg); + emitasm, outstem, workdir, patarg); }; if (outstem != nil) { cerr("ww test: -c/-S/-o need a single test file\n"); return 2; }; + if (workdir != nil) { + cerr("ww test: -w needs a single test file\n"); + return 2; + }; if (patarg != nil) { cerr("ww test: pattern needs a single test file\n"); return 2;