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

@@ -322,9 +322,11 @@ source_has_test_decl(const char *path)
#define SEP_VARIANT_PRODUCTION 0
#define SEP_VARIANT_SAME_TEST 1
#define SEP_VARIANT_EXTERNAL 2
#define SEP_VARIANT_TEST_MAIN 3
#define SEP_ROLE_NORMAL 0
#define SEP_ROLE_EXTERNAL_PRODUCTION 1
#define SEP_ROLE_TEST_SUPPORT 2
#define SEP_ROLE_GENERATED_MAIN 3
#define SEP_TEST_SUPPORT_MODULE "__wwtest"
#define SEP_MAXPRODUCT 256
#define SEP_MAXCONTEXT (SEP_MAXPRODUCT + 1)
@@ -508,7 +510,7 @@ enumerate_dir_ww(const char *dirpath, int variant, const char *test_package,
#define SEP_MAXPKG 256
struct seppkg {
char path[256]; /* dotted import path; "" == root/primary */
char path[256]; /* canonical dotted package identity */
char entry[1024]; /* resolved package dir (or file, for a file root) */
char canon[1024]; /* canonical location; never package identity */
char artifact[64]; /* non-importable product-root artifact key */
@@ -519,7 +521,9 @@ struct seppkg {
int is_dir;
int variant; /* SEP_VARIANT_*; dependencies are production */
int role; /* normal, external-production, or test support */
int root; /* independently compiled/linkable product root */
int root; /* requested package action (possibly a test variant) */
int link_entry; /* package supplies the executable's bare main */
int generated_main; /* compiler-owned generated test-main package */
int failed; /* discovery/compile failure reaches this action */
int test_support; /* compiler-generated -T support package */
int loaded; /* directory membership/name loaded exactly once */
@@ -554,6 +558,7 @@ struct sepproduct {
int variant;
int context;
int root;
int variant_root; /* production-plus-test or external test package */
};
#define SEP_MAXLFLAGS 32
@@ -591,8 +596,17 @@ sep_find_or_add_variant(struct sepgraph *g, const char *path,
if (root && g->pkg[i].root) {
if (!same_location) continue;
if (variant != g->pkg[i].variant) continue;
if (g->pkg[i].role == role
&& strcmp(g->pkg[i].path, path) == 0
&& strcmp(g->pkg[i].test_package,
test_package ? test_package : "") == 0) {
/* One canonical directory variant is one compile action,
* even when more than one product requests it. */
free(canon);
return i;
}
fprintf(stderr,
"ww: duplicate package-test root %s\n", entry);
"ww: incompatible package-test roots %s\n", entry);
free(canon);
return -1;
}
@@ -693,6 +707,8 @@ sep_find_or_add_variant(struct sepgraph *g, const char *path,
p->variant = variant;
p->role = role;
p->root = root;
p->link_entry = 0;
p->generated_main = 0;
p->failed = 0;
p->test_support = 0;
p->loaded = 0;
@@ -1135,6 +1151,62 @@ sep_dep_cmp(const struct sepgraph *g, int a, int b)
return strcmp(g->pkg[a].artifact, g->pkg[b].artifact);
}
/* Add the compiler-owned test main as a real package action. Its semantic
* identity and artifact key are distinct from every directory variant; its
* only dependencies are the selected test variant and dispatcher support. */
static int
sep_add_generated_main(struct sepgraph *g, struct sepproduct *product,
int ordinal, int support)
{
if (g->n >= SEP_MAXPKG) {
fprintf(stderr, "ww: too many packages (limit %d)\n", SEP_MAXPKG);
return -1;
}
int variant = product->variant_root;
if (variant < 0 || variant >= g->n) return -1;
char variantcanon[sizeof g->pkg[0].canon];
char variantentry[sizeof g->pkg[0].entry];
snprintf(variantcanon, sizeof variantcanon, "%s", g->pkg[variant].canon);
snprintf(variantentry, sizeof variantentry, "%s", g->pkg[variant].entry);
struct seppkg *p = &g->pkg[g->n];
memset(p, 0, sizeof *p);
int pn = snprintf(p->path, sizeof p->path,
"__wwtestmain.%03d.main", ordinal);
int an = snprintf(p->artifact, sizeof p->artifact,
"__ww-test-%03d-main", ordinal);
int cn = snprintf(p->canon, sizeof p->canon, "%s#test-main-%03d",
variantcanon, ordinal);
if (pn < 0 || (size_t)pn >= sizeof p->path
|| an < 0 || (size_t)an >= sizeof p->artifact
|| cn < 0 || (size_t)cn >= sizeof p->canon) {
fprintf(stderr, "ww: generated test-main identity is too long\n");
return -1;
}
snprintf(p->entry, sizeof p->entry, "%s", variantentry);
snprintf(p->name, sizeof p->name, "main");
p->variant = SEP_VARIANT_TEST_MAIN;
p->role = SEP_ROLE_GENERATED_MAIN;
p->root = 1;
p->link_entry = 1;
p->generated_main = 1;
p->loaded = 1;
p->emit_context = product->context;
p->context_state[product->context] = 2;
p->deps[p->ndeps++] = variant;
if (support >= 0 && support != variant)
p->deps[p->ndeps++] = support;
for (int i = 1; i < p->ndeps; i++) {
int v = p->deps[i];
int j = i;
while (j > 0 && sep_dep_cmp(g, p->deps[j - 1], v) > 0) {
p->deps[j] = p->deps[j - 1];
j--;
}
p->deps[j] = v;
}
return g->n++;
}
/* Load one canonical package under one selected-root resolution context.
* Source membership is owned once, but a shared package's imports are checked
* under every context that reaches it. The first canonical binding set owns
@@ -1224,10 +1296,11 @@ sep_load_pkg(struct sepgraph *g, int pi, int context)
g->pkg[pi].failed = 1;
return -1;
}
/* Give a non-main root its declared identity before recursively loading
* dependencies. A back-edge can then reuse the root and reach the normal
* cycle detector instead of looking like a location alias. */
if (g->pkg[pi].root && g->pkg[pi].path[0] == '\0'
/* Give a directory root its declared package identity before recursively
* loading dependencies. Explicit raw single-file compiler fixtures retain
* their historical anonymous multi-package boundary. */
if (g->pkg[pi].root && g->pkg[pi].is_dir
&& g->pkg[pi].path[0] == '\0'
&& g->pkg[pi].name[0] != '\0') {
size_t n = strlen(g->pkg[pi].name);
memcpy(g->pkg[pi].path, g->pkg[pi].name, n + 1);
@@ -1358,7 +1431,15 @@ sep_compose_unit(struct sepgraph *g, int pi, const char *unitf)
return -1;
}
int bodyrc = 0;
if (g->pkg[pi].is_dir) {
if (g->pkg[pi].generated_main) {
if (fprintf(u, "//ww:module-reset %s\npackage main;\n",
g->pkg[pi].path) < 0)
bodyrc = -1;
for (int i = 0; i < g->pkg[pi].ndeps && bodyrc == 0; i++)
if (fprintf(u, "import %s;\n",
g->pkg[g->pkg[pi].deps[i]].path) < 0)
bodyrc = -1;
} else if (g->pkg[pi].is_dir) {
for (int i = 0; i < g->pkg[pi].nsources && bodyrc == 0; i++)
bodyrc = sep_emit_body(u, g->pkg[pi].sources[i],
g->pkg[pi].path);
@@ -1536,7 +1617,7 @@ static void
workdir_stamp_text(char *buf, size_t bufsz, int is_test, int emit_asm)
{
snprintf(buf, bufsz, "ww workdir fmt %d mode %s asm %d\n",
is_test ? 7 : 6, is_test ? "test" : "build", emit_asm);
is_test ? 8 : 7, is_test ? "test" : "build", emit_asm);
}
/* A stale global builder identity invalidates every committed unit voucher in
@@ -1574,8 +1655,8 @@ invalidate_workdir_units(const char *scratch)
* package universe, compile the dependency-first union once, then link each
* root from its own complete reachable archive closure. The dependency-first
* producer loop (one `w6c -c -I` per package,
* each DEP `.o` wrapped in its own deterministic `.a`), then a
* reverse-topo `w6l` of each root `.o` + dep `.a` set + libwwrt.a. Side
* each package `.o` wrapped in its own deterministic `.a`), then a
* reverse-topo `w6l` of each root `.a` + reachable `.a` set + libwwrt.a. Side
* files land in a cold `<stem>.sepwork` dir, or under the persistent
* `-w` workdir with content-identity package reuse. */
static int
@@ -1696,6 +1777,8 @@ build_one_sep_impl(const char *src, int entry_is_dir,
if (graphout) *graphout = g;
const char *rootpath = package_only && root_identity
? root_identity : "";
int support_for[SEP_MAXPRODUCT];
for (int i = 0; i < nproducts; i++) support_for[i] = -1;
for (int i = 0; i < nproducts; i++) {
const char *entry = products[i].dir != NULL
? products[i].dir : src;
@@ -1720,11 +1803,15 @@ build_one_sep_impl(const char *src, int entry_is_dir,
entry_is_dir, products[i].variant, products[i].test_package,
SEP_ROLE_NORMAL, products[i].artifact, 1);
if (products[i].root < 0) return 1;
products[i].variant_root = products[i].root;
if (!is_test && !package_only)
g->pkg[products[i].root].link_entry = 1;
}
const char *test_support_module = "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 compiler-generated requirement as a direct edge
* of the generated-main action. 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 (is_test) {
@@ -1770,8 +1857,10 @@ build_one_sep_impl(const char *src, int entry_is_dir,
* colocated production node, which is also its support dep. */
if (root_is_support
&& strcmp(test_support_module, "test") == 0
&& products[i].variant != SEP_VARIANT_EXTERNAL)
&& products[i].variant != SEP_VARIANT_EXTERNAL) {
support_for[i] = root;
continue;
}
int ti;
if (strcmp(test_support_module,
SEP_TEST_SUPPORT_MODULE) == 0)
@@ -1782,19 +1871,27 @@ build_one_sep_impl(const char *src, int entry_is_dir,
tdir);
if (ti < 0) return 1;
g->pkg[ti].test_support = 1;
int seen = 0;
for (int k = 0; k < g->pkg[root].ndeps; k++)
if (g->pkg[root].deps[k] == ti) {
seen = 1; break;
}
if (!seen && g->pkg[root].ndeps < SEP_MAXPKG)
g->pkg[root].deps[g->pkg[root].ndeps++] = ti;
support_for[i] = ti;
}
free(tc);
}
}
for (int i = 0; i < nproducts; i++) {
int root = products[i].root;
int root = products[i].variant_root;
/* Raw single-file test fixtures are the one retained non-directory
* exception: keep compiler-owned test-main synthesis in that action.
* Its support export is still an exact direct input. */
if (is_test && !entry_is_dir) {
int support = support_for[i];
if (support >= 0 && support != root) {
int seen = 0;
for (int k = 0; k < g->pkg[root].ndeps; k++)
if (g->pkg[root].deps[k] == support) seen = 1;
if (!seen)
g->pkg[root].deps[g->pkg[root].ndeps++] = support;
}
g->pkg[root].link_entry = 1;
}
if (sep_load_pkg(g, root, products[i].context) < 0) {
g->pkg[root].failed = 1;
continue;
@@ -1808,6 +1905,22 @@ build_one_sep_impl(const char *src, int entry_is_dir,
g->pkg[root].failed = 1;
}
}
if (is_test && entry_is_dir) {
for (int i = 0; i < nproducts; i++) {
int variant = products[i].variant_root;
int support = support_for[i];
if (support >= 0 && support != variant
&& sep_load_pkg(g, support, products[i].context) < 0)
g->pkg[variant].failed = 1;
int mainpkg = sep_add_generated_main(g, &products[i], i,
support);
if (mainpkg < 0) return 1;
if (g->pkg[variant].failed
|| (support >= 0 && g->pkg[support].failed))
g->pkg[mainpkg].failed = 1;
products[i].root = mainpkg;
}
}
if (sep_validate_artifact_paths(g, scratch) < 0)
return 1;
int root_package = package_only;
@@ -1830,8 +1943,7 @@ build_one_sep_impl(const char *src, int entry_is_dir,
for (int pi = 0; pi < g->n; pi++) g->pkg[pi].color = 0;
int ignored = 0;
if (sep_topo_visit(g, root, order, &ignored, stack, 0) < 0
|| sep_validate_module_closure(g, order, ignored,
root_package) < 0)
|| sep_validate_module_closure(g, order, ignored, 1) < 0)
g->pkg[root].failed = 1;
}
for (int pi = 0; pi < g->n; pi++) g->pkg[pi].color = 0;
@@ -1843,11 +1955,6 @@ build_one_sep_impl(const char *src, int entry_is_dir,
}
}
free(stack);
if (!root_package)
for (int i = 0; i < nproducts; i++)
if (g->pkg[products[i].root].path[0] != '\0')
g->pkg[products[i].root].path[0] = '\0';
int any_failed = 0;
for (int i = 0; i < nproducts; i++)
if (g->pkg[products[i].root].failed) any_failed = 1;
@@ -1882,8 +1989,6 @@ build_one_sep_impl(const char *src, int entry_is_dir,
const char *cs = warm ? asmnew : asmf;
const char *co = warm ? objnew : obj;
const char *ca = warm ? anew : apath;
int needs_export = !g->pkg[pi].root || root_package;
int needs_archive = !g->pkg[pi].root || root_package;
if (sep_compose_unit(g, pi, cu) < 0) {
g->pkg[pi].failed = 1;
any_failed = 1;
@@ -1896,9 +2001,9 @@ build_one_sep_impl(const char *src, int entry_is_dir,
if (warm && !stale_all && !deps_changed
&& file_equal(unitnew, unitf)
&& file_is_reg(asmf)
&& (!needs_export || file_is_reg(wwi))
&& file_is_reg(wwi)
&& (emit_asm || (file_size_nonzero(obj)
&& (!needs_archive || file_size_nonzero(apath))))) {
&& file_size_nonzero(apath)))) {
if (unlink(unitnew) != 0) {
fprintf(stderr, "ww: cannot remove %s\n",
unitnew);
@@ -1907,13 +2012,6 @@ build_one_sep_impl(const char *src, int entry_is_dir,
}
continue;
}
/* BUG-1 (#69): -I <wwi> is purely the root's UNUSED
* `.wwi` output path, but it triggers wwi_emit →
* check_exported_type 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. Skip -I for the root; its `.wwi`
* is never consumed. */
size_t cargvcap = (size_t)(12 + 3 * g->pkg[pi].ndeps);
char **cargv = calloc(cargvcap, sizeof *cargv);
char (*importfiles)[SEP_ARTIFACT_MAX] = NULL;
@@ -1930,16 +2028,21 @@ build_one_sep_impl(const char *src, int entry_is_dir,
}
int cpos = 0;
cargv[cpos++] = "w6c";
if (!needs_export && is_test && g->pkg[pi].root) {
/* #79: the root carries -T under `ww test`
* so w6c synthesizes the test main. Deps never
* get -T. */
if (g->pkg[pi].generated_main
|| (is_test && g->pkg[pi].root && !g->pkg[pi].is_dir)) {
cargv[cpos++] = "-T";
cargv[cpos++] = "--entry";
cargv[cpos++] = "--test-support-module";
cargv[cpos++] = (char *)test_support_module;
} else if (needs_export && g->pkg[pi].test_support) {
cargv[cpos++] = "--test-support-module";
cargv[cpos++] = (char *)test_support_module;
} else {
if (is_test && g->pkg[pi].root)
cargv[cpos++] = "--test-package";
if (g->pkg[pi].link_entry)
cargv[cpos++] = "--entry";
if (g->pkg[pi].test_support) {
cargv[cpos++] = "--test-support-module";
cargv[cpos++] = (char *)test_support_module;
}
}
cargv[cpos++] = "-c";
for (int k = 0; k < g->pkg[pi].ndeps; k++) {
@@ -1950,10 +2053,8 @@ build_one_sep_impl(const char *src, int entry_is_dir,
cargv[cpos++] = g->pkg[dj].path;
cargv[cpos++] = importfiles[k];
}
if (needs_export) {
cargv[cpos++] = "-I";
cargv[cpos++] = (char *)cw;
}
cargv[cpos++] = "-I";
cargv[cpos++] = (char *)cw;
cargv[cpos++] = "-o";
cargv[cpos++] = (char *)cs;
cargv[cpos++] = (char *)cu;
@@ -1968,9 +2069,8 @@ build_one_sep_impl(const char *src, int entry_is_dir,
any_failed = 1;
continue;
}
if (needs_export)
g->pkg[pi].export_changed = !warm
|| !file_equal(wwinew, wwi);
g->pkg[pi].export_changed = !warm
|| !file_equal(wwinew, wwi);
if (!emit_asm) {
char *aargv[] = {"w6a", "-o", (char *)co,
(char *)cs, NULL};
@@ -1982,12 +2082,9 @@ build_one_sep_impl(const char *src, int entry_is_dir,
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, always fully linked), so `main` is defined
* before any archive is processed. The link consumes `.o`/`.a`,
* never `.wwi`. */
if (!emit_asm && needs_archive) {
/* Every package action, including executable and generated-test roots,
* produces the existing deterministic single-member archive. */
if (!emit_asm) {
if (archive_o(co, ca) != 0) {
fprintf(stderr, "ww: archive failed for %s\n",
g->pkg[pi].path[0] ? g->pkg[pi].path : "(root)");
@@ -1999,11 +2096,10 @@ build_one_sep_impl(const char *src, int entry_is_dir,
/* Commit order: artifacts before the unit that vouches for
* them, unit strictly last. */
if (warm) {
if ((needs_export && rename(wwinew, wwi) != 0)
if (rename(wwinew, wwi) != 0
|| rename(asmnew, asmf) != 0
|| (!emit_asm && rename(objnew, obj) != 0)
|| (!emit_asm && needs_archive
&& rename(anew, apath) != 0)
|| (!emit_asm && rename(anew, apath) != 0)
|| rename(unitnew, unitf) != 0) {
fprintf(stderr, "ww: cannot commit %s\n",
g->pkg[pi].path[0] ? g->pkg[pi].path : "(root)");
@@ -2066,10 +2162,10 @@ build_one_sep_impl(const char *src, int entry_is_dir,
}
free(order);
/* Each product gets its own reverse-topological closure: root `.o` first,
* then every transitively reachable dependency `.a`, then libwwrt.a. A
* same-test root already contains its production sources, so its colocated
* production archive is omitted without dropping that node's dependencies. */
/* Each product gets its own reverse-topological archive closure: root `.a`
* first, then every transitively reachable package `.a`, then libwwrt.a. An
* internal test variant already contains production sources, so its
* colocated production archive is omitted without dropping dependencies. */
char rtpaths[2][1024];
int nrt = 1;
snprintf(rtpaths[0], sizeof rtpaths[0], "%s/libwwrt.a", libdir);
@@ -2084,6 +2180,7 @@ build_one_sep_impl(const char *src, int entry_is_dir,
int nlibs = linkflags ? linkflags->nlibs : 0;
for (int i = 0; i < nproducts; i++) {
int root = products[i].root;
int variant_root = products[i].variant_root;
if (g->pkg[root].failed) { any_failed = 1; continue; }
for (int pi = 0; pi < g->n; pi++) g->pkg[pi].color = 0;
int *linkorder = calloc((size_t)g->n, sizeof *linkorder);
@@ -2110,14 +2207,15 @@ build_one_sep_impl(const char *src, int entry_is_dir,
largv[pos++] = (char *)products[i].out;
for (int oi = nlink - 1; oi >= 0; oi--) {
int pi = linkorder[oi];
if (g->pkg[root].variant == SEP_VARIANT_SAME_TEST
&& pi != root
if (variant_root >= 0
&& g->pkg[variant_root].variant == SEP_VARIANT_SAME_TEST
&& pi != variant_root
&& g->pkg[pi].variant == SEP_VARIANT_PRODUCTION
&& g->pkg[pi].role != SEP_ROLE_TEST_SUPPORT
&& strcmp(g->pkg[pi].canon, g->pkg[root].canon) == 0)
&& strcmp(g->pkg[pi].canon,
g->pkg[variant_root].canon) == 0)
continue;
sep_fname(g, pi, scratch,
pi == root ? ".o" : ".a", linkpaths[npath],
sep_fname(g, pi, scratch, ".a", linkpaths[npath],
sizeof linkpaths[npath]);
largv[pos++] = linkpaths[npath++];
}
@@ -2174,7 +2272,10 @@ build_one_sep(const char *src, int entry_is_dir, const char *root_identity,
.artifact = {0},
.variant = root_variant,
.root = -1,
.variant_root = -1,
};
if (!package_only)
snprintf(product.artifact, sizeof product.artifact, "__root");
int r = build_one_sep_impl(src, entry_is_dir, root_identity, out, objstem,
extra_includes, linkflags, package_only, is_test,
&product, 1, emit_asm, workdir, scratch,
@@ -2612,6 +2713,7 @@ do_test(int argc, char **argv)
products[nproducts].artifact[0] = '\0';
products[nproducts].variant = variant;
products[nproducts].root = -1;
products[nproducts].variant_root = -1;
nproducts++;
} else if (strcmp(argv[i], "-S") == 0) {
emit_asm = 1;