/* * 989_separchive_run — M3-tail commit-5a gate (#46, task #62): the * per-package `.a` substrate + its archive-path #31 dup-detect, both * stages, COLD. Commit 5a makes `ww build --sep` wrap each DEP package's * `.o` in a deterministic single-member `.a` (cstage archive_o / * wwstage archiveo) and link the ROOT as a positional `.o` (force-loaded) * + dep `.a` reverse-topo + libwwrt.a. The #31 dup the selective pull * would mask is caught by a post-pull PASS 3 in w6l/w6l_ww load_archive. * * Legs (all COLD — `.sepwork` is wiped each run): * 1. POSITIVE: root→helper builds + runs exit 7, BOTH stages, through * the `.a` link path (root `.o` + helper `.a`). * 2. ★ DETERMINISM (ken, load-bearing): re-archive the same package 3× * → byte-identical `.a` (proves zeroed mtime/uid/gid + fixed mode + * fixed member name; a floating md5 would poison the 5b cache key). * 3. ★ cs `.a` == ww `.a` (rule 10, the NEW byte-id substrate ken binds): * the dep `.a` from `ww --sep` is byte-identical to the one from * `ww_ww --sep`. * 4. ★ #31 dup THROUGH the `.a` path (ken #263 + D3, the leg that proves * PASS 3 closed the selective-pull hole): two DEP packages each export * the same link symbol via `@symbol("dup_sym")`, referenced by root. * One dep's member is pulled; the OTHER lands UNPULLED and defines an * already-`defined` name — exactly what selective-pull skips. BOTH * stages must (exit≠0) ∧ (stderr contains "duplicate symbol") ∧ (no * partial binary). NON-VACUITY flip: give the second dep a DISTINCT * symbol → both stages build + run (exit 3), proving the leg actually * discriminates (without PASS 3 the dup leg would WRONGLY pass green). * 5. ACHIEVABLE-parity posture (same as c4's 989_sepcycle_dup, because * #61 is a separate commit): cs vs ww is `both exit≠0 ∧ both stderrs * contain "duplicate symbol"` — NOT exact-stderr-equal (cstage names * `: duplicate symbol `, wwstage the bare form). * * Light wwstage-driver test (CLAUDE.md rule 14): all fixtures + scratch * live under /tmp, COLD each run. Models 989_sepbuild_run.c conventions. */ #include #include #include #include #include #include #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 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 int file_has(const char *path, const char *needle) { char *b = NULL; size_t n = 0; if (slurp(path, &b, &n) < 0) return 0; int found = (strstr(b, needle) != NULL); free(b); return found; } 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; } int main(void) { const char *bin = absbin(); if (!bin) return 1; char td[64], cmd[8192]; int fail = 0; snprintf(td, sizeof td, "/tmp/wwar_%d", getpid()); snprintf(cmd, sizeof cmd, "rm -rf %s", td); runwait(cmd); mkdir(td, 0755); /* ---- root → helper fixture (one dep `.a`) ------------------------ */ char helpdir[1024], helpww[1100], rootww[1100]; snprintf(helpdir, sizeof helpdir, "%s/helper", td); mkdir(helpdir, 0755); snprintf(helpww, sizeof helpww, "%s/h.ww", helpdir); snprintf(rootww, sizeof rootww, "%s/root.ww", td); if (write_file(helpww, "package helper;\n" "export fn val() i32 = { return 7; };\n") || write_file(rootww, "package main;\n" "import helper;\n" "fn main() i32 = { return helper.val(); };\n")) { fail++; goto out; } struct { const char *drv, *tag; char prog[1024]; } stg[] = { { "ww", "cs", {0} }, { "ww_ww", "ww", {0} } }; /* Leg 1: POSITIVE build + run through the `.a` path, both stages. */ for (int s = 0; s < 2; s++) { snprintf(stg[s].prog, sizeof stg[s].prog, "%s/prog.%s", td, stg[s].tag); snprintf(cmd, sizeof cmd, "timeout 240 %s/%s build --sep -I %s -o %s %s >/dev/null 2>&1", bin, stg[s].drv, td, stg[s].prog, rootww); if (runwait(cmd) != 0) { fprintf(stderr, "separchive FAIL: %s build --sep\n", stg[s].drv); fail++; continue; } int rc = runwait(stg[s].prog); if (rc != EXPECT_EXIT) { fprintf(stderr, "separchive FAIL: %s prog exit=%d expected %d\n", stg[s].drv, rc, EXPECT_EXIT); fail++; } } /* Root is a positional `.o` (force-loaded), so NO __root.a exists; * the dep is the only `.a`. Assert the layout the driver produced. */ { char roota[1100], rooto[1100], helpa[1100]; snprintf(roota, sizeof roota, "%s/prog.cs.sepwork/__root.a", td); snprintf(rooto, sizeof rooto, "%s/prog.cs.sepwork/__root.o", td); snprintf(helpa, sizeof helpa, "%s/prog.cs.sepwork/helper.a", td); if (access(roota, 0) == 0) { fprintf(stderr, "separchive FAIL: root wrapped in .a " "(should stay positional .o)\n"); fail++; } if (access(rooto, 0) != 0) { fprintf(stderr, "separchive FAIL: missing root .o\n"); fail++; } if (access(helpa, 0) != 0) { fprintf(stderr, "separchive FAIL: missing dep helper.a\n"); fail++; } } /* Leg 3: cs `.a` == ww `.a` (rule 10, the new byte-id substrate). */ { char a[1100], b[1100]; snprintf(a, sizeof a, "%s/prog.cs.sepwork/helper.a", td); snprintf(b, sizeof b, "%s/prog.ww.sepwork/helper.a", td); if (files_eq(a, b) != 0) { fprintf(stderr, "separchive FAIL: cs helper.a != ww helper.a " "(rule 10 .a byte-id)\n"); fail++; } } /* Leg 2: DETERMINISM — re-archive the same package 3× (cold each), * the `.a` must be byte-identical (zeroed mtime/uid/gid, fixed mode, * fixed member name). Copy each build's helper.a aside, compare. */ { char det[3][1100]; int ok = 1; for (int i = 0; i < 3; i++) { char prog[1100]; snprintf(prog, sizeof prog, "%s/det%d", td, i); snprintf(cmd, sizeof cmd, "timeout 240 %s/ww build --sep -I %s -o %s %s >/dev/null 2>&1", bin, td, prog, rootww); if (runwait(cmd) != 0) { ok = 0; break; } snprintf(det[i], sizeof det[i], "%s/det%d.sepwork/helper.a", td, i); } if (!ok) { fprintf(stderr, "separchive FAIL: determinism build\n"); fail++; } else if (files_eq(det[0], det[1]) != 0 || files_eq(det[1], det[2]) != 0) { fprintf(stderr, "separchive FAIL: .a not deterministic across " "3 builds (floating md5 → poisons 5b cache)\n"); fail++; } } /* ---- Leg 4: #31 dup THROUGH the `.a` path (PASS 3) --------------- */ /* Two DEP packages each export the SAME link symbol via @symbol; * root references both. One member is pulled, the other lands * UNPULLED defining an already-`defined` name → the masked dup PASS 3 * catches (#53 path-qualifies normal exports, so @symbol is the * cleanest forced cross-package clash). */ char pkgad[1024], pkgbd[1024], paww[1100], pbww[1100], droot[1100]; snprintf(pkgad, sizeof pkgad, "%s/pkga", td); snprintf(pkgbd, sizeof pkgbd, "%s/pkgb", td); mkdir(pkgad, 0755); mkdir(pkgbd, 0755); snprintf(paww, sizeof paww, "%s/a.ww", pkgad); snprintf(pbww, sizeof pbww, "%s/b.ww", pkgbd); snprintf(droot, sizeof droot, "%s/droot.ww", td); if (write_file(paww, "package pkga;\n" "@symbol(\"dup_sym\") export fn afn() i32 = { return 1; };\n") || write_file(pbww, "package pkgb;\n" "@symbol(\"dup_sym\") export fn bfn() i32 = { return 2; };\n") || write_file(droot, "package main;\n" "import pkga;\n" "import pkgb;\n" "fn main() i32 = { return pkga.afn() + pkgb.bfn(); };\n")) { fail++; goto out; } for (int s = 0; s < 2; s++) { char prog[1100], errf[1100]; snprintf(prog, sizeof prog, "%s/dup.%s", td, stg[s].tag); snprintf(errf, sizeof errf, "%s/dup.%s.err", td, stg[s].tag); snprintf(cmd, sizeof cmd, "timeout 240 %s/%s build --sep -I %s -o %s %s >/dev/null 2>%s", bin, stg[s].drv, td, prog, droot, errf); int rc = runwait(cmd); if (rc == 0) { fprintf(stderr, "separchive FAIL: %s accepted a masked " "cross-pkg dup through .a (exit 0 — PASS 3 missing?)\n", stg[s].drv); fail++; } if (!file_has(errf, "duplicate symbol")) { fprintf(stderr, "separchive FAIL: %s missing 'duplicate " "symbol' on the .a dup path\n", stg[s].drv); fail++; } if (access(prog, 0) == 0) { fprintf(stderr, "separchive FAIL: %s produced a partial " "binary on the dup reject\n", stg[s].drv); fail++; } } /* Leg 4 NON-VACUITY: give pkgb a DISTINCT symbol → no collision → * both stages build + run (exit 3 = 1 + 2). Proves the dup leg * actually discriminates (not a vacuous always-fail). */ if (write_file(pbww, "package pkgb;\n" "@symbol(\"uniq_sym\") export fn bfn() i32 = { return 2; };\n")) { fail++; goto out; } for (int s = 0; s < 2; s++) { char prog[1100]; snprintf(prog, sizeof prog, "%s/ok.%s", td, stg[s].tag); snprintf(cmd, sizeof cmd, "timeout 240 %s/%s build --sep -I %s -o %s %s >/dev/null 2>&1", bin, stg[s].drv, td, prog, droot); if (runwait(cmd) != 0) { fprintf(stderr, "separchive FAIL(non-vacuity): %s could not " "build the distinct-symbol graph\n", stg[s].drv); fail++; continue; } int rc = runwait(prog); if (rc != 3) { fprintf(stderr, "separchive FAIL(non-vacuity): %s prog " "exit=%d expected 3\n", stg[s].drv, rc); fail++; } } out: snprintf(cmd, sizeof cmd, "rm -rf %s", td); runwait(cmd); if (fail) { fprintf(stderr, "separchive: %d check(s) failed\n", fail); return 1; } printf("separchive: per-pkg .a (root=.o force-load, deps=.a) build+run " "(exit %d) + .a determinism (3x identical) + cs.a==ww.a (rule 10) + " "#31 masked-dup reject THROUGH .a (PASS 3, both stages loud+non-zero, " "non-vacuity flip exit 3)\n", EXPECT_EXIT); return 0; }