ww: clean per-pid sep-build scratch dir on run/test (#59)

ww run / ww test created /tmp/ww_{run,test}_<pid>.sepwork/ but removed only
the built binary, leaking the scratch DIR every invocation — the tmpfs filler
(98,811 entries blocked the gate twice). Add a keepscratch param to
build_one_sep: a thin wrapper rm -rfs the impl scratch at the single
choke-point when keepscratch==0 AND the path ends ".sepwork" (covers every
return, success+error; fires post-link, pre-run). do_build keeps it (the
byte-id gates read <stem>.sepwork from ww build -o); do_run/do_test clean;
do_test no-o redirects scratch into /tmp. Both stages symmetric; reuses the
existing shell rm -rf idiom (lib/os.removeall = #109). Test 989_sepscratch_run
is self-scoped by child pid (non-flaky) with a KEEP control + revert-verified
non-vacuity. Full gate: 448 pass, zero new run/test leaks. (ww_d_* relic +
historical bulk = one-time sweep + agent-probe discipline, not code.)
This commit is contained in:
2026-06-19 00:58:57 +09:00
parent 5adacd2eee
commit 3bb381ef48
4 changed files with 262 additions and 14 deletions

View File

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

View File

@@ -867,9 +867,9 @@ cleanup:
* reverse-topo `w6l` of the root `.o` + dep `.a` set + libwwrt.a. Side
* files land in a cold `<stem>.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 `<stem>.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 `<stem>.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_<pid>.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++;

View File

@@ -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 `<stem>.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: <selfdir>/../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
// `<stem>.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 `<stem>.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_<pid> 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_<pid>.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 ");

View File

@@ -0,0 +1,152 @@
/*
* 989_sepscratch_run (#59) — the driver must not LEAK its per-build
* `<stem>.sepwork` scratch dir. Pre-fix, `ww run` / `ww test` removed only
* the built binary (unlink) and left `/tmp/ww_run_<pid>.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: `<drv> build -o <td>/prog <root.ww>` → assert
* `<td>/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 `<drv> run <root.ww>`; the child's
* pid P fixes the driver temp at `/tmp/ww_run_<P>.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 <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>
#include <sys/stat.h>
#include <fcntl.h>
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 `<bin>/<drv> run <root>` 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;
}