Every surviving carrier now owns its artifacts: checked mkdir/mkdtemp/ fopen acquisition, one all-exit cleanup funnel per carrier, ENOENT- tolerant checked unlinks, exact-path deletion (rm -rf only for an owned pid-keyed dir or a .sepwork beneath one), and cleanup failure fails a passing carrier without overwriting its diagnostic. In the same pass the carriers adapt to the driver contract this branch lands: --sep and WW_PKGCACHE are gone, -S and the /tmp/ww_run_<pid> scratch contract are asserted, and rows whose runtime or reject coverage moved to test/wcc/data fixtures or test/lang @test owners are trimmed to the byte/artifact/diagnostic observations only they can make. Repair and adaptation ride together because most files interleave both in the same hunks; splitting would manufacture intermediate carrier states that never existed and cannot run against either driver.
417 lines
15 KiB
C
417 lines
15 KiB
C
/*
|
||
* 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` 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 (fresh output stems under an invocation-owned root; retained
|
||
* `<stem>.sepwork` is inspected, then removed by final carrier cleanup):
|
||
* 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` is byte-identical to the one from
|
||
* `ww_ww`.
|
||
* 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
|
||
* `<path>: duplicate symbol <sym>`, 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 <stdio.h>
|
||
#include <stdlib.h>
|
||
#include <string.h>
|
||
#include <unistd.h>
|
||
#include <sys/wait.h>
|
||
#include <sys/stat.h>
|
||
#include <errno.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
|
||
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, have_helpdir = 0, have_pkgad = 0, have_pkgbd = 0;
|
||
|
||
snprintf(td, sizeof td, "/tmp/wwar_%d", getpid());
|
||
if (mkdir(td, 0755) != 0) {
|
||
fprintf(stderr, "separchive FAIL: cannot acquire %s\n", td);
|
||
return 1;
|
||
}
|
||
|
||
/* ---- root → helper fixture (one dep `.a`) ------------------------ */
|
||
char helpdir[1024], helpww[1100], rootww[1100];
|
||
snprintf(helpdir, sizeof helpdir, "%s/helper", td);
|
||
if (mkdir(helpdir, 0755) != 0) {
|
||
fprintf(stderr, "separchive FAIL: cannot create %s\n", helpdir);
|
||
fail++; goto out;
|
||
}
|
||
have_helpdir = 1;
|
||
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 -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\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 -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);
|
||
if (mkdir(pkgad, 0755) != 0) {
|
||
fprintf(stderr, "separchive FAIL: cannot create %s\n", pkgad);
|
||
fail++; goto out;
|
||
}
|
||
have_pkgad = 1;
|
||
if (mkdir(pkgbd, 0755) != 0) {
|
||
fprintf(stderr, "separchive FAIL: cannot create %s\n", pkgbd);
|
||
fail++; goto out;
|
||
}
|
||
have_pkgbd = 1;
|
||
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 -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 -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/prog.cs.sepwork' '%s/prog.ww.sepwork' "
|
||
"'%s/det0.sepwork' '%s/det1.sepwork' '%s/det2.sepwork' "
|
||
"'%s/dup.cs.sepwork' '%s/dup.ww.sepwork' "
|
||
"'%s/ok.cs.sepwork' '%s/ok.ww.sepwork'",
|
||
td, td, td, td, td, td, td, td, td);
|
||
if (runwait(cmd) != 0) {
|
||
fprintf(stderr, "separchive FAIL: cannot clean .sepwork trees\n");
|
||
fail++;
|
||
}
|
||
{
|
||
char path[1200];
|
||
snprintf(path, sizeof path, "%s/root.ww", td);
|
||
if (unlink(path) != 0 && errno != ENOENT) { fprintf(stderr, "separchive FAIL: cannot remove %s\n", path); fail++; }
|
||
snprintf(path, sizeof path, "%s/droot.ww", td);
|
||
if (unlink(path) != 0 && errno != ENOENT) { fprintf(stderr, "separchive FAIL: cannot remove %s\n", path); fail++; }
|
||
snprintf(path, sizeof path, "%s/prog.cs", td);
|
||
if (unlink(path) != 0 && errno != ENOENT) { fprintf(stderr, "separchive FAIL: cannot remove %s\n", path); fail++; }
|
||
snprintf(path, sizeof path, "%s/prog.ww", td);
|
||
if (unlink(path) != 0 && errno != ENOENT) { fprintf(stderr, "separchive FAIL: cannot remove %s\n", path); fail++; }
|
||
snprintf(path, sizeof path, "%s/det0", td);
|
||
if (unlink(path) != 0 && errno != ENOENT) { fprintf(stderr, "separchive FAIL: cannot remove %s\n", path); fail++; }
|
||
snprintf(path, sizeof path, "%s/det1", td);
|
||
if (unlink(path) != 0 && errno != ENOENT) { fprintf(stderr, "separchive FAIL: cannot remove %s\n", path); fail++; }
|
||
snprintf(path, sizeof path, "%s/det2", td);
|
||
if (unlink(path) != 0 && errno != ENOENT) { fprintf(stderr, "separchive FAIL: cannot remove %s\n", path); fail++; }
|
||
snprintf(path, sizeof path, "%s/dup.cs", td);
|
||
if (unlink(path) != 0 && errno != ENOENT) { fprintf(stderr, "separchive FAIL: cannot remove %s\n", path); fail++; }
|
||
snprintf(path, sizeof path, "%s/dup.ww", td);
|
||
if (unlink(path) != 0 && errno != ENOENT) { fprintf(stderr, "separchive FAIL: cannot remove %s\n", path); fail++; }
|
||
snprintf(path, sizeof path, "%s/dup.cs.err", td);
|
||
if (unlink(path) != 0 && errno != ENOENT) { fprintf(stderr, "separchive FAIL: cannot remove %s\n", path); fail++; }
|
||
snprintf(path, sizeof path, "%s/dup.ww.err", td);
|
||
if (unlink(path) != 0 && errno != ENOENT) { fprintf(stderr, "separchive FAIL: cannot remove %s\n", path); fail++; }
|
||
snprintf(path, sizeof path, "%s/ok.cs", td);
|
||
if (unlink(path) != 0 && errno != ENOENT) { fprintf(stderr, "separchive FAIL: cannot remove %s\n", path); fail++; }
|
||
snprintf(path, sizeof path, "%s/ok.ww", td);
|
||
if (unlink(path) != 0 && errno != ENOENT) { fprintf(stderr, "separchive FAIL: cannot remove %s\n", path); fail++; }
|
||
}
|
||
if (have_helpdir) {
|
||
if (unlink(helpww) != 0 && errno != ENOENT) {
|
||
fprintf(stderr, "separchive FAIL: cannot remove %s\n", helpww);
|
||
fail++;
|
||
}
|
||
}
|
||
if (have_pkgad) {
|
||
char path[1200];
|
||
snprintf(path, sizeof path, "%s/a.ww", pkgad);
|
||
if (unlink(path) != 0 && errno != ENOENT) {
|
||
fprintf(stderr, "separchive FAIL: cannot remove %s\n", path);
|
||
fail++;
|
||
}
|
||
}
|
||
if (have_pkgbd) {
|
||
char path[1200];
|
||
snprintf(path, sizeof path, "%s/b.ww", pkgbd);
|
||
if (unlink(path) != 0 && errno != ENOENT) {
|
||
fprintf(stderr, "separchive FAIL: cannot remove %s\n", path);
|
||
fail++;
|
||
}
|
||
}
|
||
if (have_helpdir && rmdir(helpdir) != 0 && errno != ENOENT) {
|
||
fprintf(stderr, "separchive FAIL: cannot remove %s\n", helpdir);
|
||
fail++;
|
||
}
|
||
if (have_pkgad && rmdir(pkgad) != 0 && errno != ENOENT) {
|
||
fprintf(stderr, "separchive FAIL: cannot remove %s\n", pkgad);
|
||
fail++;
|
||
}
|
||
if (have_pkgbd && rmdir(pkgbd) != 0 && errno != ENOENT) {
|
||
fprintf(stderr, "separchive FAIL: cannot remove %s\n", pkgbd);
|
||
fail++;
|
||
}
|
||
if (rmdir(td) != 0 && errno != ENOENT) {
|
||
fprintf(stderr, "separchive FAIL: cannot remove %s\n", td);
|
||
fail++;
|
||
}
|
||
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;
|
||
}
|