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