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:
2026-06-16 14:54:19 +09:00
parent 9cceb4ca8d
commit 62c7771594
8 changed files with 689 additions and 24 deletions

View File

@@ -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) {

View File

@@ -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 : "";