diff --git a/Makefile b/Makefile index 1123eca1..8b8963ab 100644 --- a/Makefile +++ b/Makefile @@ -564,6 +564,7 @@ TESTS = $(BIN)/test_smoke $(BIN)/test_lex $(BIN)/test_parse $(BIN)/test_check \ $(BIN)/test_m3sep_run \ $(BIN)/test_barefn_collide_run \ $(BIN)/test_sepbuild_run \ + $(BIN)/test_sepscratch_run \ $(BIN)/test_sepdotpath_run \ $(BIN)/test_seproot_export_run \ $(BIN)/test_syntaxexport_run \ @@ -3172,6 +3173,16 @@ $(BIN)/test_sepbuild_run: test/wcc/989_sepbuild_run.c $(BIN)/ww $(BIN)/ww_ww \ $(BIN)/w6l $(BIN)/w6l_ww $(LIB)/libwwrt.a | $(BIN) $(CC) $(CFLAGS) -o $@ $< +# 989_sepscratch_run (#59) — the driver must rm its per-build `.sepwork` +# scratch on `run`/`test` (keepscratch 0) and KEEP it on `build -o` +# (keepscratch 1, the byte-id gates read it). Drives BOTH driver stages on +# a trivial root, so it needs both driver + both compiler + both linker +# stages + libwwrt (a `run` end-to-end build). +$(BIN)/test_sepscratch_run: test/wcc/989_sepscratch_run.c $(BIN)/ww $(BIN)/ww_ww \ + $(BIN)/w6c $(BIN)/w6c_ww $(BIN)/w6a $(BIN)/w6a_ww \ + $(BIN)/w6l $(BIN)/w6l_ww $(LIB)/libwwrt.a | $(BIN) + $(CC) $(CFLAGS) -o $@ $< + # 989_barefn_collide_run — #84 cgen bare-module fn-leaf collision gate. A # package-less root's bare `fn run` must stay BARE (distinct from an # imported `aa.run`), not mis-mangle onto the import (a #263-class silent diff --git a/cmd/ww/main.c b/cmd/ww/main.c index abac3b32..29cbdefa 100644 --- a/cmd/ww/main.c +++ b/cmd/ww/main.c @@ -867,9 +867,9 @@ cleanup: * reverse-topo `w6l` of the root `.o` + dep `.a` set + libwwrt.a. Side * files land in a cold `.sepwork` dir (content-keyed cache = 5b). */ static int -build_one_sep(const char *src, int entry_is_dir, const char *out, +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) + const char *extra_libdirs, int is_test, char *scratchout, size_t scratchoutsz) { const char *c6 = toolpath("WW_W6C", "w6c"); const char *a6 = toolpath("WW_W6A", "w6a"); @@ -929,6 +929,10 @@ build_one_sep(const char *src, int entry_is_dir, const char *out, fprintf(stderr, "ww --sep: cannot create scratch %s\n", scratch); return 1; } + /* #59: hand the scratch path back so the build_one_sep wrapper can + * rm it on run/test (keepscratch 0). Set AFTER mkdir succeeds so the + * wrapper only removes a dir we actually created. */ + if (scratchout) snprintf(scratchout, scratchoutsz, "%s", scratch); struct sepgraph *g = calloc(1, sizeof *g); if (g == NULL) return 1; @@ -1062,6 +1066,34 @@ build_one_sep(const char *src, int entry_is_dir, const char *out, return 0; } +/* build_one_sep — thin wrapper over build_one_sep_impl that removes the + * per-build `.sepwork` scratch dir when keepscratch is 0 (ww run / + * ww test — the binary is the only artifact wanted). do_build passes + * keepscratch 1: the byte-id gates read `.sepwork/*.s` from the + * `ww build -o` path, so build scratch must persist. One cleanup site + * covers every impl return (success AND error). Guard: only rm a path the + * impl actually wrote that ends ".sepwork" — never an empty/foreign stem + * (#59). Reuses the existing shell rm -rf idiom (build_one_sep_impl:927). */ +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 keepscratch) +{ + char scratch[1100] = {0}; + int r = build_one_sep_impl(src, entry_is_dir, out, objstem, + extra_includes, extra_libs, extra_libdirs, is_test, + scratch, sizeof scratch); + if (!keepscratch && scratch[0]) { + size_t sl = strlen(scratch); + if (sl > 8 && strcmp(scratch + sl - 8, ".sepwork") == 0) { + char m[1200]; + snprintf(m, sizeof m, "rm -rf %s", scratch); + run(m); + } + } + return r; +} + static int do_version(void) { @@ -1291,7 +1323,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); + libdirs, 0, 1 /* keepscratch: gates read build -o .sepwork */); } static int @@ -1317,7 +1349,8 @@ do_run(int argc, char **argv) snprintf(tmp, sizeof tmp, "/tmp/ww_run_%d", getpid()); /* objstem = tmp → intermediates land under /tmp/ww_run_.sepwork/, * never next to the source (T3). */ - if (build_one_sep(resolved, is_dir, tmp, tmp, incs, libs, libdirs, 0) != 0) + if (build_one_sep(resolved, is_dir, tmp, tmp, incs, libs, libdirs, 0, + 0 /* keepscratch: throwaway run scratch */) != 0) return 1; /* exec the built binary with any trailing argv as its argv. */ pid_t pid = fork(); @@ -1418,8 +1451,12 @@ do_test(int argc, char **argv) const char *outp; if (outstem[0]) outp = outstem; else { snprintf(tmp, sizeof tmp, "/tmp/ww_test_%d", getpid()); outp = tmp; } + /* #59: no-o → objstem=tmp so scratch lands in /tmp (cleaned), + * not next to the source; keepscratch 0 throws it away. With -o + * the user named an artifact home, so keep it (mirror do_build). */ int br = build_one_sep(resolved, is_dir, outp, - outstem[0] ? outstem : NULL, incs, "", "", 1); + outstem[0] ? outstem : tmp, incs, "", "", 1, + outstem[0] ? 1 : 0); if (br != 0) return 1; if (compileonly) return 0; int rc = run_test_bin(outp, pattern); @@ -1432,8 +1469,9 @@ do_test(int argc, char **argv) const char *outp; if (outstem[0]) outp = outstem; else { snprintf(tmp, sizeof tmp, "/tmp/ww_test_%d", getpid()); outp = tmp; } - int br = build_one_sep(target, 0, outp, outstem[0] ? outstem : NULL, - incs, "", "", 1); + /* #59: see module-mode note — no-o scratch → /tmp, cleaned. */ + int br = build_one_sep(target, 0, outp, outstem[0] ? outstem : tmp, + incs, "", "", 1, outstem[0] ? 1 : 0); if (br != 0) return 1; if (compileonly) return 0; int rc = run_test_bin(outp, pattern); @@ -1471,7 +1509,8 @@ do_test(int argc, char **argv) snprintf(tmp, sizeof tmp, "/tmp/ww_test_%d_%d", getpid(), i); const char *label = strrchr(files[i], '/'); label = label ? label + 1 : files[i]; - int rc = build_one_sep(files[i], 0, tmp, NULL, target, "", "", 1); + /* #59: objstem=tmp → scratch in /tmp (cleaned), keepscratch 0. */ + int rc = build_one_sep(files[i], 0, tmp, tmp, target, "", "", 1, 0); if (rc != 0) { fprintf(stderr, "FAIL %s (build)\n", label); fail++; diff --git a/selfhost/cmd/ww/main.ww b/selfhost/cmd/ww/main.ww index eee5e374..63e3173f 100644 --- a/selfhost/cmd/ww/main.ww +++ b/selfhost/cmd/ww/main.ww @@ -1361,8 +1361,8 @@ fn cachestore(cacheroot: *u8, g: *sepgraph, pi: i32, manifest: *u8, // each `.o` wrapped in its own deterministic per-package `.a`), then a // reverse-topo `w6l` of the `.a` set + libwwrt.a. Side files land in a // cold `.sepwork` scratch dir. Twin of cstage build_one_sep. -fn buildonesep(selfdir: *u8, src: *u8, entryisdir: i32, out: *u8, - objstem: *u8, incs: *u8, lf: *lflags, istest: i32) i32 = { +fn buildonesepimpl(selfdir: *u8, src: *u8, entryisdir: i32, out: *u8, + objstem: *u8, incs: *u8, lf: *lflags, istest: i32, scratchout: **u8) i32 = { let c6: *u8 = joinpathlit(selfdir, "w6c_ww"); let a6: *u8 = joinpathlit(selfdir, "w6a_ww"); let l6: *u8 = joinpathlit(selfdir, "w6l_ww"); @@ -1442,6 +1442,10 @@ fn buildonesep(selfdir: *u8, src: *u8, entryisdir: i32, out: *u8, // rm -rf scratch (cold); recreate. Reuse os.removeall if present; // here we mkdir and rely on TRUNC opens to overwrite stale files. os.mkdir(pathstr(scratch), 493i32); // 0o755 (idempotent; stale files TRUNC'd) + // #59: hand the scratch path back so the buildonesep wrapper can rm it + // on run/test (keepscratch 0). Set AFTER mkdir so we only remove a dir + // we created. + if (scratchout != nil) { *scratchout = scratch; }; // libwwrt.a path: /../lib/libwwrt.a let libwwrt: []u8 = alloc([], (os.PATH_MAX: u64))!; @@ -1640,6 +1644,39 @@ fn buildonesep(selfdir: *u8, src: *u8, entryisdir: i32, out: *u8, return 0; }; +// buildonesep — wrapper over buildonesepimpl that rm -rf's the per-build +// `.sepwork` scratch when keepscratch==0 (ww run / ww test — the +// binary is the only wanted artifact). dobuild passes keepscratch 1: the +// byte-id gates read `.sepwork/*.s` from the `ww build -o` path, so +// build scratch must persist. One cleanup site covers every impl return. +// Guard: only rm a path the impl wrote that ends ".sepwork". Shells the +// existing /bin/sh idiom (md5appendhex precedent; no os.removeall yet — +// #109). Twin of cstage build_one_sep wrapper. +fn buildonesep(selfdir: *u8, src: *u8, entryisdir: i32, out: *u8, + objstem: *u8, incs: *u8, lf: *lflags, istest: i32, keepscratch: i32) i32 = { + let scratch: *u8 = nil; + let r: i32 = buildonesepimpl(selfdir, src, entryisdir, out, objstem, + incs, lf, istest, &scratch); + if (keepscratch == 0 && scratch != nil) { + if (cstrendswithlit(scratch, ".sepwork")) { + let cmd: []u8 = alloc([], 8192u64)!; + cmd.len = 8192; + let co: u64 = strinto(cmd.ptr, 0u64, "rm -rf '"); + co = cstrinto(cmd.ptr, co, scratch); + co = strinto(cmd.ptr, co, "'"); + cstrseal(cmd.ptr, co); + let argv: []*u8 = alloc([], 4u64)!; + argv.len = 4; + argv[0] = "sh\0".ptr; + argv[1] = "-c\0".ptr; + argv[2] = cmd.ptr; + argv[3] = nil; + procrun("/bin/sh\0".ptr, argv.ptr); + }; + }; + return r; +}; + // ---- Module-by-name resolution ---------------------------------------- // // Mirrors cmd/ww/main.c:resolvemodule. Maps a name like "foo", "lib/foo", @@ -1913,7 +1950,8 @@ fn dobuild(selfdir: *u8, argv: **u8, argc: i32, start: i32) i32 = { lf.nlibdirs = nlibdirs; lf.libs = libs.ptr; lf.nlibs = nlibs; - return buildonesep(selfdir, resolved, isdir, out, objstem, incs.ptr, &lf, 0i32); + return buildonesep(selfdir, resolved, isdir, out, objstem, incs.ptr, &lf, 0i32, + 1i32 /* keepscratch: gates read build -o .sepwork */); }; // Format the scratch path /tmp/ww_run_ into buf. Returns NUL- @@ -2070,7 +2108,8 @@ fn dorun(selfdir: *u8, argv: **u8, argc: i32, start: i32) i32 = { lf.nlibs = nlibs; // objstem = tmp → intermediates under /tmp/ww_run_.sepwork/, // never next to the source (T3). - if (buildonesep(selfdir, resolved, isdir, tmp.ptr, tmp.ptr, incs.ptr, &lf, 0i32) != 0) { + if (buildonesep(selfdir, resolved, isdir, tmp.ptr, tmp.ptr, incs.ptr, &lf, 0i32, + 0i32 /* keepscratch: throwaway run scratch */) != 0) { os.remove(pathstr(tmp.ptr)); return 1; }; @@ -2113,6 +2152,9 @@ fn runsingletest(selfdir: *u8, src: *u8, incs: *u8, compileonly: i32, outstem: * } else { makeruntmp(tmp.ptr); outp = tmp.ptr; + // #59: scratch under /tmp (cleaned by keepscratch 0), not next to + // the source. + objstem = tmp.ptr; }; // E3-C1: separate compilation is the sole build path (task #87). let lf: lflags; @@ -2120,7 +2162,10 @@ fn runsingletest(selfdir: *u8, src: *u8, incs: *u8, compileonly: i32, outstem: * lf.nlibdirs = 0; lf.libs = nil; lf.nlibs = 0; - let bres: i32 = buildonesep(selfdir, src, 0, outp, objstem, incs, &lf, 1i32); + // #59: -o → keep artifacts (mirror dobuild); no-o → throwaway, clean. + let keep: i32 = 0; + if (outstem != nil) { keep = 1; }; + let bres: i32 = buildonesep(selfdir, src, 0, outp, objstem, incs, &lf, 1i32, keep); if (bres != 0) { if (outstem == nil) { os.remove(pathstr(outp)); }; return 1; @@ -2188,7 +2233,8 @@ fn rundirtests(selfdir: *u8, dir: *u8) i32 = { let tmp: []u8 = alloc([], (os.PATH_MAX: u64))!; tmp.len = os.PATH_MAX; makeruntmp(tmp.ptr); - let bres: i32 = buildonesep(selfdir, path.ptr, 0, tmp.ptr, nil, tincs.ptr, nil, 1i32); + // #59: objstem=tmp → scratch in /tmp (cleaned), keepscratch 0. + let bres: i32 = buildonesep(selfdir, path.ptr, 0, tmp.ptr, tmp.ptr, tincs.ptr, nil, 1i32, 0i32); if (bres != 0) { fail += 1; cerr("FAIL "); diff --git a/test/wcc/989_sepscratch_run.c b/test/wcc/989_sepscratch_run.c new file mode 100644 index 00000000..c0305613 --- /dev/null +++ b/test/wcc/989_sepscratch_run.c @@ -0,0 +1,152 @@ +/* + * 989_sepscratch_run (#59) — the driver must not LEAK its per-build + * `.sepwork` scratch dir. Pre-fix, `ww run` / `ww test` removed only + * the built binary (unlink) and left `/tmp/ww_run_.sepwork/` behind + * every invocation — the tmpfs filler. The fix (build_one_sep keepscratch + * param + guarded rm at the wrapper choke-point) removes run/test scratch + * while KEEPING build -o scratch (the byte-id gates read it). + * + * Two deterministic, self-scoped checks per driver stage (ww + ww_ww), so + * they never flake under parallel phase-2 (no global /tmp glob): + * A. KEEP control: ` build -o /prog ` → assert + * `/prog.sepwork` STILL EXISTS (keepscratch 1). Proves the test is + * non-vacuous AND that the gate-read build scratch survives. + * B. CLEAN: fork a child that exec's ` run `; the child's + * pid P fixes the driver temp at `/tmp/ww_run_

.sepwork` EXACTLY + * (do_run / makeruntmp both key on getpid()). After the child exits, + * assert that dir is GONE (keepscratch 0). Self-scoped to P → immune to + * concurrent builds. + * + * run/test share ONE cleanup choke-point (the build_one_sep wrapper), so + * proving `run` proves the `test` path by construction. Revert the wrapper + * rm → check B reddens (the mandatory non-vacuity revert-experiment). + * + * Light wwstage-driver test (CLAUDE.md rule 14): the trivial no-import root + * builds fast; all artifacts live under a per-pid /tmp td or the driver's + * own /tmp temp, so it is phase-1 parallel-safe. Models 989_sepbuild_run.c. + */ +#include +#include +#include +#include +#include +#include + +static const char * +absbin(void) +{ + const char *b = getenv("BIN"); + if (!b) b = "out/bin"; + if (b[0] == '/') return b; + static char buf[2048]; + char cwd[1024]; + if (getcwd(cwd, sizeof cwd) == NULL) return NULL; + snprintf(buf, sizeof buf, "%s/%s", cwd, b); + return buf; +} + +static int +write_file(const char *path, const char *body) +{ + FILE *f = fopen(path, "wb"); + if (!f) return -1; + fputs(body, f); + fclose(f); + return 0; +} + +/* fork+exec `/ run ` with output muted; return the child's + * pid via *outpid so the caller can name the driver's /tmp scratch. */ +static int +run_child(const char *bin, const char *drv, const char *root, pid_t *outpid) +{ + char drvpath[2048]; + snprintf(drvpath, sizeof drvpath, "%s/%s", bin, drv); + pid_t pid = fork(); + if (pid < 0) return -1; + if (pid == 0) { + int dn = open("/dev/null", O_WRONLY); + if (dn >= 0) { dup2(dn, 1); dup2(dn, 2); } + /* argv[0] MUST be the full path: the driver derives self_dir + * (to locate w6c/w6a/w6l/libwwrt) from argv[0]. */ + execl(drvpath, drvpath, "run", root, (char *)NULL); + _exit(127); + } + *outpid = pid; + int status = 0; + waitpid(pid, &status, 0); + if (WIFEXITED(status)) return WEXITSTATUS(status); + return 1; +} + +/* A no-import root: builds fast, still produces a real `__root` scratch. */ +static const char *root_src = + "package main;\n" + "fn main() i32 = { return 0; };\n"; + +int +main(void) +{ + const char *bin = absbin(); + if (!bin) return 1; + char td[64], cmd[4096], rootww[1024]; + int fail = 0; + + snprintf(td, sizeof td, "/tmp/wwscratch_%d", getpid()); + snprintf(cmd, sizeof cmd, "rm -rf %s", td); + if (system(cmd) == -1) return 1; + mkdir(td, 0755); + + snprintf(rootww, sizeof rootww, "%s/root.ww", td); + if (write_file(rootww, root_src)) { fail++; goto out; } + + const char *drvs[] = { "ww", "ww_ww" }; + for (int s = 0; s < 2; s++) { + /* A. KEEP control — build -o scratch must SURVIVE. */ + char prog[1024], buildscr[1100]; + snprintf(prog, sizeof prog, "%s/prog_%s", td, drvs[s]); + snprintf(cmd, sizeof cmd, + "timeout 240 %s/%s build -o %s %s >/dev/null 2>&1", + bin, drvs[s], prog, rootww); + int brc = system(cmd); + if (brc == -1 || (WIFEXITED(brc) && WEXITSTATUS(brc) != 0)) { + fprintf(stderr, "sepscratch FAIL: %s build -o\n", drvs[s]); + fail++; + } + snprintf(buildscr, sizeof buildscr, "%s.sepwork", prog); + if (access(buildscr, 0) != 0) { + fprintf(stderr, "sepscratch FAIL: %s build -o scratch %s was " + "removed (gates read it; keepscratch must be 1)\n", + drvs[s], buildscr); + fail++; + } + + /* B. CLEAN — run scratch must be GONE post-exit (self-scoped P). */ + pid_t p = 0; + int rrc = run_child(bin, drvs[s], rootww, &p); + if (rrc != 0) { + fprintf(stderr, "sepscratch FAIL: %s run exit=%d (expected 0; " + "build must have created+run, proving non-vacuity)\n", + drvs[s], rrc); + fail++; + } + char runscr[64]; + snprintf(runscr, sizeof runscr, "/tmp/ww_run_%d.sepwork", (int)p); + if (access(runscr, 0) == 0) { + fprintf(stderr, "sepscratch FAIL: %s LEAKED %s (keepscratch 0 " + "must rm run scratch)\n", drvs[s], runscr); + fail++; + } + } + +out: + snprintf(cmd, sizeof cmd, "rm -rf %s", td); + if (system(cmd) == -1) { /* best-effort cleanup */ } + if (fail) { + fprintf(stderr, "sepscratch: %d check(s) failed\n", fail); + return 1; + } + printf("sepscratch: ww+ww_ww — build -o scratch kept, run scratch " + "removed (no /tmp .sepwork leak)\n"); + return 0; +}