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:
@@ -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;
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user