/* * 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 /.s; * a HIT copies the cached .o/.wwi and skips compose+w6c+w6a, so no .s * is produced. The scratch is wiped each build, so .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 #include #include #include #include #include #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; } /* /.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 .s could persist and defeat the MISS/HIT sentinel. * The gate owns its scratch: wipe it before every build so a * fresh .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; }