build: make executable and test roots package actions

This commit is contained in:
2026-08-12 22:00:02 +09:00
parent fc4bde703e
commit 1724ea086f
11 changed files with 722 additions and 364 deletions

View File

@@ -447,9 +447,11 @@ fn dirfileattest(dirpath: *u8, name: *u8) i32 = {
def SEP_VARIANT_PRODUCTION: i32 = 0;
def SEP_VARIANT_SAME_TEST: i32 = 1;
def SEP_VARIANT_EXTERNAL: i32 = 2;
def SEP_VARIANT_TEST_MAIN: i32 = 3;
def SEP_ROLE_NORMAL: i32 = 0;
def SEP_ROLE_EXTERNAL_PRODUCTION: i32 = 1;
def SEP_ROLE_TEST_SUPPORT: i32 = 2;
def SEP_ROLE_GENERATED_MAIN: i32 = 3;
def SEP_TEST_SUPPORT_MODULE: str = "__wwtest";
def SEP_MAXPRODUCT: i32 = 256;
def SEP_MAXCONTEXT: i32 = 257;
@@ -757,7 +759,7 @@ type sepbind = struct {
};
type seppkg = struct {
path: *u8, // dotted import path, NUL-term; root path[0]==0
path: *u8, // canonical dotted package identity, NUL-term
entry: *u8, // resolved package dir (or file, file root), NUL-term
artifact: *u8, // non-importable product-root artifact key
name: *u8, // validated declared name; directory packages only
@@ -768,6 +770,8 @@ type seppkg = struct {
variant: i32,
role: i32,
root: bool,
linkentry: bool,
generatedmain: bool,
failed: bool,
testsupport: bool,
loaded: bool,
@@ -802,6 +806,7 @@ type sepproduct = struct {
variant: i32,
context: i32,
root: i32,
variantroot: i32,
};
fn sepfindoraddvariant(g: *sepgraph, path: *u8, entry: *u8,
@@ -819,7 +824,17 @@ fn sepfindoraddvariant(g: *sepgraph, path: *u8, entry: *u8,
if (root && g.pkg[i].root) {
if (!samelocation) { i += 1; continue; };
if (variant != g.pkg[i].variant) { i += 1; continue; };
cerr("ww: duplicate package-test root ");
let sametest: bool = testpackage == nil
&& g.pkg[i].testpackage == nil;
if (testpackage != nil && g.pkg[i].testpackage != nil) {
sametest = cstreq(testpackage, g.pkg[i].testpackage);
};
if (g.pkg[i].role == role && cstreq(g.pkg[i].path, path)
&& sametest) {
// One directory variant is one compile action across products.
return i;
};
cerr("ww: incompatible package-test roots ");
cerr(pathstr(entry)); cerr("\n");
return -1;
};
@@ -923,6 +938,8 @@ fn sepfindoraddvariant(g: *sepgraph, path: *u8, entry: *u8,
g.pkg[g.n].variant = variant;
g.pkg[g.n].role = role;
g.pkg[g.n].root = root;
g.pkg[g.n].linkentry = false;
g.pkg[g.n].generatedmain = false;
g.pkg[g.n].failed = false;
g.pkg[g.n].testsupport = false;
g.pkg[g.n].loaded = false;
@@ -1356,6 +1373,79 @@ fn sepdepcmp(g: *sepgraph, a: i32, b: i32) i32 = {
pathstr(g.pkg[b].artifact)): i32;
};
fn generatedmainpath(index: i32) *u8 = {
let buf: []u8 = alloc([], 64u64)!;
buf.len = 64;
let off: u64 = strinto(buf.ptr, 0u64, "__wwtestmain.");
buf[off] = (((index / 100) % 10) + 48): u8; off += 1u64;
buf[off] = (((index / 10) % 10) + 48): u8; off += 1u64;
buf[off] = ((index % 10) + 48): u8; off += 1u64;
off = strinto(buf.ptr, off, ".main");
cstrseal(buf.ptr, off);
return buf.ptr;
};
// Materialize the generated test dispatcher as a normal package action. It
// owns a generated source unit and imports exactly its test variant/support.
fn sepaddgeneratedmain(g: *sepgraph, product: *sepproduct, ordinal: i32,
support: i32) i32 = {
if (g.n >= SEP_MAXPKG) {
cerr("ww: too many packages\n");
return -1;
};
let variant: i32 = product.variantroot;
if (variant < 0 || variant >= g.n) { return -1; };
let p: *seppkg = &g.pkg[g.n];
p.path = generatedmainpath(ordinal);
p.entry = g.pkg[variant].entry;
p.artifact = productartifact(ordinal, SEP_VARIANT_TEST_MAIN);
p.name = arenadupcstr("main\0".ptr, 4u64);
p.testpackage = nil;
p.sources = nil;
p.nsources = 0;
p.isdir = 0;
p.variant = SEP_VARIANT_TEST_MAIN;
p.role = SEP_ROLE_GENERATED_MAIN;
p.root = true;
p.linkentry = true;
p.generatedmain = true;
p.failed = false;
p.testsupport = false;
p.loaded = true;
p.exportchanged = false;
p.emitcontext = product.context;
let cslot: []u8 = alloc([], SEP_MAXCONTEXT: u64)!;
cslot.len = SEP_MAXCONTEXT;
p.contextstate = cslot;
p.contextstate[product.context] = 2u8;
let emptybindings: []sepbind;
p.bindings = emptybindings;
let dslot: []i32 = alloc([], SEP_MAXPKG: u64)!;
dslot.len = SEP_MAXPKG;
p.deps = dslot;
p.ndeps = 1;
p.deps[0] = variant;
if (support >= 0 && support != variant) {
p.deps[p.ndeps] = support;
p.ndeps += 1;
};
let i: i32 = 1;
for (i < p.ndeps) {
let v: i32 = p.deps[i];
let j: i32 = i;
for (j > 0 && sepdepcmp(g, p.deps[j - 1], v) > 0) {
p.deps[j] = p.deps[j - 1];
j -= 1;
};
p.deps[j] = v;
i += 1;
};
p.color = 0;
let r: i32 = g.n;
g.n += 1;
return r;
};
// Load pi once: a directory node takes ownership of its sorted production
// paths, then every selected-root context verifies the same canonical import
// bindings before the package is compiled once.
@@ -1450,7 +1540,10 @@ fn seploadpkg(g: *sepgraph, pi: i32, context: i32) i32 = {
g.pkg[pi].failed = true;
return rc;
};
if (g.pkg[pi].root && g.pkg[pi].path[0u64] == 0u8
// Directory roots acquire their canonical package identity. Explicit raw
// single-file compiler fixtures retain their anonymous multi-package reset.
if (g.pkg[pi].root && g.pkg[pi].isdir != 0
&& g.pkg[pi].path[0u64] == 0u8
&& g.pkg[pi].name != nil) {
g.pkg[pi].path = arenadupcstr(g.pkg[pi].name,
cstrlen(g.pkg[pi].name));
@@ -1598,7 +1691,27 @@ fn sepcomposeunit(g: *sepgraph, pi: i32, unitf: *u8) i32 = {
return -1;
};
let bodyrc: i32 = 0;
if (g.pkg[pi].isdir != 0) {
if (g.pkg[pi].generatedmain) {
let head: str = "//ww:module-reset ";
let pkg: str = "\npackage main;\n";
if (!sepwriteall(u, head.ptr, head.len: u64)
|| !sepwriteall(u, g.pkg[pi].path, cstrlen(g.pkg[pi].path))
|| !sepwriteall(u, pkg.ptr, pkg.len: u64)) {
bodyrc = -1;
};
let i: i32 = 0;
for (i < g.pkg[pi].ndeps && bodyrc == 0) {
let pre: str = "import ";
let end: str = ";\n";
let dep: *u8 = g.pkg[g.pkg[pi].deps[i]].path;
if (!sepwriteall(u, pre.ptr, pre.len: u64)
|| !sepwriteall(u, dep, cstrlen(dep))
|| !sepwriteall(u, end.ptr, end.len: u64)) {
bodyrc = -1;
};
i += 1;
};
} else { if (g.pkg[pi].isdir != 0) {
let i: i32 = 0;
for (i < g.pkg[pi].nsources && bodyrc == 0) {
bodyrc = sepemitbody(u, g.pkg[pi].sources[i], g.pkg[pi].path);
@@ -1606,7 +1719,7 @@ fn sepcomposeunit(g: *sepgraph, pi: i32, unitf: *u8) i32 = {
};
} else {
bodyrc = sepemitbody(u, g.pkg[pi].entry, g.pkg[pi].path);
};
}; };
if (os.close(u) != 0) {
cerr("ww: cannot close package unit\n");
return -1;
@@ -1691,8 +1804,8 @@ fn archiveo(objpath: *u8, apath: *u8) i32 = {
// buildonesep — discover deps, reverse-topo,
// the dependency-first producer loop (one `w6c -c -I` per package,
// each dependency `.o` wrapped in its own deterministic per-package `.a`), then a
// reverse-topo `w6l` of the root `.o` + dependency `.a` set + libwwrt.a.
// each package `.o` wrapped in its own deterministic per-package `.a`), then a
// reverse-topo `w6l` of the root `.a` + reachable `.a` set + libwwrt.a.
// Side files land in a cold `<stem>.sepwork` scratch dir. Twin of cstage
// build_one_sep.
@@ -1811,14 +1924,14 @@ fn copyfileatomic(src: *u8, dst: *u8) i32 = {
fn workdirstamptext(istest: i32, emitasm: i32) str = {
if (istest != 0) {
if (emitasm != 0) {
return "ww workdir fmt 7 mode test asm 1\n";
return "ww workdir fmt 8 mode test asm 1\n";
};
return "ww workdir fmt 7 mode test asm 0\n";
return "ww workdir fmt 8 mode test asm 0\n";
};
if (emitasm != 0) {
return "ww workdir fmt 6 mode build asm 1\n";
return "ww workdir fmt 7 mode build asm 1\n";
};
return "ww workdir fmt 6 mode build asm 0\n";
return "ww workdir fmt 7 mode build asm 0\n";
};
fn stampmatches(path: *u8, want: str) bool = {
@@ -2097,7 +2210,11 @@ fn buildonesepimpl(selfdir: *u8, src: *u8, entryisdir: i32,
if (graphout != nil) { *graphout = g; };
let rootpath: *u8 = "\0".ptr;
if (packageonly != 0 && rootidentity != nil) { rootpath = rootidentity; };
let supportfor: []i32 = alloc([], nproducts: u64)!;
supportfor.len = nproducts;
let producti: i32 = 0;
for (producti < nproducts) { supportfor[producti] = -1; producti += 1; };
producti = 0;
for (producti < nproducts) {
let entry: *u8 = src;
if (products[producti].dir != nil) {
@@ -2113,12 +2230,16 @@ fn buildonesepimpl(selfdir: *u8, src: *u8, entryisdir: i32,
products[producti].testpackage,
SEP_ROLE_NORMAL, products[producti].artifact, true);
if (products[producti].root < 0) { return 1; };
products[producti].variantroot = products[producti].root;
if (istest == 0 && packageonly == 0) {
g.pkg[products[producti].root].linkentry = true;
};
producti += 1;
};
let testsupportmodule: str = "test";
// -T generates a dispatcher whose support qualifier is selected by the
// command. Represent that compiler-generated requirement as a direct root
// edge. It normally coalesces with an explicit toolchain `import test`;
// command. Represent that requirement as a direct generated-main edge. It
// normally coalesces with an explicit toolchain `import test`;
// when user source occupies that identity, the reserved graph alias keeps
// it distinct. The linker receives the same support archive closure.
if (istest != 0) {
@@ -2168,6 +2289,7 @@ fn buildonesepimpl(selfdir: *u8, src: *u8, entryisdir: i32,
if (rootissupport
&& syntax.streq(testsupportmodule, "test")
&& products[producti].variant != SEP_VARIANT_EXTERNAL) {
supportfor[producti] = root;
producti += 1;
continue;
};
@@ -2181,25 +2303,32 @@ fn buildonesepimpl(selfdir: *u8, src: *u8, entryisdir: i32,
};
if (ti < 0) { return 1; };
g.pkg[ti].testsupport = true;
let seen: bool = false;
let m: i32 = 0;
for (m < g.pkg[root].ndeps) {
if (g.pkg[root].deps[m] == ti) { seen = true; };
m += 1;
};
if (!seen) {
if (g.pkg[root].ndeps < SEP_MAXPKG) {
g.pkg[root].deps[g.pkg[root].ndeps] = ti;
g.pkg[root].ndeps += 1;
};
};
supportfor[producti] = ti;
producti += 1;
};
};
};
producti = 0;
for (producti < nproducts) {
let root: i32 = products[producti].root;
let root: i32 = products[producti].variantroot;
// Raw single-file tests retain the explicit fixture exception: test-main
// synthesis stays in that action and support remains a direct export.
if (istest != 0 && entryisdir == 0) {
let support: i32 = supportfor[producti];
if (support >= 0 && support != root) {
let seen: bool = false;
let sk: i32 = 0;
for (sk < g.pkg[root].ndeps) {
if (g.pkg[root].deps[sk] == support) { seen = true; };
sk += 1;
};
if (!seen) {
g.pkg[root].deps[g.pkg[root].ndeps] = support;
g.pkg[root].ndeps += 1;
};
};
g.pkg[root].linkentry = true;
};
if (seploadpkg(g, root, products[producti].context) < 0) {
g.pkg[root].failed = true;
producti += 1;
@@ -2214,6 +2343,27 @@ fn buildonesepimpl(selfdir: *u8, src: *u8, entryisdir: i32,
};
producti += 1;
};
if (istest != 0 && entryisdir != 0) {
producti = 0;
for (producti < nproducts) {
let variant: i32 = products[producti].variantroot;
let support: i32 = supportfor[producti];
if (support >= 0 && support != variant) {
if (seploadpkg(g, support, products[producti].context) < 0) {
g.pkg[variant].failed = true;
};
};
let mainpkg: i32 = sepaddgeneratedmain(g, &products[producti],
producti, support);
if (mainpkg < 0) { return 1; };
if (g.pkg[variant].failed
|| (support >= 0 && g.pkg[support].failed)) {
g.pkg[mainpkg].failed = true;
};
products[producti].root = mainpkg;
producti += 1;
};
};
if (sepvalidateartifactpaths(g, scratch) < 0) { return 1; };
let rootpackage: bool = packageonly != 0;
if (rootpackage && !g.pkg[products[0].root].failed
@@ -2239,8 +2389,7 @@ fn buildonesepimpl(selfdir: *u8, src: *u8, entryisdir: i32,
let ignored: i32 = 0;
if (septopovisit(g, root, order,
&ignored, stack, 0) < 0
|| sepvalidatemoduleclosure(g, order, ignored,
rootpackage) < 0) {
|| sepvalidatemoduleclosure(g, order, ignored, true) < 0) {
g.pkg[root].failed = true;
};
};
@@ -2257,14 +2406,6 @@ fn buildonesepimpl(selfdir: *u8, src: *u8, entryisdir: i32,
};
producti += 1;
};
if (!rootpackage) {
producti = 0;
for (producti < nproducts) {
g.pkg[products[producti].root].path = "\0".ptr;
producti += 1;
};
};
let anyfailed: bool = false;
producti = 0;
for (producti < nproducts) {
@@ -2307,8 +2448,6 @@ fn buildonesepimpl(selfdir: *u8, src: *u8, entryisdir: i32,
cu = unitnew; cw = wwinew; cs = asmnew;
co = objnew; ca = anew;
};
let needsexport: bool = !g.pkg[pi].root || rootpackage;
let needsarchive: bool = !g.pkg[pi].root || rootpackage;
if (sepcomposeunit(g, pi, cu) < 0) {
g.pkg[pi].failed = true;
anyfailed = true;
@@ -2328,20 +2467,14 @@ fn buildonesepimpl(selfdir: *u8, src: *u8, entryisdir: i32,
if (!staleall && !depschanged) {
fresh = fileequal(unitnew, unitf);
if (fresh) { fresh = fileisreg(asmf); };
if (fresh) {
if (needsexport) {
fresh = fileisreg(wwi);
};
};
if (fresh) { fresh = fileisreg(wwi); };
if (fresh) {
if (emitasm == 0) {
fresh = filesizenonzero(objf);
};
};
if (fresh) {
if (emitasm == 0 && needsarchive) {
fresh = filesizenonzero(apath);
};
if (fresh && emitasm == 0) {
fresh = filesizenonzero(apath);
};
};
};
@@ -2355,31 +2488,34 @@ fn buildonesepimpl(selfdir: *u8, src: *u8, entryisdir: i32,
continue;
};
{
// BUG-1 (#69): -I <wwi> is purely the root's UNUSED
// `.wwi` output path, but it triggers wwiemit ->
// checkexportedtype on the root. A terminal binary's
// root legitimately has `export fn` over an unexported
// LOCAL type (the root is never imported), which the
// export-check rejects. Build a shorter root argv
// without the -I/wwi pair; root's `.wwi` is unconsumed.
// #79: the root carries -T under `ww test` so w6c
// synthesizes the test main; deps never get -T.
let roott: bool = g.pkg[pi].root && (istest != 0);
let supportt: bool = g.pkg[pi].testsupport;
let rawtest: bool = (istest != 0) && g.pkg[pi].root
&& g.pkg[pi].isdir == 0;
let gent: bool = g.pkg[pi].generatedmain || rawtest;
let testpkg: bool = g.pkg[pi].root && (istest != 0) && !gent;
let entry: bool = g.pkg[pi].linkentry;
let supportpkg: bool = g.pkg[pi].testsupport;
let alen: u64 = 8u64;
if (!needsexport) { alen = 6u64; if (roott) { alen = 9u64; }; };
if (supportt) { alen += 2u64; };
if (gent) { alen += 4u64; }
else {
if (testpkg) { alen += 1u64; };
if (entry) { alen += 1u64; };
if (supportpkg) { alen += 2u64; };
};
alen += (g.pkg[pi].ndeps: u64) * 3u64;
let argv: []str = alloc([], alen)!;
append(argv, "w6c");
if (roott) {
if (gent) {
append(argv, "-T");
append(argv, "--entry");
append(argv, "--test-support-module");
append(argv, testsupportmodule);
};
if (supportt) {
append(argv, "--test-support-module");
append(argv, testsupportmodule);
} else {
if (testpkg) { append(argv, "--test-package"); };
if (entry) { append(argv, "--entry"); };
if (supportpkg) {
append(argv, "--test-support-module");
append(argv, testsupportmodule);
};
};
append(argv, "-c");
let importk: i32 = 0;
@@ -2390,10 +2526,8 @@ fn buildonesepimpl(selfdir: *u8, src: *u8, entryisdir: i32,
append(argv, pathstr(sepfname(g, dj, scratch, ".wwi")));
importk += 1;
};
if (needsexport) {
append(argv, "-I");
append(argv, pathstr(cw));
};
append(argv, "-I");
append(argv, pathstr(cw));
append(argv, "-o");
append(argv, pathstr(cs));
append(argv, pathstr(cu));
@@ -2418,10 +2552,8 @@ fn buildonesepimpl(selfdir: *u8, src: *u8, entryisdir: i32,
continue;
};
};
if (needsexport) {
if (!warm || !fileequal(wwinew, wwi)) {
g.pkg[pi].exportchanged = true;
};
if (!warm || !fileequal(wwinew, wwi)) {
g.pkg[pi].exportchanged = true;
};
if (emitasm == 0) {
let argv: []str = alloc([], 4u64)!;
@@ -2450,12 +2582,9 @@ fn buildonesepimpl(selfdir: *u8, src: *u8, entryisdir: i32,
continue;
};
};
// 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. The link
// consumes `.o`/`.a`, never `.wwi`.
if (emitasm == 0 && needsarchive) {
// Every package action, including executable/generated roots, produces
// the existing deterministic single-member archive.
if (emitasm == 0) {
if (archiveo(co, ca) != 0) {
cerr("ww: archive failed\n");
g.pkg[pi].failed = true;
@@ -2468,10 +2597,8 @@ fn buildonesepimpl(selfdir: *u8, src: *u8, entryisdir: i32,
// them, unit strictly last.
if (warm) {
let bad: bool = false;
if (needsexport) {
if (os.rename(pathstr(wwinew), pathstr(wwi)) != 0) {
bad = true;
};
if (os.rename(pathstr(wwinew), pathstr(wwi)) != 0) {
bad = true;
};
if (!bad) {
if (os.rename(pathstr(asmnew), pathstr(asmf)) != 0) {
@@ -2486,7 +2613,7 @@ fn buildonesepimpl(selfdir: *u8, src: *u8, entryisdir: i32,
};
};
if (!bad) {
if (emitasm == 0 && needsarchive) {
if (emitasm == 0) {
if (os.rename(pathstr(anew), pathstr(apath)) != 0) {
bad = true;
};
@@ -2573,6 +2700,7 @@ fn buildonesepimpl(selfdir: *u8, src: *u8, entryisdir: i32,
producti = 0;
for (producti < nproducts) {
let root: i32 = products[producti].root;
let variantroot: i32 = products[producti].variantroot;
if (g.pkg[root].failed) {
anyfailed = true;
producti += 1;
@@ -2598,18 +2726,17 @@ fn buildonesepimpl(selfdir: *u8, src: *u8, entryisdir: i32,
let li: i32 = nlink - 1;
for (li >= 0) {
let pi: i32 = linkorder[li];
if (g.pkg[root].variant == SEP_VARIANT_SAME_TEST
&& pi != root
if (variantroot >= 0
&& g.pkg[variantroot].variant == SEP_VARIANT_SAME_TEST
&& pi != variantroot
&& g.pkg[pi].variant == SEP_VARIANT_PRODUCTION
&& g.pkg[pi].role != SEP_ROLE_TEST_SUPPORT
&& os.samefile(pathstr(g.pkg[pi].entry),
pathstr(g.pkg[root].entry))) {
pathstr(g.pkg[variantroot].entry))) {
li -= 1;
continue;
};
let suf: str = ".a";
if (pi == root) { suf = ".o"; };
largv[pos] = sepfname(g, pi, scratch, suf);
largv[pos] = sepfname(g, pi, scratch, ".a");
pos += 1;
li -= 1;
};
@@ -2694,8 +2821,10 @@ fn buildonesep(selfdir: *u8, src: *u8, entryisdir: i32,
product.testpackage = testpackage;
product.status = nil;
product.artifact = nil;
if (packageonly == 0) { product.artifact = "__root\0".ptr; };
product.variant = rootvariant;
product.root = -1;
product.variantroot = -1;
let r: i32 = buildonesepimpl(selfdir, src, entryisdir, rootidentity,
out, objstem,
incs, lf, packageonly, istest, &product, 1,
@@ -2755,9 +2884,11 @@ fn productartifact(index: i32, variant: i32) *u8 = {
off = byteinto(buf.ptr, off, '-': u8);
if (variant == SEP_VARIANT_SAME_TEST) {
off = strinto(buf.ptr, off, "same");
} else { if (variant == SEP_VARIANT_TEST_MAIN) {
off = strinto(buf.ptr, off, "main");
} else {
off = strinto(buf.ptr, off, "external");
};
}; };
cstrseal(buf.ptr, off);
return buf.ptr;
};
@@ -3446,6 +3577,7 @@ fn dotest(selfdir: *u8, argv: **u8, argc: i32, start: i32) i32 = {
product.artifact = nil;
product.variant = variant;
product.root = -1;
product.variantroot = -1;
append(products, product);
i += 6;
continue;