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

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