ww: own the scratch contract, drop the package cache, add -S
Scratch acquisition is one bare mkdir that refuses a pre-existing
.sepwork; the path reaches cleanup only after acquisition succeeds.
ww build and ww test -o keep the tree for the caller; ww run and
no-output ww test build under an owned /tmp/ww_{run,test}_<pid> dir
and remove it on every result, escalating rc on cleanup failure.
The content-keyed package cache is deleted from both stages; -S stops
after per-package .s emission; --sep is gone and unknown flags are
rejected; directory ww test delegates to the wwtest coordinator; the
wwstage driver routes every subprocess through os.exec.
The wwstage Makefile recipes move off the persistent <tool>_ww.d
build-in-place shape to a per-invocation mktemp workspace: the old
recipes relied on the retired pre-wipe semantics and any rebuild
would trip the new refusal on their leftover main.sepwork.
The three 989_pkgcache_* carriers lose their subject with the cache
and go with it; the rest of the wave's carrier adaptation follows in
its own commits.
This commit is contained in:
12
.gitignore
vendored
12
.gitignore
vendored
@@ -1,12 +1,12 @@
|
||||
# Build outputs.
|
||||
/out/
|
||||
|
||||
# Separate-compilation scratch dirs. `ww build` / `ww --sep` (the sole
|
||||
# build path post-#22 flip) drops <stem>.sepwork/ next to the OUTPUT
|
||||
# stem; an in-place build (no -o) lands it next to the source. The
|
||||
# Makefile byte-id gates read <stem>.sepwork/*.s deliberately (rule-10
|
||||
# #223 discriminator), so the dir is transient-not-tracked (CLAUDE.md
|
||||
# rule-14), never relocated.
|
||||
# Separate-compilation artifact dirs. `ww build` and explicit `ww test -o`
|
||||
# leave caller-owned <stem>.sepwork/ at the selected artifact stem; they
|
||||
# refuse to replace an existing tree. `ww run` and no-output single-file tests
|
||||
# put the executable and scratch in one driver-owned temporary directory and
|
||||
# remove both. Byte gates read selected files deliberately, then remove the
|
||||
# exact tree, so it is transient-not-tracked.
|
||||
*.sepwork/
|
||||
|
||||
# Claude Code session artifacts.
|
||||
|
||||
78
Makefile
78
Makefile
@@ -132,12 +132,16 @@ $(BIN)/wwdump_ww: selfhost/cmd/wwdump/main.ww \
|
||||
$(BIN)/ww $(BIN)/w6c $(BIN)/w6a $(BIN)/w6l \
|
||||
$(LIB)/libwwrt.a | $(BIN)
|
||||
@mkdir -p $(BIN)/wwdump_ww.d
|
||||
cd $(BIN)/wwdump_ww.d && $(CURDIR)/$(BIN)/ww build \
|
||||
-o main \
|
||||
-I $(CURDIR)/lib/ww \
|
||||
-I $(CURDIR)/selfhost/cmd/wcc \
|
||||
$(CURDIR)/selfhost/cmd/wwdump/main.ww
|
||||
mv $(BIN)/wwdump_ww.d/main $@
|
||||
@d=$$(mktemp -d "$(CURDIR)/$(BIN)/wwdump_ww.d/build.XXXXXX") || exit 1; \
|
||||
rc=0; published=0; \
|
||||
(cd "$$d" && $(CURDIR)/$(BIN)/ww build -o main \
|
||||
-I $(CURDIR)/lib/ww -I $(CURDIR)/selfhost/cmd/wcc \
|
||||
$(CURDIR)/selfhost/cmd/wwdump/main.ww) || rc=$$?; \
|
||||
rm -rf -- "$$d/main.sepwork" || { echo "$@: scratch cleanup failed" >&2; if [ $$rc -eq 0 ]; then rc=1; fi; }; \
|
||||
if [ $$rc -eq 0 ]; then mv "$$d/main" "$(CURDIR)/$@" && published=1 || rc=$$?; fi; \
|
||||
if [ $$published -eq 0 ]; then rm -f -- "$$d/main" || { echo "$@: output cleanup failed" >&2; if [ $$rc -eq 0 ]; then rc=1; fi; }; fi; \
|
||||
if ! rmdir "$$d"; then echo "$@: workspace cleanup failed" >&2; if [ $$published -eq 1 ]; then rm -f -- "$(CURDIR)/$@" || echo "$@: published-output cleanup failed" >&2; fi; if [ $$rc -eq 0 ]; then rc=1; fi; fi; \
|
||||
exit $$rc
|
||||
|
||||
# ---- ww-side w6c (compiler port, exercised by 994_w6c_ww) -------------
|
||||
# Thin driver: parse + cgen. The frontend (lex/parse/ast/...) is the
|
||||
@@ -153,12 +157,16 @@ $(BIN)/w6c_ww: selfhost/cmd/w6c/main.ww \
|
||||
$(BIN)/ww $(BIN)/w6c $(BIN)/w6a $(BIN)/w6l \
|
||||
$(LIB)/libwwrt.a | $(BIN)
|
||||
@mkdir -p $(BIN)/w6c_ww.d
|
||||
cd $(BIN)/w6c_ww.d && $(CURDIR)/$(BIN)/ww build \
|
||||
-o main \
|
||||
-I $(CURDIR)/lib/ww \
|
||||
-I $(CURDIR)/selfhost/cmd/wcc \
|
||||
$(CURDIR)/selfhost/cmd/w6c/main.ww
|
||||
mv $(BIN)/w6c_ww.d/main $@
|
||||
@d=$$(mktemp -d "$(CURDIR)/$(BIN)/w6c_ww.d/build.XXXXXX") || exit 1; \
|
||||
rc=0; published=0; \
|
||||
(cd "$$d" && $(CURDIR)/$(BIN)/ww build -o main \
|
||||
-I $(CURDIR)/lib/ww -I $(CURDIR)/selfhost/cmd/wcc \
|
||||
$(CURDIR)/selfhost/cmd/w6c/main.ww) || rc=$$?; \
|
||||
rm -rf -- "$$d/main.sepwork" || { echo "$@: scratch cleanup failed" >&2; if [ $$rc -eq 0 ]; then rc=1; fi; }; \
|
||||
if [ $$rc -eq 0 ]; then mv "$$d/main" "$(CURDIR)/$@" && published=1 || rc=$$?; fi; \
|
||||
if [ $$published -eq 0 ]; then rm -f -- "$$d/main" || { echo "$@: output cleanup failed" >&2; if [ $$rc -eq 0 ]; then rc=1; fi; }; fi; \
|
||||
if ! rmdir "$$d"; then echo "$@: workspace cleanup failed" >&2; if [ $$published -eq 1 ]; then rm -f -- "$(CURDIR)/$@" || echo "$@: published-output cleanup failed" >&2; fi; if [ $$rc -eq 0 ]; then rc=1; fi; fi; \
|
||||
exit $$rc
|
||||
|
||||
# ---- ww-side w6a (assembler port, exercised by 991_w6a_ww) ------------
|
||||
# Built like wwdump_ww. Needs -I selfhost/cmd/w6a for the local types/lex/
|
||||
@@ -171,11 +179,16 @@ $(BIN)/w6a_ww: selfhost/cmd/w6a/main.ww selfhost/cmd/w6a/opcodes.ww \
|
||||
$(BIN)/ww $(BIN)/w6c $(BIN)/w6a $(BIN)/w6l \
|
||||
$(LIB)/libwwrt.a | $(BIN)
|
||||
@mkdir -p $(BIN)/w6a_ww.d
|
||||
cd $(BIN)/w6a_ww.d && $(CURDIR)/$(BIN)/ww build \
|
||||
-o main \
|
||||
-I $(CURDIR)/selfhost/cmd/w6a \
|
||||
$(CURDIR)/selfhost/cmd/w6a/main.ww
|
||||
mv $(BIN)/w6a_ww.d/main $@
|
||||
@d=$$(mktemp -d "$(CURDIR)/$(BIN)/w6a_ww.d/build.XXXXXX") || exit 1; \
|
||||
rc=0; published=0; \
|
||||
(cd "$$d" && $(CURDIR)/$(BIN)/ww build -o main \
|
||||
-I $(CURDIR)/selfhost/cmd/w6a \
|
||||
$(CURDIR)/selfhost/cmd/w6a/main.ww) || rc=$$?; \
|
||||
rm -rf -- "$$d/main.sepwork" || { echo "$@: scratch cleanup failed" >&2; if [ $$rc -eq 0 ]; then rc=1; fi; }; \
|
||||
if [ $$rc -eq 0 ]; then mv "$$d/main" "$(CURDIR)/$@" && published=1 || rc=$$?; fi; \
|
||||
if [ $$published -eq 0 ]; then rm -f -- "$$d/main" || { echo "$@: output cleanup failed" >&2; if [ $$rc -eq 0 ]; then rc=1; fi; }; fi; \
|
||||
if ! rmdir "$$d"; then echo "$@: workspace cleanup failed" >&2; if [ $$published -eq 1 ]; then rm -f -- "$(CURDIR)/$@" || echo "$@: published-output cleanup failed" >&2; fi; if [ $$rc -eq 0 ]; then rc=1; fi; fi; \
|
||||
exit $$rc
|
||||
|
||||
# ---- ww-side w6l (linker port, exercised by 992_w6l_ww) ---------------
|
||||
# Built like 6a_ww. Needs -I selfhost/cmd/w6l for the local sym/obj/pass/
|
||||
@@ -189,25 +202,36 @@ $(BIN)/w6l_ww: selfhost/cmd/w6l/main.ww selfhost/cmd/w6l/sym.ww \
|
||||
$(BIN)/ww $(BIN)/w6c $(BIN)/w6a $(BIN)/w6l \
|
||||
$(LIB)/libwwrt.a | $(BIN)
|
||||
@mkdir -p $(BIN)/w6l_ww.d
|
||||
cd $(BIN)/w6l_ww.d && $(CURDIR)/$(BIN)/ww build \
|
||||
-o main \
|
||||
-I $(CURDIR)/selfhost/cmd/w6l \
|
||||
$(CURDIR)/selfhost/cmd/w6l/main.ww
|
||||
mv $(BIN)/w6l_ww.d/main $@
|
||||
@d=$$(mktemp -d "$(CURDIR)/$(BIN)/w6l_ww.d/build.XXXXXX") || exit 1; \
|
||||
rc=0; published=0; \
|
||||
(cd "$$d" && $(CURDIR)/$(BIN)/ww build -o main \
|
||||
-I $(CURDIR)/selfhost/cmd/w6l \
|
||||
$(CURDIR)/selfhost/cmd/w6l/main.ww) || rc=$$?; \
|
||||
rm -rf -- "$$d/main.sepwork" || { echo "$@: scratch cleanup failed" >&2; if [ $$rc -eq 0 ]; then rc=1; fi; }; \
|
||||
if [ $$rc -eq 0 ]; then mv "$$d/main" "$(CURDIR)/$@" && published=1 || rc=$$?; fi; \
|
||||
if [ $$published -eq 0 ]; then rm -f -- "$$d/main" || { echo "$@: output cleanup failed" >&2; if [ $$rc -eq 0 ]; then rc=1; fi; }; fi; \
|
||||
if ! rmdir "$$d"; then echo "$@: workspace cleanup failed" >&2; if [ $$published -eq 1 ]; then rm -f -- "$(CURDIR)/$@" || echo "$@: published-output cleanup failed" >&2; fi; if [ $$rc -eq 0 ]; then rc=1; fi; fi; \
|
||||
exit $$rc
|
||||
|
||||
# ---- ww-side ww driver (exercised by 993_ww_ww) ------------------------
|
||||
# The driver pulls in lib/os (default search path) and orchestrates
|
||||
# w6c/w6a/w6l like the C driver.
|
||||
$(BIN)/ww_ww: selfhost/cmd/ww/main.ww lib/os/os.ww lib/rt/malloc.ww \
|
||||
$(BIN)/ww_ww: selfhost/cmd/ww/main.ww lib/os/exec/exec.ww \
|
||||
lib/os/os.ww lib/rt/malloc.ww \
|
||||
lib/time/time.ww lib/strconv/strconv.ww \
|
||||
lib/strings/strings.ww lib/bytes/bytes.ww lib/encoding/utf8/utf8.ww \
|
||||
$(BIN)/ww $(BIN)/w6c $(BIN)/w6a $(BIN)/w6l \
|
||||
$(LIB)/libwwrt.a | $(BIN)
|
||||
@mkdir -p $(BIN)/ww_ww.d
|
||||
cd $(BIN)/ww_ww.d && $(CURDIR)/$(BIN)/ww build \
|
||||
-o main \
|
||||
$(CURDIR)/selfhost/cmd/ww/main.ww
|
||||
mv $(BIN)/ww_ww.d/main $@
|
||||
@d=$$(mktemp -d "$(CURDIR)/$(BIN)/ww_ww.d/build.XXXXXX") || exit 1; \
|
||||
rc=0; published=0; \
|
||||
(cd "$$d" && $(CURDIR)/$(BIN)/ww build -o main \
|
||||
$(CURDIR)/selfhost/cmd/ww/main.ww) || rc=$$?; \
|
||||
rm -rf -- "$$d/main.sepwork" || { echo "$@: scratch cleanup failed" >&2; if [ $$rc -eq 0 ]; then rc=1; fi; }; \
|
||||
if [ $$rc -eq 0 ]; then mv "$$d/main" "$(CURDIR)/$@" && published=1 || rc=$$?; fi; \
|
||||
if [ $$published -eq 0 ]; then rm -f -- "$$d/main" || { echo "$@: output cleanup failed" >&2; if [ $$rc -eq 0 ]; then rc=1; fi; }; fi; \
|
||||
if ! rmdir "$$d"; then echo "$@: workspace cleanup failed" >&2; if [ $$published -eq 1 ]; then rm -f -- "$(CURDIR)/$@" || echo "$@: published-output cleanup failed" >&2; fi; if [ $$rc -eq 0 ]; then rc=1; fi; fi; \
|
||||
exit $$rc
|
||||
|
||||
# ---- runtime (libwwrt.a, assembled by our own w6a) ---------------------
|
||||
$(OBJ)/rt/%.o: rt/%.s $(BIN)/w6a | $(OBJ)/rt
|
||||
|
||||
1037
cmd/ww/main.c
1037
cmd/ww/main.c
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1,227 +0,0 @@
|
||||
/*
|
||||
* 989_pkgcache_concurrent_run — concurrent shared-cache build-correctness
|
||||
* smoke (#104). N distinct root programs that all `import shared` are built
|
||||
* CONCURRENTLY into ONE shared WW_PKGCACHE; every resulting binary must be
|
||||
* byte-IDENTICAL to a reference built in ISOLATION (private cache) and run to
|
||||
* its expected exit. This guards that concurrent `ww build --sep` sharing one
|
||||
* out/.pkgcache produces correct, deterministic binaries — a regression guard
|
||||
* for the cache subsystem under contention (store crash, lock bug, wrong-key
|
||||
* copy, etc.).
|
||||
*
|
||||
* It does NOT prove the temp+rename store is atomic against torn reads: with
|
||||
* content-keying every concurrent cold build MISSES at lookup and STORES (it
|
||||
* never HIT-reads a mid-store entry), so the torn-read window is not forced
|
||||
* here. That race is closed by construction at the cache_store fix site; the
|
||||
* deferred white-box guard is TASK #105.
|
||||
*
|
||||
* Both driver stages (rule 10): the cs and ww references are byte-identical.
|
||||
* Light wwstage-driver test (CLAUDE.md rule 14): all outputs + caches live
|
||||
* under a private /tmp tree, so it is parallel-safe and off every byte-id /
|
||||
* bootstrap gate. Models 989_pkgcache_run conventions.
|
||||
*/
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <unistd.h>
|
||||
#include <sys/wait.h>
|
||||
#include <sys/stat.h>
|
||||
|
||||
#define NPROG 4
|
||||
#define BASE_EXIT 7 /* shared.v() == 7; rootI returns 7 + I */
|
||||
|
||||
static int
|
||||
runwait(const char *cmd)
|
||||
{
|
||||
int rc = system(cmd);
|
||||
if (rc == -1) return -1;
|
||||
if (WIFEXITED(rc)) return WEXITSTATUS(rc);
|
||||
return 1;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
static int
|
||||
slurp(const char *path, char **outbuf, size_t *outlen)
|
||||
{
|
||||
FILE *f = fopen(path, "rb");
|
||||
if (!f) return -1;
|
||||
fseek(f, 0, SEEK_END);
|
||||
long n = ftell(f);
|
||||
fseek(f, 0, SEEK_SET);
|
||||
if (n < 0) { fclose(f); return -1; }
|
||||
char *b = malloc((size_t)n + 1);
|
||||
if (!b) { fclose(f); return -1; }
|
||||
if (fread(b, 1, (size_t)n, f) != (size_t)n) { free(b); fclose(f); return -1; }
|
||||
b[n] = '\0';
|
||||
fclose(f);
|
||||
*outbuf = b;
|
||||
*outlen = (size_t)n;
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int
|
||||
files_eq(const char *a, const char *b)
|
||||
{
|
||||
char *ba = NULL, *bb = NULL;
|
||||
size_t na = 0, nb = 0;
|
||||
if (slurp(a, &ba, &na) < 0 || slurp(b, &bb, &nb) < 0) {
|
||||
free(ba); free(bb);
|
||||
return -1;
|
||||
}
|
||||
int eq = (na == nb && memcmp(ba, bb, na) == 0);
|
||||
free(ba); free(bb);
|
||||
return eq ? 0 : 1;
|
||||
}
|
||||
|
||||
static const char *shared_src =
|
||||
"package shared;\n"
|
||||
"export fn v() i32 = { return 7; };\n";
|
||||
|
||||
int
|
||||
main(void)
|
||||
{
|
||||
const char *bin = absbin();
|
||||
if (!bin) return 1;
|
||||
int fail = 0;
|
||||
char td[64], cmd[8192], batch[32768];
|
||||
char sharedww[1024];
|
||||
|
||||
snprintf(td, sizeof td, "/tmp/wwconc_%d", getpid());
|
||||
snprintf(cmd, sizeof cmd, "rm -rf %s", td);
|
||||
runwait(cmd);
|
||||
snprintf(cmd, sizeof cmd, "mkdir -p %s/shared", td);
|
||||
runwait(cmd);
|
||||
|
||||
snprintf(sharedww, sizeof sharedww, "%s/shared/shared.ww", td);
|
||||
if (write_file(sharedww, shared_src)) { fail++; goto out; }
|
||||
|
||||
/* N distinct roots, each importing the one shared pkg. Distinct return
|
||||
* (7 + I) so each program — and so each clean binary — is distinguishable,
|
||||
* while the contended cache entry (shared) is common to all. */
|
||||
for (int i = 0; i < NPROG; i++) {
|
||||
char rootww[1024], src[256];
|
||||
snprintf(rootww, sizeof rootww, "%s/root%d.ww", td, i);
|
||||
snprintf(src, sizeof src,
|
||||
"package main;\nimport shared;\n"
|
||||
"fn main() i32 = { return shared.v() + %d; };\n", i);
|
||||
if (write_file(rootww, src)) { fail++; goto out; }
|
||||
}
|
||||
|
||||
struct { const char *drv, *tag; } stg[] = {
|
||||
{ "ww", "cs" },
|
||||
{ "ww_ww", "ww" },
|
||||
};
|
||||
|
||||
for (int s = 0; s < 2; s++) {
|
||||
const char *drv = stg[s].drv, *tag = stg[s].tag;
|
||||
char shcache[1024];
|
||||
snprintf(shcache, sizeof shcache, "%s/cache.%s", td, tag);
|
||||
|
||||
/* References: each rootI built ISOLATED (private cache, no contention)
|
||||
* = the clean baseline bytes a concurrent build must reproduce. */
|
||||
for (int i = 0; i < NPROG; i++) {
|
||||
char refcache[1024], refprog[1024], refscr[1024], rootww[1024];
|
||||
snprintf(refcache, sizeof refcache, "%s/refc.%s.%d", td, tag, i);
|
||||
snprintf(refprog, sizeof refprog, "%s/ref.%s.%d", td, tag, i);
|
||||
snprintf(refscr, sizeof refscr, "%s/ref.%s.%d.sepwork", td, tag, i);
|
||||
snprintf(rootww, sizeof rootww, "%s/root%d.ww", td, i);
|
||||
snprintf(cmd, sizeof cmd,
|
||||
"rm -rf %s %s; WW_PKGCACHE='%s' timeout 240 %s/%s build --sep "
|
||||
"-o %s %s >/dev/null 2>&1",
|
||||
refscr, refcache, refcache, bin, drv, refprog, rootww);
|
||||
if (runwait(cmd) != 0) {
|
||||
fprintf(stderr, "concur FAIL[%s]: reference build %d failed\n",
|
||||
drv, i);
|
||||
fail++;
|
||||
}
|
||||
if (runwait(refprog) != BASE_EXIT + i) {
|
||||
fprintf(stderr, "concur FAIL[%s]: reference %d wrong exit\n",
|
||||
drv, i);
|
||||
fail++;
|
||||
}
|
||||
}
|
||||
|
||||
/* Fresh shared cache so every build in the batch MISS-stores `shared`
|
||||
* concurrently → write contention on shared/P.{wwi,o,key}. Distinct
|
||||
* per-prog output so every binary can be verified, not just one. */
|
||||
snprintf(cmd, sizeof cmd, "rm -rf %s", shcache);
|
||||
runwait(cmd);
|
||||
|
||||
size_t off = 0;
|
||||
off += (size_t)snprintf(batch + off, sizeof batch - off,
|
||||
"export WW_PKGCACHE='%s'; ", shcache);
|
||||
for (int i = 0; i < NPROG; i++) {
|
||||
off += (size_t)snprintf(batch + off, sizeof batch - off,
|
||||
"( rm -rf %s/c.%s.%d.sepwork; timeout 240 %s/%s build "
|
||||
"--sep -o %s/c.%s.%d %s/root%d.ww >/dev/null 2>&1 ) & ",
|
||||
td, tag, i, bin, drv,
|
||||
td, tag, i, td, i);
|
||||
}
|
||||
off += (size_t)snprintf(batch + off, sizeof batch - off, "wait");
|
||||
if (off >= sizeof batch) {
|
||||
fprintf(stderr, "concur FAIL: batch cmd truncated\n");
|
||||
fail++; goto out;
|
||||
}
|
||||
runwait(batch);
|
||||
|
||||
for (int i = 0; i < NPROG; i++) {
|
||||
char prog[1024], refprog[1024];
|
||||
snprintf(prog, sizeof prog, "%s/c.%s.%d", td, tag, i);
|
||||
snprintf(refprog, sizeof refprog, "%s/ref.%s.%d", td, tag, i);
|
||||
if (runwait(prog) != BASE_EXIT + i) {
|
||||
fprintf(stderr, "concur FAIL[%s]: concurrent prog i=%d "
|
||||
"wrong/failed exit\n", drv, i);
|
||||
fail++;
|
||||
}
|
||||
if (files_eq(prog, refprog) != 0) {
|
||||
fprintf(stderr, "concur FAIL[%s]: concurrent binary i=%d "
|
||||
"!= isolated reference\n", drv, i);
|
||||
fail++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* rule 10: the cs and ww isolated references are byte-identical. */
|
||||
for (int i = 0; i < NPROG; i++) {
|
||||
char a[1024], b[1024];
|
||||
snprintf(a, sizeof a, "%s/ref.cs.%d", td, i);
|
||||
snprintf(b, sizeof b, "%s/ref.ww.%d", td, i);
|
||||
if (files_eq(a, b) != 0) {
|
||||
fprintf(stderr, "concur FAIL: cs != ww reference %d (rule 10)\n", i);
|
||||
fail++;
|
||||
}
|
||||
}
|
||||
|
||||
out:
|
||||
snprintf(cmd, sizeof cmd, "rm -rf %s", td);
|
||||
runwait(cmd);
|
||||
if (fail) {
|
||||
fprintf(stderr, "concur: %d check(s) failed\n", fail);
|
||||
return 1;
|
||||
}
|
||||
printf("concur: %d concurrent --sep builds sharing one cache, both stages "
|
||||
"— every binary byte-identical to its isolated reference + correct run; "
|
||||
"cs==ww references (rule 10)\n", NPROG);
|
||||
return 0;
|
||||
}
|
||||
@@ -1,247 +0,0 @@
|
||||
/*
|
||||
* 989_pkgcache_poison_run — out/.pkgcache 0-byte-artifact self-heal gate (#10,
|
||||
* BUG-B). A torn producer write (e.g. disk-full mid-copy) can leave a 0-byte
|
||||
* P.wwi/P.o under a SELF-CONSISTENT P.key. Pre-fix the cache served that empty
|
||||
* artifact forever (the key matched and access() saw the file), so every later
|
||||
* build link-failed or produced a wrong binary — a SILENT serve-wrong that
|
||||
* never surfaces on its own. The fix rejects size==0 on BOTH the lookup (read)
|
||||
* and the store (write) side: a poisoned entry self-heals (treated as MISS →
|
||||
* re-derived), and a torn store never commits the key. The guard is symmetric
|
||||
* cstage (filenonempty, stat) / wwstage (cachefilesize, open+filesize) — the
|
||||
* hit/miss DECISION must match (rule 10).
|
||||
*
|
||||
* Target: a 2-level graph root(main) -> leaf in a private temp tree. ROOT is
|
||||
* never cached (it is the build target); `leaf` is the cacheable package. We
|
||||
* seed a warm cache, POISON the cached artifact to 0 bytes, rebuild, and assert
|
||||
* the build self-heals: leaf re-compiles (MISS sentinel) AND the program runs
|
||||
* to its correct value (not an empty/link-failed binary) AND the cache is
|
||||
* re-stored non-empty.
|
||||
*
|
||||
* HIT/MISS sentinel (same as 989_pkgcache_run): a MISS runs w6c → writes
|
||||
* <scratch>/leaf.s; a HIT copies the cached .o/.wwi and skips compose+w6c+w6a,
|
||||
* so no leaf.s. The scratch is wiped before each build so leaf.s presence
|
||||
* reflects exactly the last build.
|
||||
*
|
||||
* Table-driven over WHICH artifact is poisoned: { P.wwi | P.o | both }. Each
|
||||
* row, per stage (ww, ww_ww):
|
||||
* - warm baseline: an unchanged rebuild HITs (leaf.s ABSENT, prog runs) —
|
||||
* proves the cache is genuinely warm before we poison it (non-vacuity);
|
||||
* - POISON the row's artifact(s) to 0 bytes;
|
||||
* - heal rebuild: leaf re-compiles (leaf.s PRESENT = MISS, not served poison)
|
||||
* AND prog runs == EXPECT_EXIT AND both cached artifacts are non-empty again
|
||||
* (re-stored). Pre-fix this row served the 0-byte artifact and the assert
|
||||
* on leaf.s (and on prog exit) fails.
|
||||
*
|
||||
* Light wwstage-driver test (CLAUDE.md rule 14): all build outputs and caches
|
||||
* live under a private /tmp tree, wiped on exit, so it is parallel-safe and off
|
||||
* every byte-id/bootstrap gate. Models 989_pkgcache_run conventions.
|
||||
*/
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <unistd.h>
|
||||
#include <sys/wait.h>
|
||||
#include <sys/stat.h>
|
||||
|
||||
#define EXPECT_EXIT 7
|
||||
|
||||
static int
|
||||
runwait(const char *cmd)
|
||||
{
|
||||
int rc = system(cmd);
|
||||
if (rc == -1) return -1;
|
||||
if (WIFEXITED(rc)) return WEXITSTATUS(rc);
|
||||
return 1;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
/* Truncate `path` to 0 bytes (models a torn producer write under a valid key);
|
||||
* "wb" opens-and-truncates, leaving the file present but empty. */
|
||||
static int
|
||||
truncate0(const char *path)
|
||||
{
|
||||
FILE *f = fopen(path, "wb");
|
||||
if (!f) return -1;
|
||||
fclose(f);
|
||||
return 0;
|
||||
}
|
||||
|
||||
static long
|
||||
fsize(const char *path)
|
||||
{
|
||||
struct stat st;
|
||||
if (stat(path, &st) != 0) return -1;
|
||||
return (long)st.st_size;
|
||||
}
|
||||
|
||||
/* <scratch>/<pkg>.s present ⇒ that package compiled this build (MISS). */
|
||||
static int
|
||||
sdot(const char *scratch, const char *pkg)
|
||||
{
|
||||
char p[1100];
|
||||
snprintf(p, sizeof p, "%s/%s.s", scratch, pkg);
|
||||
return access(p, 0) == 0;
|
||||
}
|
||||
|
||||
static const char *leaf_src =
|
||||
"package leaf;\n"
|
||||
"export fn base() i32 = { return 7; };\n";
|
||||
|
||||
static const char *root_src =
|
||||
"package main;\n"
|
||||
"import leaf;\n"
|
||||
"fn main() i32 = { return leaf.base(); };\n";
|
||||
|
||||
int
|
||||
main(void)
|
||||
{
|
||||
const char *bin = absbin();
|
||||
if (!bin) return 1;
|
||||
int fail = 0;
|
||||
char td[64], cmd[8192];
|
||||
char rootww[1024], leafww[1024];
|
||||
char scratch[1024], prog[1024];
|
||||
|
||||
snprintf(td, sizeof td, "/tmp/wwpkgpoison_%d", getpid());
|
||||
snprintf(cmd, sizeof cmd, "rm -rf %s", td);
|
||||
runwait(cmd);
|
||||
snprintf(cmd, sizeof cmd, "mkdir -p %s/leaf", td);
|
||||
runwait(cmd);
|
||||
|
||||
snprintf(rootww, sizeof rootww, "%s/root.ww", td);
|
||||
snprintf(leafww, sizeof leafww, "%s/leaf/leaf.ww", td);
|
||||
snprintf(prog, sizeof prog, "%s/p", td);
|
||||
snprintf(scratch, sizeof scratch, "%s/p.sepwork", td);
|
||||
|
||||
if (write_file(rootww, root_src) || write_file(leafww, leaf_src)) {
|
||||
fail++; goto out;
|
||||
}
|
||||
|
||||
/* Which cached artifact(s) the row poisons. */
|
||||
struct { const char *tag; int wwi, obj; } rows[] = {
|
||||
{ "P.wwi", 1, 0 },
|
||||
{ "P.o", 0, 1 },
|
||||
{ "both", 1, 1 },
|
||||
};
|
||||
struct { const char *drv; } stg[] = { { "ww" }, { "ww_ww" } };
|
||||
|
||||
for (int s = 0; s < 2; s++) {
|
||||
const char *drv = stg[s].drv;
|
||||
char cache[1024], cwwi[1100], cobj[1100];
|
||||
snprintf(cache, sizeof cache, "%s/cache.%d", td, s);
|
||||
snprintf(cwwi, sizeof cwwi, "%s/leaf/P.wwi", cache);
|
||||
snprintf(cobj, sizeof cobj, "%s/leaf/P.o", cache);
|
||||
|
||||
/* The cstage driver rm -rf's the scratch each build; the wwstage
|
||||
* driver only mkdir's it (#58(a) asymmetry), so a stale leaf.s could
|
||||
* persist and defeat the MISS/HIT sentinel. The gate owns its scratch:
|
||||
* wipe it before every build so a fresh leaf.s reflects the last build. */
|
||||
#define BUILD() do { \
|
||||
snprintf(cmd, sizeof cmd, \
|
||||
"rm -rf %s; WW_PKGCACHE='%s' timeout 240 %s/%s build --sep " \
|
||||
"-o %s %s >/dev/null 2>&1", scratch, cache, bin, drv, prog, \
|
||||
rootww); \
|
||||
} while (0)
|
||||
|
||||
/* fresh cache for this stage, cold-seed it (MISS) */
|
||||
snprintf(cmd, sizeof cmd, "rm -rf %s", cache);
|
||||
runwait(cmd);
|
||||
BUILD();
|
||||
if (runwait(cmd) != 0) {
|
||||
fprintf(stderr, "poison FAIL[%s]: cold seed build failed\n", drv);
|
||||
fail++; continue;
|
||||
}
|
||||
if (!sdot(scratch, "leaf")) {
|
||||
fprintf(stderr, "poison FAIL[%s]: cold seed — leaf did not "
|
||||
"compile (MISS expected)\n", drv);
|
||||
fail++;
|
||||
}
|
||||
if (runwait(prog) != EXPECT_EXIT) {
|
||||
fprintf(stderr, "poison FAIL[%s]: cold seed prog exit != %d\n",
|
||||
drv, EXPECT_EXIT);
|
||||
fail++;
|
||||
}
|
||||
|
||||
for (size_t r = 0; r < sizeof rows / sizeof rows[0]; r++) {
|
||||
/* warm baseline — the cache is warm (heals re-store it), so an
|
||||
* unchanged rebuild must HIT; proves we poison a LIVE entry. */
|
||||
BUILD(); runwait(cmd);
|
||||
if (sdot(scratch, "leaf")) {
|
||||
fprintf(stderr, "poison FAIL[%s/%s]: warm baseline — leaf "
|
||||
"recompiled (cache not warm before poison)\n",
|
||||
drv, rows[r].tag);
|
||||
fail++;
|
||||
}
|
||||
|
||||
/* poison the row's artifact(s) to 0 bytes, key stays valid */
|
||||
if (rows[r].wwi && truncate0(cwwi) != 0) {
|
||||
fprintf(stderr, "poison FAIL[%s/%s]: cannot truncate P.wwi\n",
|
||||
drv, rows[r].tag);
|
||||
fail++;
|
||||
}
|
||||
if (rows[r].obj && truncate0(cobj) != 0) {
|
||||
fprintf(stderr, "poison FAIL[%s/%s]: cannot truncate P.o\n",
|
||||
drv, rows[r].tag);
|
||||
fail++;
|
||||
}
|
||||
|
||||
/* heal rebuild — a 0-byte artifact under a valid key must be
|
||||
* treated as MISS (re-derived), NOT served. */
|
||||
BUILD(); runwait(cmd);
|
||||
if (!sdot(scratch, "leaf")) {
|
||||
fprintf(stderr, "poison FAIL[%s/%s]: served poison — leaf did "
|
||||
"NOT re-derive after 0-byte artifact (silent serve-wrong)\n",
|
||||
drv, rows[r].tag);
|
||||
fail++;
|
||||
}
|
||||
if (runwait(prog) != EXPECT_EXIT) {
|
||||
fprintf(stderr, "poison FAIL[%s/%s]: heal prog exit != %d "
|
||||
"(empty/link-failed binary)\n", drv, rows[r].tag, EXPECT_EXIT);
|
||||
fail++;
|
||||
}
|
||||
/* re-store must leave both artifacts non-empty (write guard let a
|
||||
* good store through, AND a torn store never commits a 0-byte). */
|
||||
if (fsize(cwwi) <= 0 || fsize(cobj) <= 0) {
|
||||
fprintf(stderr, "poison FAIL[%s/%s]: cached artifact still "
|
||||
"0-byte after heal (re-store did not repair)\n",
|
||||
drv, rows[r].tag);
|
||||
fail++;
|
||||
}
|
||||
}
|
||||
#undef BUILD
|
||||
}
|
||||
|
||||
out:
|
||||
snprintf(cmd, sizeof cmd, "rm -rf %s", td);
|
||||
runwait(cmd);
|
||||
if (fail) {
|
||||
fprintf(stderr, "poison: %d check(s) failed\n", fail);
|
||||
return 1;
|
||||
}
|
||||
printf("poison: root->leaf via build_one_sep — a 0-byte cached P.wwi/P.o/both "
|
||||
"under a valid key self-heals (treated as MISS, re-derived to correct "
|
||||
"value, re-stored non-empty) — both stages\n");
|
||||
return 0;
|
||||
}
|
||||
@@ -1,381 +0,0 @@
|
||||
/*
|
||||
* 989_pkgcache_run — M3-tail commit-5b out/.pkgcache content-keyed package
|
||||
* cache gate (#63). COLD, dev-only: the cache is keyed by content, never
|
||||
* mtime, and every bootstrap/byte-id gate cold-compiles (the `--sep` scratch
|
||||
* is wiped each run), so 5b changes NO gate output. This leg certifies the
|
||||
* cache itself, through the REAL `ww` / `ww_ww` drivers.
|
||||
*
|
||||
* Target: a synthetic 3-level graph root(main) -> mid -> leaf in a private
|
||||
* temp tree, so source bytes and the dep interface are fully under test
|
||||
* control. The ROOT is never cached (it is the build target); `mid` and
|
||||
* `leaf` are the cacheable packages. Each stage uses its OWN WW_PKGCACHE dir
|
||||
* (the compiler-binary line of P.key is intentionally stage-specific — it
|
||||
* keys CODEGEN identity — so cs and ww keep separate cache namespaces; the
|
||||
* cacheable OUTPUTS they reuse stay byte-identical, asserted below).
|
||||
*
|
||||
* HIT/MISS sentinel: a cache MISS runs w6c, which writes <scratch>/<pkg>.s;
|
||||
* a HIT copies the cached .o/.wwi and skips compose+w6c+w6a, so no <pkg>.s
|
||||
* is produced. The scratch is wiped each build, so <pkg>.s presence reflects
|
||||
* exactly the last build.
|
||||
*
|
||||
* Legs, per stage (ww, ww_ww):
|
||||
* 1. MISS: a cold build compiles (leaf.s AND mid.s present; prog runs == 5).
|
||||
* 2. HIT (+ non-vacuity baseline): an unchanged rebuild skips both
|
||||
* (leaf.s AND mid.s ABSENT; prog still runs == 5 from cached artifacts).
|
||||
* 3. BUST — source edit: perturb mid.ww -> mid MISS, leaf still HIT;
|
||||
* restore + reseed -> HIT again.
|
||||
* 4. BUST — dep .wwi change: change leaf's EXPORTED interface -> leaf's .wwi
|
||||
* md5 changes -> the importer `mid` (own source unchanged) MISSES via its
|
||||
* dep line; restore + reseed -> HIT again.
|
||||
* 5. BUST — compiler binary md5: corrupt the stored mid/P.key `w6c` line ->
|
||||
* the freshly recomputed manifest mismatches -> mid MISS; the rebuild
|
||||
* rewrites a correct key -> HIT again. (Models a codegen-changed compiler;
|
||||
* a real WW_W6C swap is cstage-only — the wwstage driver hardcodes its
|
||||
* toolchain — so a white-box key-line edit proves the class on both.)
|
||||
* 6. BUST — flag string: corrupt the stored mid/P.key `flags` line -> mid
|
||||
* MISS; rebuild rewrites -> HIT again.
|
||||
*
|
||||
* Cross-stage (rule 10): the cached mid/leaf P.o AND P.wwi are byte-identical
|
||||
* between the cs and ww caches.
|
||||
*
|
||||
* Light wwstage-driver test (CLAUDE.md rule 14): all build outputs and caches
|
||||
* live under a private /tmp tree, so it is parallel-safe and off every
|
||||
* byte-id/bootstrap gate. Models 989_sepbuild_run conventions.
|
||||
*/
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <unistd.h>
|
||||
#include <sys/wait.h>
|
||||
#include <sys/stat.h>
|
||||
|
||||
#define EXPECT_EXIT 5
|
||||
|
||||
static int
|
||||
runwait(const char *cmd)
|
||||
{
|
||||
int rc = system(cmd);
|
||||
if (rc == -1) return -1;
|
||||
if (WIFEXITED(rc)) return WEXITSTATUS(rc);
|
||||
return 1;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
static int
|
||||
slurp(const char *path, char **outbuf, size_t *outlen)
|
||||
{
|
||||
FILE *f = fopen(path, "rb");
|
||||
if (!f) return -1;
|
||||
fseek(f, 0, SEEK_END);
|
||||
long n = ftell(f);
|
||||
fseek(f, 0, SEEK_SET);
|
||||
if (n < 0) { fclose(f); return -1; }
|
||||
char *b = malloc((size_t)n + 1);
|
||||
if (!b) { fclose(f); return -1; }
|
||||
if (fread(b, 1, (size_t)n, f) != (size_t)n) { free(b); fclose(f); return -1; }
|
||||
b[n] = '\0';
|
||||
fclose(f);
|
||||
*outbuf = b;
|
||||
*outlen = (size_t)n;
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int
|
||||
files_eq(const char *a, const char *b)
|
||||
{
|
||||
char *ba = NULL, *bb = NULL;
|
||||
size_t na = 0, nb = 0;
|
||||
if (slurp(a, &ba, &na) < 0 || slurp(b, &bb, &nb) < 0) {
|
||||
free(ba); free(bb);
|
||||
return -1;
|
||||
}
|
||||
int eq = (na == nb && memcmp(ba, bb, na) == 0);
|
||||
free(ba); free(bb);
|
||||
return eq ? 0 : 1;
|
||||
}
|
||||
|
||||
/* Flip one byte inside the line of `path` that starts with `prefix` (keeping
|
||||
* the line length so the mutation isolates that one input class, not the
|
||||
* blob length). Returns 0 on success. */
|
||||
static int
|
||||
mutate_keyline(const char *path, const char *prefix)
|
||||
{
|
||||
char *buf = NULL;
|
||||
size_t n = 0;
|
||||
if (slurp(path, &buf, &n) < 0) return -1;
|
||||
size_t pl = strlen(prefix);
|
||||
char *hit = NULL;
|
||||
for (size_t i = 0; i < n; i++) {
|
||||
if ((i == 0 || buf[i-1] == '\n') && i + pl <= n &&
|
||||
memcmp(buf + i, prefix, pl) == 0) {
|
||||
hit = buf + i + pl;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!hit || hit >= buf + n || *hit == '\n') { free(buf); return -1; }
|
||||
*hit = (*hit == 'a') ? 'b' : 'a';
|
||||
FILE *f = fopen(path, "wb");
|
||||
if (!f) { free(buf); return -1; }
|
||||
fwrite(buf, 1, n, f);
|
||||
fclose(f);
|
||||
free(buf);
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* <scratch>/<pkg>.s present ⇒ that package compiled this build (MISS). */
|
||||
static int
|
||||
sdot(const char *scratch, const char *pkg)
|
||||
{
|
||||
char p[1100];
|
||||
snprintf(p, sizeof p, "%s/%s.s", scratch, pkg);
|
||||
return access(p, 0) == 0;
|
||||
}
|
||||
|
||||
static const char *leaf_v1 =
|
||||
"package leaf;\n"
|
||||
"export fn base() i32 = { return 3; };\n";
|
||||
/* a CHANGED EXPORTED interface (extra export) → leaf's .wwi md5 changes. */
|
||||
static const char *leaf_v2 =
|
||||
"package leaf;\n"
|
||||
"export fn base() i32 = { return 3; };\n"
|
||||
"export fn extra() i32 = { return 9; };\n";
|
||||
|
||||
static const char *mid_v1 =
|
||||
"package mid;\n"
|
||||
"import leaf;\n"
|
||||
"export fn val() i32 = { return leaf.base() + 2; };\n";
|
||||
/* mid source perturbed by a comment — busts mid's src md5 without changing
|
||||
* codegen (so the program result is preserved). */
|
||||
static const char *mid_v2 =
|
||||
"package mid;\n"
|
||||
"import leaf;\n"
|
||||
"// 5b src-edit bust probe\n"
|
||||
"export fn val() i32 = { return leaf.base() + 2; };\n";
|
||||
|
||||
static const char *root_src =
|
||||
"package main;\n"
|
||||
"import mid;\n"
|
||||
"fn main() i32 = { return mid.val(); };\n";
|
||||
|
||||
int
|
||||
main(void)
|
||||
{
|
||||
const char *bin = absbin();
|
||||
if (!bin) return 1;
|
||||
int fail = 0;
|
||||
char td[64], cmd[8192];
|
||||
char rootww[1024], midww[1024], leafww[1024];
|
||||
char scratch[1024], prog[1024];
|
||||
char cache_cs[1024], cache_ww[1024];
|
||||
|
||||
snprintf(td, sizeof td, "/tmp/wwpkgc_%d", getpid());
|
||||
snprintf(cmd, sizeof cmd, "rm -rf %s", td);
|
||||
runwait(cmd);
|
||||
snprintf(cmd, sizeof cmd, "mkdir -p %s/mid %s/leaf", td, td);
|
||||
runwait(cmd);
|
||||
|
||||
snprintf(rootww, sizeof rootww, "%s/root.ww", td);
|
||||
snprintf(midww, sizeof midww, "%s/mid/mid.ww", td);
|
||||
snprintf(leafww, sizeof leafww, "%s/leaf/leaf.ww", td);
|
||||
snprintf(prog, sizeof prog, "%s/p", td);
|
||||
snprintf(scratch, sizeof scratch, "%s/p.sepwork", td);
|
||||
snprintf(cache_cs, sizeof cache_cs, "%s/cache.cs", td);
|
||||
snprintf(cache_ww, sizeof cache_ww, "%s/cache.ww", td);
|
||||
|
||||
if (write_file(rootww, root_src) || write_file(midww, mid_v1) ||
|
||||
write_file(leafww, leaf_v1)) { fail++; goto out; }
|
||||
|
||||
struct { const char *drv, *cache; } stg[] = {
|
||||
{ "ww", cache_cs },
|
||||
{ "ww_ww", cache_ww },
|
||||
};
|
||||
|
||||
for (int s = 0; s < 2; s++) {
|
||||
const char *drv = stg[s].drv;
|
||||
const char *cache = stg[s].cache;
|
||||
char midkey[1100];
|
||||
snprintf(midkey, sizeof midkey, "%s/mid/P.key", cache);
|
||||
/* The cstage driver rm -rf's the scratch each build; the wwstage
|
||||
* driver only mkdir's it (pre-existing #58(a) asymmetry), so a
|
||||
* stale <pkg>.s could persist and defeat the MISS/HIT sentinel.
|
||||
* The gate owns its scratch: wipe it before every build so a
|
||||
* fresh <pkg>.s reflects exactly the last compile. */
|
||||
#define BUILD() do { \
|
||||
snprintf(cmd, sizeof cmd, \
|
||||
"rm -rf %s; WW_PKGCACHE='%s' timeout 240 %s/%s build --sep " \
|
||||
"-o %s %s >/dev/null 2>&1", scratch, cache, bin, drv, prog, \
|
||||
rootww); \
|
||||
} while (0)
|
||||
#define SDOT(pkg) sdot(scratch, (pkg))
|
||||
|
||||
/* fresh cache for this stage */
|
||||
snprintf(cmd, sizeof cmd, "rm -rf %s", cache);
|
||||
runwait(cmd);
|
||||
/* sources back to v1 (a prior stage's restore may have left them) */
|
||||
write_file(midww, mid_v1);
|
||||
write_file(leafww, leaf_v1);
|
||||
|
||||
/* leg 1 — MISS: cold build compiles both, prog runs */
|
||||
BUILD();
|
||||
if (runwait(cmd) != 0) {
|
||||
fprintf(stderr, "pkgcache FAIL[%s]: cold build failed\n", drv);
|
||||
fail++; continue;
|
||||
}
|
||||
if (!SDOT("leaf") || !SDOT("mid")) {
|
||||
fprintf(stderr, "pkgcache FAIL[%s]: leg1 MISS — leaf/mid did "
|
||||
"not compile\n", drv);
|
||||
fail++;
|
||||
}
|
||||
if (runwait(prog) != EXPECT_EXIT) {
|
||||
fprintf(stderr, "pkgcache FAIL[%s]: leg1 prog exit != %d\n",
|
||||
drv, EXPECT_EXIT);
|
||||
fail++;
|
||||
}
|
||||
|
||||
/* leg 2 — HIT + non-vacuity: unchanged rebuild skips both */
|
||||
BUILD();
|
||||
runwait(cmd);
|
||||
if (SDOT("leaf") || SDOT("mid")) {
|
||||
fprintf(stderr, "pkgcache FAIL[%s]: leg2 HIT — recompiled an "
|
||||
"unchanged package (cache vacuous)\n", drv);
|
||||
fail++;
|
||||
}
|
||||
if (runwait(prog) != EXPECT_EXIT) {
|
||||
fprintf(stderr, "pkgcache FAIL[%s]: leg2 prog exit != %d "
|
||||
"(cached artifact unusable)\n", drv, EXPECT_EXIT);
|
||||
fail++;
|
||||
}
|
||||
|
||||
/* leg 3 — BUST source edit: mid MISS, leaf still HIT */
|
||||
write_file(midww, mid_v2);
|
||||
BUILD();
|
||||
runwait(cmd);
|
||||
if (!SDOT("mid") || SDOT("leaf")) {
|
||||
fprintf(stderr, "pkgcache FAIL[%s]: leg3 src-edit — expected "
|
||||
"mid MISS + leaf HIT\n", drv);
|
||||
fail++;
|
||||
}
|
||||
/* restore + reseed, then prove HIT returns */
|
||||
write_file(midww, mid_v1);
|
||||
BUILD(); runwait(cmd); /* reseeds the v1 key (MISS) */
|
||||
BUILD(); runwait(cmd); /* must HIT now */
|
||||
if (SDOT("mid")) {
|
||||
fprintf(stderr, "pkgcache FAIL[%s]: leg3 non-vacuity — mid did "
|
||||
"not HIT after restore\n", drv);
|
||||
fail++;
|
||||
}
|
||||
|
||||
/* leg 4 — BUST dep .wwi: change leaf interface → importer mid MISSES */
|
||||
write_file(leafww, leaf_v2);
|
||||
BUILD();
|
||||
runwait(cmd);
|
||||
if (!SDOT("leaf") || !SDOT("mid")) {
|
||||
fprintf(stderr, "pkgcache FAIL[%s]: leg4 dep-.wwi — leaf "
|
||||
"interface change did not bust importer mid\n", drv);
|
||||
fail++;
|
||||
}
|
||||
write_file(leafww, leaf_v1);
|
||||
BUILD(); runwait(cmd); /* reseed v1 */
|
||||
BUILD(); runwait(cmd); /* must HIT now */
|
||||
if (SDOT("leaf") || SDOT("mid")) {
|
||||
fprintf(stderr, "pkgcache FAIL[%s]: leg4 non-vacuity — did not "
|
||||
"HIT after restore\n", drv);
|
||||
fail++;
|
||||
}
|
||||
|
||||
/* leg 5 — BUST compiler md5: corrupt the stored mid w6c line */
|
||||
if (mutate_keyline(midkey, "w6c ") != 0) {
|
||||
fprintf(stderr, "pkgcache FAIL[%s]: leg5 — no w6c line in "
|
||||
"mid/P.key\n", drv);
|
||||
fail++;
|
||||
}
|
||||
BUILD();
|
||||
runwait(cmd);
|
||||
if (!SDOT("mid") || SDOT("leaf")) {
|
||||
fprintf(stderr, "pkgcache FAIL[%s]: leg5 compiler-md5 — corrupt "
|
||||
"w6c line did not bust mid (only)\n", drv);
|
||||
fail++;
|
||||
}
|
||||
BUILD(); runwait(cmd); /* key rewritten → must HIT */
|
||||
if (SDOT("mid")) {
|
||||
fprintf(stderr, "pkgcache FAIL[%s]: leg5 non-vacuity — mid did "
|
||||
"not HIT after key rewrite\n", drv);
|
||||
fail++;
|
||||
}
|
||||
|
||||
/* leg 6 — BUST flag string: corrupt the stored mid flags line */
|
||||
if (mutate_keyline(midkey, "flags ") != 0) {
|
||||
fprintf(stderr, "pkgcache FAIL[%s]: leg6 — no flags line in "
|
||||
"mid/P.key\n", drv);
|
||||
fail++;
|
||||
}
|
||||
BUILD();
|
||||
runwait(cmd);
|
||||
if (!SDOT("mid")) {
|
||||
fprintf(stderr, "pkgcache FAIL[%s]: leg6 flags — corrupt flags "
|
||||
"line did not bust mid\n", drv);
|
||||
fail++;
|
||||
}
|
||||
BUILD(); runwait(cmd);
|
||||
if (SDOT("mid")) {
|
||||
fprintf(stderr, "pkgcache FAIL[%s]: leg6 non-vacuity — mid did "
|
||||
"not HIT after key rewrite\n", drv);
|
||||
fail++;
|
||||
}
|
||||
#undef BUILD
|
||||
#undef SDOT
|
||||
}
|
||||
|
||||
/* cross-stage byte-id (rule 10): cached OUTPUTS identical cs==ww. The
|
||||
* caches end in the v1/HIT state from each stage's leg 6 tail. */
|
||||
{
|
||||
const char *pkgs[] = { "mid", "leaf" };
|
||||
const char *arts[] = { "P.o", "P.wwi" };
|
||||
for (int i = 0; i < 2; i++)
|
||||
for (int k = 0; k < 2; k++) {
|
||||
char a[1100], b[1100];
|
||||
snprintf(a, sizeof a, "%s/%s/%s", cache_cs, pkgs[i], arts[k]);
|
||||
snprintf(b, sizeof b, "%s/%s/%s", cache_ww, pkgs[i], arts[k]);
|
||||
if (files_eq(a, b) != 0) {
|
||||
fprintf(stderr, "pkgcache FAIL: cs != ww for cached "
|
||||
"%s/%s (rule 10)\n", pkgs[i], arts[k]);
|
||||
fail++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
out:
|
||||
snprintf(cmd, sizeof cmd, "rm -rf %s", td);
|
||||
runwait(cmd);
|
||||
if (fail) {
|
||||
fprintf(stderr, "pkgcache: %d check(s) failed\n", fail);
|
||||
return 1;
|
||||
}
|
||||
printf("pkgcache: 3-level root->mid->leaf via build_one_sep — MISS "
|
||||
"compiles, HIT skips (non-vacuous, prog runs), and key busts on each "
|
||||
"input class (src / dep .wwi / compiler md5 / flags) — both stages; "
|
||||
"cached P.o/P.wwi byte-identical cs==ww\n");
|
||||
return 0;
|
||||
}
|
||||
Reference in New Issue
Block a user