ww: add -w persistent workdir builds with content-identity reuse

A -w DIR workdir replaces the fresh .sepwork scratch with a caller-owned
persistent package-artifact tree. A package is reused only when its
freshly composed unit byte-equals the committed unit and byte copies of
the compiler/assembler recorded in the dir equal the live tools — pure
content identity, no mtimes, no hashes, every decision reproducible
with cmp against plain files. Recompiles stage at .new names and commit
by rename, unit strictly last, so an interrupted build forces a
recompile and can never leave a committed unit vouching for uncommitted
artifacts; .o/.a additionally reject zero size (ELF/ar are never
empty), while .s/.wwi accept legitimate empties (FFI-only rt). A mode
stamp pins the -T/-S shape and the artifact protocol revision. Classic
scratch keeps its exact acquire/refuse/cleanup contract; run rejects
-w; dir-mode test rejects -w; both driver stages implement identical
behavior and wording.
This commit is contained in:
2026-08-08 03:51:45 +09:00
parent e1141330f2
commit e06b871ab9
3 changed files with 693 additions and 72 deletions

View File

@@ -22,9 +22,9 @@
static const char *usage =
"usage: ww [-V] <subcommand> [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 <path> 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 `<stem>.sepwork` dir. */
* files land in a cold `<stem>.sepwork` dir, or under the persistent
* `-w` workdir with content-identity package reuse. */
static int
build_one_sep_impl(const char *src, int entry_is_dir, const char *out,
const char *objstem, const char *extra_includes, const 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 <wwi> 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) {

View File

@@ -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.

View File

@@ -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 `<stem>.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: <head><path>\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: <selfdir>/../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 <wwi> 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] <subcommand> [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 <basename>.ww\n";
let s: str = "usage: ww [-V] <subcommand> [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 <basename>.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/<prefix><pid> 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 <stem>; 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;