wcc/ww: self-hosted deterministic ar-writer + archive dup-detect (M3-tail c5a, #62)
Per-package .a archives are written by a self-hosted deterministic ar writer (zeroed mtime/uid/gid, fixed mode 100644, stable member order) so cstage and wwstage emit byte-identical archives. The linker force-loads the root .o positionally and pulls deps from .a; a post-pull PASS-3 over unloaded members reports duplicate symbols through the archive (#31). Both stages mirrored (cmd/ + selfhost/). USER-ruled D2 (self-hosted ar writer); pike P1/P2 link model. Gate 989_separchive_run proves cs.a==ww.a byte-identity, 3x-determinism, link-consumes-.a (exit 7), and the masked-dup-through-.a loud fire.
This commit is contained in:
11
Makefile
11
Makefile
@@ -565,6 +565,7 @@ TESTS = $(BIN)/test_smoke $(BIN)/test_lex $(BIN)/test_parse $(BIN)/test_check \
|
||||
$(BIN)/test_m3sep_run \
|
||||
$(BIN)/test_sepbuild_run \
|
||||
$(BIN)/test_sepcycle_dup \
|
||||
$(BIN)/test_separchive_run \
|
||||
$(BIN)/test_floatlit_run \
|
||||
$(BIN)/test_checked_run \
|
||||
$(BIN)/test_floatarr_run \
|
||||
@@ -3118,6 +3119,16 @@ $(BIN)/test_sepcycle_dup: test/wcc/989_sepcycle_dup.c $(BIN)/ww $(BIN)/ww_ww \
|
||||
$(LIB)/libwwrt.a | $(BIN)
|
||||
$(CC) $(CFLAGS) -o $@ $<
|
||||
|
||||
# 989_separchive_run — M3-tail commit-5a gate (#46, #62): per-package `.a`
|
||||
# substrate (root=.o force-load, deps=.a) + the archive-path #31 dup-detect
|
||||
# (PASS 3). Asserts build+run, `.a` determinism (3x), cs.a==ww.a byte-id,
|
||||
# and the masked-dup reject THROUGH the `.a` path (both stages, achievable
|
||||
# parity). Needs both driver + both compiler + both linker stages + libwwrt.
|
||||
$(BIN)/test_separchive_run: test/wcc/989_separchive_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 $@ $<
|
||||
|
||||
$(BIN)/test_floatlit_run: test/wcc/989_floatlit_run.c $(BIN)/ww $(BIN)/w6c \
|
||||
$(BIN)/w6c_ww $(BIN)/w6a $(BIN)/w6l $(LIB)/libwwrt.a | $(BIN)
|
||||
$(CC) $(CFLAGS) -o $@ $<
|
||||
|
||||
@@ -227,6 +227,27 @@ load_archive(Lnk *l, const char *path, u8 *buf, u64 len)
|
||||
}
|
||||
}
|
||||
|
||||
/* Pass 3 (#31, task #62): catch a dup the selective pull MASKED.
|
||||
* A member that pass 2 left unloaded yet defines a name some other
|
||||
* object already `defined` is exactly the cross-package collision
|
||||
* archive selective-pull would silently skip (obj.c:218). Lookup
|
||||
* (never intern) so the pull predicate / selective-pull is wholly
|
||||
* untouched → byte-id-neutral on every clean link. Pulled-member
|
||||
* and direct-`.o` dups stay caught at load by the defined-check
|
||||
* below in load_image. ww has no weak symbols → a genuine dup. */
|
||||
for (ArMember *m = head; m; m = m->next) {
|
||||
if (m->loaded || m->defs == NULL) continue;
|
||||
for (int i = 0; m->defs[i]; i++) {
|
||||
Lsym *s = l_lookup(l, m->defs[i]);
|
||||
if (s != NULL && s->defined) {
|
||||
fprintf(stderr,
|
||||
"w6l: %s: duplicate symbol %s\n",
|
||||
path, m->defs[i]);
|
||||
l->errs++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Free unloaded members; loaded ones had their bytes consumed
|
||||
* by load_image (which took the copy). */
|
||||
while (head) {
|
||||
|
||||
@@ -885,11 +885,68 @@ sep_compose_unit(struct sepgraph *g, int pi, const char *scratch,
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* archive_o — write a deterministic single-member SysV ar archive at
|
||||
* `apath` wrapping the object at `objpath`. No armap / long-name table:
|
||||
* w6l reads each member's ELF .symtab directly (obj.c elf_globals) and
|
||||
* skips '/'-named members, so a package `.a` needs only the global magic,
|
||||
* one 60-byte member header, and the `.o` bytes (newline-padded to even).
|
||||
* Zeroed mtime/uid/gid + fixed mode + a fixed member name make the bytes
|
||||
* a pure function of the `.o` content → cstage `.a` == wwstage `.a`
|
||||
* (rule 10) and a stable md5 for the 5b cache key. The wwstage twin is
|
||||
* archiveo (selfhost/cmd/ww/main.ww). */
|
||||
static int
|
||||
archive_o(const char *objpath, const char *apath)
|
||||
{
|
||||
FILE *in = fopen(objpath, "rb");
|
||||
if (in == NULL) {
|
||||
fprintf(stderr, "ww --sep: cannot read %s\n", objpath);
|
||||
return -1;
|
||||
}
|
||||
fseek(in, 0, SEEK_END);
|
||||
long n = ftell(in);
|
||||
fseek(in, 0, SEEK_SET);
|
||||
if (n < 0) { fclose(in); return -1; }
|
||||
unsigned char *buf = malloc((size_t)n);
|
||||
if (buf == NULL) { fclose(in); return -1; }
|
||||
if (fread(buf, 1, (size_t)n, in) != (size_t)n) {
|
||||
free(buf); fclose(in); return -1;
|
||||
}
|
||||
fclose(in);
|
||||
|
||||
FILE *out = fopen(apath, "wb");
|
||||
if (out == NULL) {
|
||||
fprintf(stderr, "ww --sep: cannot open %s\n", apath);
|
||||
free(buf);
|
||||
return -1;
|
||||
}
|
||||
fwrite("!<arch>\n", 1, 8, out);
|
||||
/* sizelint-ok: the 60-byte ar(5) member header and its field offsets
|
||||
* are a FILE-FORMAT constant, not a type size (CLAUDE.md rule 13). */
|
||||
char hdr[60];
|
||||
memset(hdr, ' ', sizeof hdr);
|
||||
memcpy(hdr + 0, "pkg.o/", 6); /* GNU short-name '/' terminator */
|
||||
hdr[16] = '0'; /* mtime (zeroed → determinism) */
|
||||
hdr[28] = '0'; /* uid (zeroed) */
|
||||
hdr[34] = '0'; /* gid (zeroed) */
|
||||
memcpy(hdr + 40, "100644", 6); /* mode (fixed octal) */
|
||||
char sz[12];
|
||||
int szn = snprintf(sz, sizeof sz, "%lu", (unsigned long)n);
|
||||
memcpy(hdr + 48, sz, (size_t)szn);
|
||||
hdr[58] = 0x60; /* member-header magic byte */
|
||||
hdr[59] = 0x0a;
|
||||
fwrite(hdr, 1, sizeof hdr, out);
|
||||
fwrite(buf, 1, (size_t)n, out);
|
||||
if (n & 1) fputc('\n', out); /* members are 2-byte aligned */
|
||||
fclose(out);
|
||||
free(buf);
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* build_one_sep — the --sep orchestration: discover_deps, reverse_topo,
|
||||
* the transitive producer loop (one `w6c -c -I` per package, dep-first),
|
||||
* then a flat `w6l` of the `.o` set (per-pkg `.a` + multi-archive link
|
||||
* is commit 4). Side files land in a cold `<stem>.sepwork` scratch dir
|
||||
* (the structured cache is commit 5). */
|
||||
* the transitive producer loop (one `w6c -c -I` per package, dep-first,
|
||||
* each DEP `.o` wrapped in its own deterministic `.a`), then a
|
||||
* 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,
|
||||
const char *objstem, const char *extra_includes, const char *extra_libs,
|
||||
@@ -992,11 +1049,26 @@ build_one_sep(const char *src, int entry_is_dir, const char *out,
|
||||
g->pkg[pi].path[0] ? g->pkg[pi].path : "(root)");
|
||||
free(order); free(g); return 1;
|
||||
}
|
||||
/* wrap each DEP package's `.o` in its own deterministic `.a`
|
||||
* (5a). The ROOT stays a positional `.o` (force-loaded — it's
|
||||
* the build target, always fully linked), so `main` is defined
|
||||
* before any archive is processed, matching build_one's root
|
||||
* treatment. The link consumes `.o`/`.a`, never `.wwi`. */
|
||||
if (pi != root) {
|
||||
char apath[1024];
|
||||
sep_fname(g, pi, scratch, ".a", apath, sizeof apath);
|
||||
if (archive_o(obj, apath) != 0) {
|
||||
fprintf(stderr, "ww --sep: archive failed for %s\n",
|
||||
g->pkg[pi].path[0] ? g->pkg[pi].path : "(root)");
|
||||
free(order); free(g); return 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* flat link: root.o first (order[norder-1]), deps after; runtime
|
||||
* archive selectively pulls only undefined runtime symbols (commit 4
|
||||
* adds per-pkg `.a` + multi-archive reverse-topo link). */
|
||||
/* reverse-topo link: root `.o` first (order[norder-1], force-loaded),
|
||||
* then transitive dep `.a` in reverse-topo order, then libwwrt.a —
|
||||
* each archive selectively pulls only members satisfying a live
|
||||
* undef. */
|
||||
char rtargs[2048] = {0};
|
||||
char rtpath[1024];
|
||||
snprintf(rtpath, sizeof rtpath, "%s/libwwrt.a", libdir);
|
||||
@@ -1010,10 +1082,12 @@ build_one_sep(const char *src, int entry_is_dir, const char *out,
|
||||
}
|
||||
char objs[8192] = {0};
|
||||
for (int oi = norder - 1; oi >= 0; oi--) {
|
||||
char obj[1024];
|
||||
sep_fname(g, order[oi], scratch, ".o", obj, sizeof obj);
|
||||
char path[1024];
|
||||
/* root: positional `.o` (force-load); deps: `.a` (selective). */
|
||||
sep_fname(g, order[oi], scratch,
|
||||
order[oi] == root ? ".o" : ".a", path, sizeof path);
|
||||
size_t n = strlen(objs);
|
||||
snprintf(objs + n, sizeof objs - n, "%s%s", n ? " " : "", obj);
|
||||
snprintf(objs + n, sizeof objs - n, "%s%s", n ? " " : "", path);
|
||||
}
|
||||
const char *libargs = (extra_libs && extra_libs[0]) ? extra_libs : "";
|
||||
const char *libdirset = (extra_libdirs && extra_libdirs[0]) ? extra_libdirs : "";
|
||||
|
||||
@@ -3352,6 +3352,33 @@ fn loadarchive(l: *lnk, path: *u8, buf: *u8, len: u64) i32 = {
|
||||
m = m.mnext;
|
||||
};
|
||||
};
|
||||
|
||||
// Pass 3 (#31, task #62): catch a dup the selective pull MASKED. A
|
||||
// member pass 2 left unloaded yet defining a name some other object
|
||||
// already `defined` is exactly the cross-package collision archive
|
||||
// selective-pull would silently skip. lookup (never intern) keeps
|
||||
// the pull machinery untouched → byte-id-neutral on every clean
|
||||
// link. Pulled-member and direct-`.o` dups stay caught in loadimage.
|
||||
// ww has no weak symbols → a genuine dup. (The bare message matches
|
||||
// loadimage's; the w6l path+name text divergence is #61, separate.)
|
||||
let dm: *armember = head;
|
||||
for (dm != nil) {
|
||||
if (dm.loaded == 0) {
|
||||
let de: *defent = dm.defs;
|
||||
for (de != nil) {
|
||||
let s: *lsym = lookup(l, de.name);
|
||||
if (s != nil) {
|
||||
if (s.defined != 0) {
|
||||
let msg: str = "w6l: duplicate symbol\n";
|
||||
os.write(2, msg.ptr, msg.len: u64);
|
||||
l.errs += 1;
|
||||
};
|
||||
};
|
||||
de = de.dnext;
|
||||
};
|
||||
};
|
||||
dm = dm.mnext;
|
||||
};
|
||||
return 0;
|
||||
};
|
||||
|
||||
|
||||
@@ -376,6 +376,33 @@ fn loadarchive(l: *lnk, path: *u8, buf: *u8, len: u64) i32 = {
|
||||
m = m.mnext;
|
||||
};
|
||||
};
|
||||
|
||||
// Pass 3 (#31, task #62): catch a dup the selective pull MASKED. A
|
||||
// member pass 2 left unloaded yet defining a name some other object
|
||||
// already `defined` is exactly the cross-package collision archive
|
||||
// selective-pull would silently skip. lookup (never intern) keeps
|
||||
// the pull machinery untouched → byte-id-neutral on every clean
|
||||
// link. Pulled-member and direct-`.o` dups stay caught in loadimage.
|
||||
// ww has no weak symbols → a genuine dup. (The bare message matches
|
||||
// loadimage's; the w6l path+name text divergence is #61, separate.)
|
||||
let dm: *armember = head;
|
||||
for (dm != nil) {
|
||||
if (dm.loaded == 0) {
|
||||
let de: *defent = dm.defs;
|
||||
for (de != nil) {
|
||||
let s: *lsym = lookup(l, de.name);
|
||||
if (s != nil) {
|
||||
if (s.defined != 0) {
|
||||
let msg: str = "w6l: duplicate symbol\n";
|
||||
os.write(2, msg.ptr, msg.len: u64);
|
||||
l.errs += 1;
|
||||
};
|
||||
};
|
||||
de = de.dnext;
|
||||
};
|
||||
};
|
||||
dm = dm.mnext;
|
||||
};
|
||||
return 0;
|
||||
};
|
||||
|
||||
|
||||
@@ -4078,10 +4078,83 @@ fn sepcomposeunit(g: *sepgraph, pi: i32, scratch: *u8, order: []i32,
|
||||
return 0;
|
||||
};
|
||||
|
||||
// archiveo — twin of cmd/ww/main.c archive_o. Writes a deterministic
|
||||
// single-member SysV ar archive at `apath` wrapping the `.o` at
|
||||
// `objpath`. No armap / long-name table: w6l reads each member's ELF
|
||||
// .symtab directly and skips '/'-named members, so a package `.a` is
|
||||
// just the global magic + one 60-byte member header + the `.o` bytes
|
||||
// (newline-padded to even). Zeroed mtime/uid/gid + fixed mode + a fixed
|
||||
// member name make the bytes a pure function of the `.o` content →
|
||||
// cstage `.a` == wwstage `.a` (rule 10) and a stable md5 for the 5b
|
||||
// cache key.
|
||||
fn archiveo(objpath: *u8, apath: *u8) i32 = {
|
||||
let objp: *u8;
|
||||
let objn: u64;
|
||||
objp, objn = slurp(objpath);
|
||||
if (objp == nil) {
|
||||
cerr("ww --sep: cannot read object for archive\n");
|
||||
return -1;
|
||||
};
|
||||
let pad: u64 = 0u64;
|
||||
if ((objn & 1u64) != 0u64) { pad = 1u64; };
|
||||
// sizelint-ok: 8B ar(5) magic + 60B member header are FILE-FORMAT
|
||||
// constants, not type sizes (CLAUDE.md rule 13 carve-out).
|
||||
let total: u64 = 8u64 + 60u64 + objn + pad;
|
||||
let outs: []u8 = alloc([], total)!;
|
||||
let out: *u8 = outs.ptr;
|
||||
|
||||
// 60-byte member header at offset 8, ASCII space-filled, fields
|
||||
// left-justified; the 8-byte global magic precedes it. strinto
|
||||
// copies a str's bytes (the working i32-index idiom) — a direct
|
||||
// `out[i] = lit[i: i32]` store trips the cgen's str-index-rvalue arm.
|
||||
let h: u64 = 8u64;
|
||||
let j: u64 = 0u64;
|
||||
for (j < 60u64) { out[h + j] = 32u8; j += 1u64; }; // 0x20 fill
|
||||
strinto(out, 0u64, "!<arch>\n"); // global magic
|
||||
strinto(out, h, "pkg.o/"); // name (GNU '/' terminator)
|
||||
out[h + 16u64] = 48u8; // mtime "0" (zeroed → determinism)
|
||||
out[h + 28u64] = 48u8; // uid "0"
|
||||
out[h + 34u64] = 48u8; // gid "0"
|
||||
strinto(out, h + 40u64, "100644"); // mode (fixed octal)
|
||||
// size: decimal byte-count of the .o, left-justified at [48..58)
|
||||
if (objn == 0u64) {
|
||||
out[h + 48u64] = 48u8;
|
||||
} else {
|
||||
let ndig: u64 = 0u64;
|
||||
let t: u64 = objn;
|
||||
for (t > 0u64) { ndig += 1u64; t = t / 10u64; };
|
||||
let d: u64 = ndig;
|
||||
t = objn;
|
||||
for (t > 0u64) {
|
||||
d -= 1u64;
|
||||
out[h + 48u64 + d] = ((t % 10u64): u8) + 48u8;
|
||||
t = t / 10u64;
|
||||
};
|
||||
};
|
||||
out[h + 58u64] = 96u8; // member-header magic 0x60
|
||||
out[h + 59u64] = 10u8; // 0x0a
|
||||
|
||||
// the .o bytes, then a '\n' pad iff the size is odd (2-byte align).
|
||||
let k: u64 = 0u64;
|
||||
for (k < objn) { out[h + 60u64 + k] = objp[k]; k += 1u64; };
|
||||
if (pad != 0u64) { out[h + 60u64 + objn] = 10u8; };
|
||||
|
||||
let fd: i32 = os.open(pathstr(apath),
|
||||
os.flag.WRONLY | os.flag.CREATE | os.flag.TRUNC, 420i32); // 0o644
|
||||
if (fd < 0) {
|
||||
cerr("ww --sep: cannot open archive\n");
|
||||
return -1;
|
||||
};
|
||||
os.writeall(fd, out, total);
|
||||
os.close(fd);
|
||||
return 0;
|
||||
};
|
||||
|
||||
// buildonesep — the --sep orchestration: discover deps, reverse-topo,
|
||||
// the transitive producer loop (one `w6c -c -I` per package, dep-first),
|
||||
// then a flat `w6l` of the `.o` set. Side files land in a cold
|
||||
// `<stem>.sepwork` scratch dir. Twin of cstage build_one_sep.
|
||||
// the transitive producer loop (one `w6c -c -I` per package, dep-first,
|
||||
// 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) i32 = {
|
||||
let c6: *u8 = joinpathlit(selfdir, "w6c_ww");
|
||||
@@ -4231,12 +4304,26 @@ fn buildonesep(selfdir: *u8, src: *u8, entryisdir: i32, out: *u8,
|
||||
return 1;
|
||||
};
|
||||
};
|
||||
// Wrap each DEP package's `.o` in its own deterministic `.a`
|
||||
// (5a). The ROOT stays a positional `.o` (force-loaded — it's
|
||||
// the build target), so `main` is defined before any archive is
|
||||
// processed (mirrors buildone's root treatment). The link
|
||||
// consumes `.o`/`.a`, never `.wwi`.
|
||||
if (pi != root) {
|
||||
let apath: *u8 = sepfname(g, pi, scratch, ".a");
|
||||
if (archiveo(objf, apath) != 0) {
|
||||
cerr("ww --sep: archive failed\n");
|
||||
return 1;
|
||||
};
|
||||
};
|
||||
oi += 1;
|
||||
};
|
||||
|
||||
// Flat link: root.o first (order[norder-1]), deps after; libwwrt.a
|
||||
// selectively pulls runtime symbols. argv: 4 fixed (w6l,-o,out) + 1
|
||||
// per .o + 1 libwwrt + 2*nlibdirs + 2*nlibs + 1 nil.
|
||||
// Reverse-topo link of the per-package `.a` set: root.a first
|
||||
// (order[norder-1]), deps after, then libwwrt.a (which still
|
||||
// selectively pulls only the runtime members a live undef needs).
|
||||
// argv: 3 fixed (w6l,-o,out) + 1 per .a + 1 libwwrt + 2*nlibdirs
|
||||
// + 2*nlibs + 1 nil.
|
||||
let nldirs: i32 = 0;
|
||||
let nllibs: i32 = 0;
|
||||
let ldirs: **u8 = nil;
|
||||
@@ -4256,7 +4343,10 @@ fn buildonesep(selfdir: *u8, src: *u8, entryisdir: i32, out: *u8,
|
||||
let pos: i32 = 3;
|
||||
let li: i32 = norder - 1;
|
||||
for (li >= 0) {
|
||||
largv[pos] = sepfname(g, order[li], scratch, ".o");
|
||||
// root: positional `.o` (force-load); deps: `.a` (selective).
|
||||
let suf: str = ".a";
|
||||
if (order[li] == root) { suf = ".o"; };
|
||||
largv[pos] = sepfname(g, order[li], scratch, suf);
|
||||
pos += 1;
|
||||
li -= 1;
|
||||
};
|
||||
|
||||
@@ -1204,10 +1204,83 @@ fn sepcomposeunit(g: *sepgraph, pi: i32, scratch: *u8, order: []i32,
|
||||
return 0;
|
||||
};
|
||||
|
||||
// archiveo — twin of cmd/ww/main.c archive_o. Writes a deterministic
|
||||
// single-member SysV ar archive at `apath` wrapping the `.o` at
|
||||
// `objpath`. No armap / long-name table: w6l reads each member's ELF
|
||||
// .symtab directly and skips '/'-named members, so a package `.a` is
|
||||
// just the global magic + one 60-byte member header + the `.o` bytes
|
||||
// (newline-padded to even). Zeroed mtime/uid/gid + fixed mode + a fixed
|
||||
// member name make the bytes a pure function of the `.o` content →
|
||||
// cstage `.a` == wwstage `.a` (rule 10) and a stable md5 for the 5b
|
||||
// cache key.
|
||||
fn archiveo(objpath: *u8, apath: *u8) i32 = {
|
||||
let objp: *u8;
|
||||
let objn: u64;
|
||||
objp, objn = slurp(objpath);
|
||||
if (objp == nil) {
|
||||
cerr("ww --sep: cannot read object for archive\n");
|
||||
return -1;
|
||||
};
|
||||
let pad: u64 = 0u64;
|
||||
if ((objn & 1u64) != 0u64) { pad = 1u64; };
|
||||
// sizelint-ok: 8B ar(5) magic + 60B member header are FILE-FORMAT
|
||||
// constants, not type sizes (CLAUDE.md rule 13 carve-out).
|
||||
let total: u64 = 8u64 + 60u64 + objn + pad;
|
||||
let outs: []u8 = alloc([], total)!;
|
||||
let out: *u8 = outs.ptr;
|
||||
|
||||
// 60-byte member header at offset 8, ASCII space-filled, fields
|
||||
// left-justified; the 8-byte global magic precedes it. strinto
|
||||
// copies a str's bytes (the working i32-index idiom) — a direct
|
||||
// `out[i] = lit[i: i32]` store trips the cgen's str-index-rvalue arm.
|
||||
let h: u64 = 8u64;
|
||||
let j: u64 = 0u64;
|
||||
for (j < 60u64) { out[h + j] = 32u8; j += 1u64; }; // 0x20 fill
|
||||
strinto(out, 0u64, "!<arch>\n"); // global magic
|
||||
strinto(out, h, "pkg.o/"); // name (GNU '/' terminator)
|
||||
out[h + 16u64] = 48u8; // mtime "0" (zeroed → determinism)
|
||||
out[h + 28u64] = 48u8; // uid "0"
|
||||
out[h + 34u64] = 48u8; // gid "0"
|
||||
strinto(out, h + 40u64, "100644"); // mode (fixed octal)
|
||||
// size: decimal byte-count of the .o, left-justified at [48..58)
|
||||
if (objn == 0u64) {
|
||||
out[h + 48u64] = 48u8;
|
||||
} else {
|
||||
let ndig: u64 = 0u64;
|
||||
let t: u64 = objn;
|
||||
for (t > 0u64) { ndig += 1u64; t = t / 10u64; };
|
||||
let d: u64 = ndig;
|
||||
t = objn;
|
||||
for (t > 0u64) {
|
||||
d -= 1u64;
|
||||
out[h + 48u64 + d] = ((t % 10u64): u8) + 48u8;
|
||||
t = t / 10u64;
|
||||
};
|
||||
};
|
||||
out[h + 58u64] = 96u8; // member-header magic 0x60
|
||||
out[h + 59u64] = 10u8; // 0x0a
|
||||
|
||||
// the .o bytes, then a '\n' pad iff the size is odd (2-byte align).
|
||||
let k: u64 = 0u64;
|
||||
for (k < objn) { out[h + 60u64 + k] = objp[k]; k += 1u64; };
|
||||
if (pad != 0u64) { out[h + 60u64 + objn] = 10u8; };
|
||||
|
||||
let fd: i32 = os.open(pathstr(apath),
|
||||
os.flag.WRONLY | os.flag.CREATE | os.flag.TRUNC, 420i32); // 0o644
|
||||
if (fd < 0) {
|
||||
cerr("ww --sep: cannot open archive\n");
|
||||
return -1;
|
||||
};
|
||||
os.writeall(fd, out, total);
|
||||
os.close(fd);
|
||||
return 0;
|
||||
};
|
||||
|
||||
// buildonesep — the --sep orchestration: discover deps, reverse-topo,
|
||||
// the transitive producer loop (one `w6c -c -I` per package, dep-first),
|
||||
// then a flat `w6l` of the `.o` set. Side files land in a cold
|
||||
// `<stem>.sepwork` scratch dir. Twin of cstage build_one_sep.
|
||||
// the transitive producer loop (one `w6c -c -I` per package, dep-first,
|
||||
// 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) i32 = {
|
||||
let c6: *u8 = joinpathlit(selfdir, "w6c_ww");
|
||||
@@ -1357,12 +1430,26 @@ fn buildonesep(selfdir: *u8, src: *u8, entryisdir: i32, out: *u8,
|
||||
return 1;
|
||||
};
|
||||
};
|
||||
// Wrap each DEP package's `.o` in its own deterministic `.a`
|
||||
// (5a). The ROOT stays a positional `.o` (force-loaded — it's
|
||||
// the build target), so `main` is defined before any archive is
|
||||
// processed (mirrors buildone's root treatment). The link
|
||||
// consumes `.o`/`.a`, never `.wwi`.
|
||||
if (pi != root) {
|
||||
let apath: *u8 = sepfname(g, pi, scratch, ".a");
|
||||
if (archiveo(objf, apath) != 0) {
|
||||
cerr("ww --sep: archive failed\n");
|
||||
return 1;
|
||||
};
|
||||
};
|
||||
oi += 1;
|
||||
};
|
||||
|
||||
// Flat link: root.o first (order[norder-1]), deps after; libwwrt.a
|
||||
// selectively pulls runtime symbols. argv: 4 fixed (w6l,-o,out) + 1
|
||||
// per .o + 1 libwwrt + 2*nlibdirs + 2*nlibs + 1 nil.
|
||||
// Reverse-topo link of the per-package `.a` set: root.a first
|
||||
// (order[norder-1]), deps after, then libwwrt.a (which still
|
||||
// selectively pulls only the runtime members a live undef needs).
|
||||
// argv: 3 fixed (w6l,-o,out) + 1 per .a + 1 libwwrt + 2*nlibdirs
|
||||
// + 2*nlibs + 1 nil.
|
||||
let nldirs: i32 = 0;
|
||||
let nllibs: i32 = 0;
|
||||
let ldirs: **u8 = nil;
|
||||
@@ -1382,7 +1469,10 @@ fn buildonesep(selfdir: *u8, src: *u8, entryisdir: i32, out: *u8,
|
||||
let pos: i32 = 3;
|
||||
let li: i32 = norder - 1;
|
||||
for (li >= 0) {
|
||||
largv[pos] = sepfname(g, order[li], scratch, ".o");
|
||||
// root: positional `.o` (force-load); deps: `.a` (selective).
|
||||
let suf: str = ".a";
|
||||
if (order[li] == root) { suf = ".o"; };
|
||||
largv[pos] = sepfname(g, order[li], scratch, suf);
|
||||
pos += 1;
|
||||
li -= 1;
|
||||
};
|
||||
|
||||
325
test/wcc/989_separchive_run.c
Normal file
325
test/wcc/989_separchive_run.c
Normal file
@@ -0,0 +1,325 @@
|
||||
/*
|
||||
* 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 — `<stem>.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
|
||||
* `<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>
|
||||
|
||||
#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;
|
||||
}
|
||||
Reference in New Issue
Block a user