From 29e97ff8a13e1cc71d80175c394c7e3803706559 Mon Sep 17 00:00:00 2001 From: Hojun-Cho Date: Wed, 12 Aug 2026 13:54:15 +0900 Subject: [PATCH] cmd: share package builds across test directories --- cmd/ww/main.c | 815 ++++++++++++++++++++++++++-------- internal/wwpackage/package.ww | 38 +- selfhost/cmd/ww/main.ww | 612 ++++++++++++++++++++----- 3 files changed, 1143 insertions(+), 322 deletions(-) diff --git a/cmd/ww/main.c b/cmd/ww/main.c index df91410a..5ad1aa2c 100644 --- a/cmd/ww/main.c +++ b/cmd/ww/main.c @@ -52,6 +52,25 @@ run(const char *cmd) return 1; } +static int +run_argv(const char *prog, char *const argv[]) +{ + pid_t pid = fork(); + if (pid < 0) { perror("ww: fork"); return -1; } + if (pid == 0) { + execv(prog, argv); + static const char msg[] = "ww: execve failed\n"; + (void)write(2, msg, sizeof msg - 1); + _exit(127); + } + int status = 0; + pid_t got; + do { got = waitpid(pid, &status, 0); } while (got < 0 && errno == EINTR); + if (got < 0) { perror("ww: waitpid"); return -1; } + if (WIFEXITED(status)) return WEXITSTATUS(status); + return 1; +} + /* run_test_bin — exec the built test binary with an optional name-filter * pattern as argv[1] (lib/test run() reads it via os.args). fork+execv * (not system()) so glob metacharacters in the pattern reach the binary @@ -299,8 +318,12 @@ 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_ROLE_NORMAL 0 +#define SEP_ROLE_EXTERNAL_PRODUCTION 1 +#define SEP_ROLE_TEST_SUPPORT 2 #define SEP_TEST_SUPPORT_MODULE "__wwtest" -#define SEP_MAXPRODUCT 2 +#define SEP_MAXPRODUCT 256 +#define SEP_MAXCONTEXT (SEP_MAXPRODUCT + 1) /* Test-file package classification uses the compiler's imports-only parser. * The coordinator chooses variants, but the command owns which real source @@ -490,37 +513,63 @@ struct seppkg { char path[256]; /* dotted import path; "" == root/primary */ 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 */ char name[256]; /* validated declared name; directory packages only */ char test_package[256]; /* selected test package; root variants only */ char **sources; /* owned, byte-sorted selected paths; dirs only */ int nsources; 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 failed; /* discovery/compile failure reaches this action */ int test_support; /* compiler-generated -T support package */ + int loaded; /* directory membership/name loaded exactly once */ + int emit_context; /* resolution context used to compose file imports */ + unsigned char context_state[SEP_MAXCONTEXT]; /* 0 new, 1 active, 2 checked */ + struct ImportSet bindings; /* first context's canonical import bindings */ int deps[SEP_MAXPKG]; /* direct-dep indices into sepgraph.pkg */ int ndeps; int color; /* tri-color DFS: 0 white, 1 gray, 2 black */ }; +struct sepcontext { + char root[1024]; /* selected entry directory; diagnostic identity */ + char searchpath[8192]; /* root : explicit -I roots : toolchain source */ +}; + struct sepgraph { struct seppkg pkg[SEP_MAXPKG]; int n; + struct sepcontext context[SEP_MAXCONTEXT]; + int ncontext; + int support_context; }; struct sepproduct { + const char *dir; const char *out; const char *test_package; const char *status; + char artifact[64]; int variant; + int context; int root; }; +#define SEP_MAXLFLAGS 32 +#define SEP_ARTIFACT_MAX 1024 +struct seplinkflags { + const char *libdirs[SEP_MAXLFLAGS]; + int nlibdirs; + const char *libs[SEP_MAXLFLAGS]; + int nlibs; +}; + static int sep_find_or_add_variant(struct sepgraph *g, const char *path, const char *entry, int is_dir, int variant, const char *test_package, - int root) + int role, const char *artifact, int root) { if (strlen(path) >= sizeof g->pkg[0].path) { fprintf(stderr, "ww: package path is too long (limit %zu bytes)\n", @@ -539,24 +588,73 @@ sep_find_or_add_variant(struct sepgraph *g, const char *path, } for (int i = 0; i < g->n; i++) { int same_location = strcmp(g->pkg[i].canon, canon) == 0; - int test_root_pair = same_location && is_dir && g->pkg[i].is_dir - && ((root && g->pkg[i].root - && variant != g->pkg[i].variant) - || (root && !g->pkg[i].root - && variant != SEP_VARIANT_PRODUCTION - && g->pkg[i].variant == SEP_VARIANT_PRODUCTION) - || (!root && g->pkg[i].root - && variant == SEP_VARIANT_PRODUCTION - && g->pkg[i].variant != SEP_VARIANT_PRODUCTION)); - if (strcmp(g->pkg[i].path, path) == 0) { - if (test_root_pair) - continue; - if (strcmp(g->pkg[i].canon, canon) != 0 - || g->pkg[i].variant != variant) { + if (root || g->pkg[i].root) { + if (root && g->pkg[i].root) { + if (!same_location) continue; + if (variant != g->pkg[i].variant) continue; fprintf(stderr, - "ww: package %s resolves to both %s and %s\n", - path[0] ? path : "(root)", g->pkg[i].entry, - entry); + "ww: duplicate package-test root %s\n", entry); + free(canon); + return -1; + } + /* A cycle back to an ordinary production root reuses that root + * after loading has assigned its declared package identity. Test + * roots remain non-importable and distinct from production. */ + if (same_location + && variant == SEP_VARIANT_PRODUCTION + && g->pkg[i].variant == SEP_VARIANT_PRODUCTION + && strcmp(g->pkg[i].path, path) == 0) { + free(canon); + return i; + } + if (!same_location) continue; + int root_variant = root ? variant : g->pkg[i].variant; + int production_variant = root ? g->pkg[i].variant : variant; + if (is_dir && g->pkg[i].is_dir + && root_variant != SEP_VARIANT_PRODUCTION + && production_variant == SEP_VARIANT_PRODUCTION) + continue; + fprintf(stderr, + "ww: package directory %s has incompatible root and production variants\n", + entry); + free(canon); + return -1; + } + int same_path = strcmp(g->pkg[i].path, path) == 0; + int same_action = same_path && same_location + && g->pkg[i].variant == variant && g->pkg[i].role == role + && (role != SEP_ROLE_EXTERNAL_PRODUCTION + || strcmp(g->pkg[i].artifact, + artifact ? artifact : "") == 0); + if (same_action) { + free(canon); + return i; + } + /* External tests still consume the directory's one canonical + * production action. The role only disambiguates genuinely distinct + * physical packages that share a source qualifier in this request. */ + if (same_path && same_location + && g->pkg[i].variant == SEP_VARIANT_PRODUCTION + && variant == SEP_VARIANT_PRODUCTION + && ((role == SEP_ROLE_EXTERNAL_PRODUCTION + && g->pkg[i].role == SEP_ROLE_NORMAL) + || (role == SEP_ROLE_NORMAL + && g->pkg[i].role == SEP_ROLE_EXTERNAL_PRODUCTION))) { + free(canon); + return i; + } + /* Differing physical packages with one source qualifier may need an + * owning-product external action in the command-global universe. A + * per-product closure check below still forbids linking both. */ + if (same_path && (role == SEP_ROLE_EXTERNAL_PRODUCTION + || g->pkg[i].role == SEP_ROLE_EXTERNAL_PRODUCTION)) + continue; + if (same_path) { + if (!same_location || g->pkg[i].variant != variant + || g->pkg[i].role != role) { + fprintf(stderr, + "ww: package %s resolves to more than one location\n", + path[0] ? path : "(root)"); free(canon); return -1; } @@ -564,11 +662,11 @@ sep_find_or_add_variant(struct sepgraph *g, const char *path, return i; } if (same_location) { - /* A test root and a production variant reached by its imports or - * runtime closure, and the selected test roots themselves, - * intentionally may share one directory. No other physical-directory - * alias is permitted. */ - if (test_root_pair) + /* A reserved compiler-generated test-support action is a + * deliberately distinct qualifier for the same toolchain sources. + * No ordinary source import can create this role. */ + if (role == SEP_ROLE_TEST_SUPPORT + || g->pkg[i].role == SEP_ROLE_TEST_SUPPORT) continue; fprintf(stderr, "ww: package directory %s has identities %s and %s\n", @@ -588,12 +686,22 @@ sep_find_or_add_variant(struct sepgraph *g, const char *path, snprintf(p->path, sizeof p->path, "%s", path); snprintf(p->entry, sizeof p->entry, "%s", entry); snprintf(p->canon, sizeof p->canon, "%s", canon); + p->artifact[0] = '\0'; + if (artifact != NULL) + snprintf(p->artifact, sizeof p->artifact, "%s", artifact); free(canon); p->is_dir = is_dir; p->variant = variant; + p->role = role; p->root = root; p->failed = 0; p->test_support = 0; + p->loaded = 0; + p->emit_context = -1; + memset(p->context_state, 0, sizeof p->context_state); + p->bindings.paths = NULL; + p->bindings.n = 0; + p->bindings.cap = 0; p->name[0] = '\0'; p->test_package[0] = '\0'; if (test_package != NULL) @@ -611,7 +719,15 @@ sep_find_or_add(struct sepgraph *g, const char *path, const char *entry, int is_dir) { return sep_find_or_add_variant(g, path, entry, is_dir, - SEP_VARIANT_PRODUCTION, NULL, 0); + SEP_VARIANT_PRODUCTION, NULL, SEP_ROLE_NORMAL, NULL, 0); +} + +static int +sep_find_or_add_role(struct sepgraph *g, const char *path, const char *entry, + int is_dir, int role, const char *artifact) +{ + return sep_find_or_add_variant(g, path, entry, is_dir, + SEP_VARIANT_PRODUCTION, NULL, role, artifact, 0); } /* Release the one package-owned directory-membership list. Every graph exit @@ -624,10 +740,49 @@ sep_graph_free(struct sepgraph *g) for (int j = 0; j < g->pkg[i].nsources; j++) free(g->pkg[i].sources[j]); free(g->pkg[i].sources); + for (int j = 0; j < g->pkg[i].bindings.n; j++) + free(g->pkg[i].bindings.paths[j]); + free(g->pkg[i].bindings.paths); } free(g); } +/* One selected directory owns one import-resolution context. Same/external + * variants of that directory share it; different directories never acquire + * precedence merely from their request order. */ +static int +sep_context_for(struct sepgraph *g, const char *root, + const char *extra_includes, const char *toolsrcdir) +{ + char searchpath[8192]; + int n; + if (extra_includes != NULL && extra_includes[0] != '\0') + n = snprintf(searchpath, sizeof searchpath, "%s:%s:%s", + root, extra_includes, toolsrcdir); + else + n = snprintf(searchpath, sizeof searchpath, "%s:%s", + root, toolsrcdir); + if (n < 0 || (size_t)n >= sizeof searchpath) { + fprintf(stderr, "ww: package import search path is too long\n"); + return -1; + } + for (int i = 0; i < g->ncontext; i++) + if (strcmp(g->context[i].searchpath, searchpath) == 0) + return i; + if (g->ncontext >= SEP_MAXCONTEXT) { + fprintf(stderr, "ww: too many package import contexts\n"); + return -1; + } + struct sepcontext *c = &g->context[g->ncontext]; + if (snprintf(c->root, sizeof c->root, "%s", root) + >= (int)sizeof c->root) { + fprintf(stderr, "ww: package root path is too long\n"); + return -1; + } + snprintf(c->searchpath, sizeof c->searchpath, "%s", searchpath); + return g->ncontext++; +} + /* Dots stay (legal in filenames). Product roots have distinct artifact names * even though each compiler unit resets to the bare executable namespace. */ static void @@ -635,19 +790,36 @@ sep_fname(const struct sepgraph *g, int pi, const char *scratch, const char *suffix, char *out, size_t outsz) { const char *base = g->pkg[pi].path; - if (g->pkg[pi].root) { - if (g->pkg[pi].variant == SEP_VARIANT_SAME_TEST) - base = "__ww-test-same"; - else if (g->pkg[pi].variant == SEP_VARIANT_EXTERNAL) - base = "__ww-test-external"; - else if (base[0] == '\0') - base = "__root"; + if (g->pkg[pi].artifact[0] != '\0') { + base = g->pkg[pi].artifact; + } else if (g->pkg[pi].root) { + if (base[0] == '\0') base = "__root"; } else if (base[0] == '\0') { base = "__root"; } snprintf(out, outsz, "%s/%s%s", scratch, base, suffix); } +/* All later artifact construction uses fixed SEP_ARTIFACT_MAX buffers. Check + * the longest suffix once, before any file is opened, so truncation can never + * collapse two command-owned action identities onto one path. */ +static int +sep_validate_artifact_paths(const struct sepgraph *g, const char *scratch) +{ + for (int i = 0; i < g->n; i++) { + const char *base = g->pkg[i].artifact[0] != '\0' + ? g->pkg[i].artifact + : (g->pkg[i].path[0] != '\0' ? g->pkg[i].path : "__root"); + size_t need = strlen(scratch) + 1 + strlen(base) + + strlen(".unit.new") + 1; + if (need > SEP_ARTIFACT_MAX) { + fprintf(stderr, "ww: package artifact path is too long\n"); + return -1; + } + } + return 0; +} + static int sep_slurp(const char *path, char **out, u64 *len) { @@ -713,6 +885,38 @@ sep_external_production_import(const struct seppkg *pkg, const char *path) return sep_external_production_name(pkg, path, 0); } +/* Canonical bindings make a shared package independent of which selected + * root reaches it first. Directory, folded-file, and inline bindings are all + * part of the package action's source meaning. */ +static int +sep_binding_add(struct ImportSet *bindings, char kind, const char *name, + const char *target) +{ + size_t nn = strlen(name), tn = target ? strlen(target) : 0; + char *binding = malloc(nn + tn + 4); + if (binding == NULL) return -1; + binding[0] = kind; + binding[1] = ':'; + memcpy(binding + 2, name, nn); + binding[2 + nn] = ':'; + if (target != NULL) memcpy(binding + 3 + nn, target, tn); + binding[3 + nn + tn] = '\0'; + if (import_seen(bindings, binding)) { + free(binding); + return 0; + } + if (bindings->n == bindings->cap) { + int cap = bindings->cap ? bindings->cap * 2 : 8; + char **paths = realloc(bindings->paths, + (size_t)cap * sizeof *paths); + if (paths == NULL) { free(binding); return -1; } + bindings->paths = paths; + bindings->cap = cap; + } + bindings->paths[bindings->n++] = binding; + return 0; +} + /* A DIRECTORY import is a package boundary: add it as a direct dep of pkg * `pi`. A FILE import is an intra-package split — fold its imports into * `pi` (its bytes join pi's body at emit time). Collects package PATHS @@ -720,7 +924,8 @@ sep_external_production_import(const struct seppkg *pkg, const char *path) * (§1.1). */ static int sep_scan_file(struct sepgraph *g, int pi, const char *file, - const char *searchpath, struct ImportSet *filevisit, int owned_source) + const char *searchpath, struct ImportSet *filevisit, + struct ImportSet *bindings, int owned_source) { if (import_seen(filevisit, file)) return 0; import_add(filevisit, file); @@ -840,8 +1045,11 @@ sep_scan_file(struct sepgraph *g, int pi, const char *file, inline_package = 1; break; } - if (inline_package) + if (inline_package) { + if (sep_binding_add(bindings, 'I', name, NULL) < 0) + rc = -1; continue; + } errorf(u->pos, "cannot find package %s", name); rc = -1; break; @@ -853,7 +1061,21 @@ sep_scan_file(struct sepgraph *g, int pi, const char *file, rc = -1; break; } + if (sep_binding_add(bindings, 'D', name, canon) < 0) { + free(canon); + rc = -1; + break; + } int self = strcmp(canon, g->pkg[pi].canon) == 0; + int runtime_production = 0; + if (external_production) + for (int gi = 0; gi < g->n; gi++) + if (g->pkg[gi].test_support + && strcmp(g->pkg[gi].path, name) == 0 + && strcmp(g->pkg[gi].canon, canon) == 0) { + runtime_production = 1; + break; + } free(canon); if (self && sep_external_production_name(&g->pkg[pi], name, 1)) external_production = 1; @@ -865,7 +1087,20 @@ sep_scan_file(struct sepgraph *g, int pi, const char *file, rc = -1; break; } - int di = sep_find_or_add(g, name, ipath, 1); + int di; + if (external_production && !runtime_production) { + char artifact[64]; + int an = snprintf(artifact, sizeof artifact, "%s-production", + g->pkg[pi].artifact); + if (an < 0 || (size_t)an >= sizeof artifact) { + rc = -1; + break; + } + di = sep_find_or_add_role(g, name, ipath, 1, + SEP_ROLE_EXTERNAL_PRODUCTION, artifact); + } else { + di = sep_find_or_add(g, name, ipath, 1); + } if (di < 0) { rc = -1; break; } int seen = 0; for (int k = 0; k < g->pkg[pi].ndeps; k++) @@ -874,8 +1109,20 @@ sep_scan_file(struct sepgraph *g, int pi, const char *file, if (g->pkg[pi].ndeps >= SEP_MAXPKG) { rc = -1; break; } g->pkg[pi].deps[g->pkg[pi].ndeps++] = di; } - } else if (sep_scan_file(g, pi, ipath, searchpath, filevisit, 0) < 0) { - rc = -1; break; + } else { + char *canon = realpath(ipath, NULL); + if (canon == NULL + || sep_binding_add(bindings, 'F', name, canon) < 0) { + free(canon); + rc = -1; + break; + } + free(canon); + if (sep_scan_file(g, pi, ipath, searchpath, filevisit, + bindings, 0) < 0) { + rc = -1; + break; + } } } free(uses); @@ -884,38 +1131,70 @@ sep_scan_file(struct sepgraph *g, int pi, const char *file, return rc; } -/* Load package pi once: a directory node takes ownership of its sorted - * production paths, then the same stored list supplies package-name - * validation and dependency scanning. Recurse over the resulting edges. - * `color` doubles as a loaded marker here (2 == loaded); it is reset to - * white before the topo pass. */ -static int -sep_load_pkg(struct sepgraph *g, int pi, const char *searchpath) +static void +sep_import_set_free(struct ImportSet *s) { - if (g->pkg[pi].color == 2) return g->pkg[pi].failed ? -1 : 0; - g->pkg[pi].color = 2; - struct ImportSet fv = {0}; + for (int i = 0; i < s->n; i++) free(s->paths[i]); + free(s->paths); + s->paths = NULL; + s->n = s->cap = 0; +} + +static int +sep_dep_cmp(const struct sepgraph *g, int a, int b) +{ + int r = strcmp(g->pkg[a].path, g->pkg[b].path); + if (r != 0) return r; + if (g->pkg[a].role != g->pkg[b].role) + return g->pkg[a].role - g->pkg[b].role; + return strcmp(g->pkg[a].artifact, g->pkg[b].artifact); +} + +/* 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 + * file-body composition; every later set must be identical. */ +static int +sep_load_pkg(struct sepgraph *g, int pi, int context) +{ + if (context < 0 || context >= g->ncontext) return -1; + if (g->pkg[pi].test_support + && g->support_context >= 0) + context = g->support_context; + if (g->pkg[pi].context_state[context] == 2) + return g->pkg[pi].failed ? -1 : 0; + if (g->pkg[pi].context_state[context] == 1) + return 0; + g->pkg[pi].context_state[context] = 1; + const char *searchpath = g->context[context].searchpath; + struct ImportSet fv = {0}, bindings = {0}; int rc = 0; - if (g->pkg[pi].is_dir) { - const char *test_package = g->pkg[pi].test_package[0] - ? g->pkg[pi].test_package : NULL; - g->pkg[pi].nsources = enumerate_dir_ww(g->pkg[pi].entry, - g->pkg[pi].variant, test_package, &g->pkg[pi].sources); - if (g->pkg[pi].nsources == -2) { - rc = -1; /* diagnosed in enumerate_dir_ww */ - } else if (g->pkg[pi].nsources < 0) { - fprintf(stderr, "ww: cannot read directory %s\n", - g->pkg[pi].entry); - rc = -1; - } else if (g->pkg[pi].nsources == 0) { - fprintf(stderr, - "ww: %s: directory contains no WW package sources\n", - g->pkg[pi].entry); - rc = -1; + if (!g->pkg[pi].loaded) { + g->pkg[pi].loaded = 1; + if (g->pkg[pi].is_dir) { + const char *test_package = g->pkg[pi].test_package[0] + ? g->pkg[pi].test_package : NULL; + g->pkg[pi].nsources = enumerate_dir_ww(g->pkg[pi].entry, + g->pkg[pi].variant, test_package, + &g->pkg[pi].sources); + if (g->pkg[pi].nsources == -2) { + rc = -1; /* diagnosed in enumerate_dir_ww */ + } else if (g->pkg[pi].nsources < 0) { + fprintf(stderr, "ww: cannot read directory %s\n", + g->pkg[pi].entry); + rc = -1; + } else if (g->pkg[pi].nsources == 0) { + fprintf(stderr, + "ww: %s: directory contains no WW package sources\n", + g->pkg[pi].entry); + rc = -1; + } } + } + if (g->pkg[pi].is_dir) { for (int i = 0; i < g->pkg[pi].nsources && rc == 0; i++) rc = sep_scan_file(g, pi, g->pkg[pi].sources[i], - searchpath, &fv, 1); + searchpath, &fv, &bindings, 1); if (rc == 0 && g->pkg[pi].path[0] != '\0' && !g->pkg[pi].test_support) { const char *dot = strrchr(g->pkg[pi].path, '.'); @@ -927,19 +1206,42 @@ sep_load_pkg(struct sepgraph *g, int pi, const char *searchpath) rc = -1; } } - } else { - rc = sep_scan_file(g, pi, g->pkg[pi].entry, searchpath, &fv, 0); + } else if (rc == 0) { + rc = sep_scan_file(g, pi, g->pkg[pi].entry, searchpath, + &fv, &bindings, 0); } - for (int i = 0; i < fv.n; i++) free(fv.paths[i]); - free(fv.paths); + sep_import_set_free(&fv); + if (bindings.n > 1) + qsort(bindings.paths, (size_t)bindings.n, + sizeof *bindings.paths, strs_cmp); + if (rc == 0 && g->pkg[pi].emit_context < 0) { + g->pkg[pi].bindings = bindings; + bindings.paths = NULL; + bindings.n = bindings.cap = 0; + g->pkg[pi].emit_context = context; + } else if (rc == 0) { + struct ImportSet *want = &g->pkg[pi].bindings; + if (want->n != bindings.n) rc = -1; + for (int i = 0; i < want->n && rc == 0; i++) + if (strcmp(want->paths[i], bindings.paths[i]) != 0) + rc = -1; + if (rc < 0) { + fprintf(stderr, + "ww: package %s resolves imports differently in %s and %s\n", + g->pkg[pi].path[0] ? g->pkg[pi].path : g->pkg[pi].name, + g->context[g->pkg[pi].emit_context].root, + g->context[context].root); + } + } + sep_import_set_free(&bindings); if (rc < 0) { + g->pkg[pi].context_state[context] = 2; g->pkg[pi].failed = 1; - return rc; + return -1; } /* Give a non-main root its declared identity before recursively loading - * dependencies. A back-edge can then reuse node 0 and reach the normal - * cycle detector instead of looking like a location alias. Executable - * roots are reset to the bare-root identity after 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' && g->pkg[pi].name[0] != '\0') { size_t n = strlen(g->pkg[pi].name); @@ -948,17 +1250,17 @@ sep_load_pkg(struct sepgraph *g, int pi, const char *searchpath) for (int i = 1; i < g->pkg[pi].ndeps; i++) { int v = g->pkg[pi].deps[i]; int j = i; - while (j > 0 && strcmp(g->pkg[g->pkg[pi].deps[j - 1]].path, - g->pkg[v].path) > 0) { + while (j > 0 && sep_dep_cmp(g, g->pkg[pi].deps[j - 1], v) > 0) { g->pkg[pi].deps[j] = g->pkg[pi].deps[j - 1]; j--; } g->pkg[pi].deps[j] = v; } - /* recurse into freshly-added deps (sep_find_or_add may have grown - * g->n during the scan; iterate by index). */ + /* Mark before recursion so a source cycle terminates here; topo emits the + * stable cycle diagnostic after all direct bindings are known. */ + g->pkg[pi].context_state[context] = 2; for (int k = 0; k < g->pkg[pi].ndeps; k++) - if (sep_load_pkg(g, g->pkg[pi].deps[k], searchpath) < 0) { + if (sep_load_pkg(g, g->pkg[pi].deps[k], context) < 0) { g->pkg[pi].failed = 1; return -1; } @@ -997,6 +1299,32 @@ sep_topo_visit(struct sepgraph *g, int pi, int *order, int *no, return 0; } +/* Special external-production actions may share a compiler module qualifier + * with an unrelated normal action in the command-global universe, but never + * in one product. A single link closure must remain an unambiguous package + * namespace. */ +static int +sep_validate_module_closure(struct sepgraph *g, const int *order, int n, + int include_root) +{ + for (int i = 0; i < n; i++) { + int a = order[i]; + if ((!include_root && g->pkg[a].root) + || g->pkg[a].path[0] == '\0') continue; + for (int j = i + 1; j < n; j++) { + int b = order[j]; + if (!include_root && g->pkg[b].root) continue; + if (strcmp(g->pkg[a].path, g->pkg[b].path) == 0) { + fprintf(stderr, + "ww: product closure contains multiple packages named %s\n", + g->pkg[a].path); + return -1; + } + } + } + return 0; +} + /* Emit one of pi's own source files into the sep-unit under the * //ww:module-reset primary boundary (so -c emits its decls, imported * ==0). DIRECTORY imports are skipped (provided as `.wwi` ahead of the @@ -1086,8 +1414,13 @@ sep_emit_body(FILE *out, const char *path, struct ImportSet *visited, * facts; the linker separately retains the reachable archive closure. */ static int sep_compose_unit(struct sepgraph *g, int pi, const char *scratch, - const char *searchpath, const char *unitf) + const char *unitf) { + if (g->pkg[pi].emit_context < 0 + || g->pkg[pi].emit_context >= g->ncontext) + return -1; + const char *searchpath = + g->context[g->pkg[pi].emit_context].searchpath; FILE *u = fopen(unitf, "wb"); if (u == NULL) { fprintf(stderr, "ww: cannot open %s\n", unitf); @@ -1095,7 +1428,7 @@ sep_compose_unit(struct sepgraph *g, int pi, const char *scratch, } for (int k = 0; k < g->pkg[pi].ndeps; k++) { int dj = g->pkg[pi].deps[k]; - char wwi[1024]; + char wwi[SEP_ARTIFACT_MAX]; sep_fname(g, dj, scratch, ".wwi", wwi, sizeof wwi); FILE *wf = fopen(wwi, "rb"); if (wf == NULL) { @@ -1297,8 +1630,39 @@ record_product_status(const char *path) static void workdir_stamp_text(char *buf, size_t bufsz, int is_test, int emit_asm) { - snprintf(buf, bufsz, "ww workdir fmt 4 mode %s asm %d\n", - is_test ? "test" : "build", emit_asm); + snprintf(buf, bufsz, "ww workdir fmt %d mode %s asm %d\n", + is_test ? 5 : 4, is_test ? "test" : "build", emit_asm); +} + +/* A stale global tool identity invalidates every committed unit voucher in + * this driver-owned workdir before compilation starts. Artifacts may remain, + * but without their unit they cannot be reused. That makes it safe to record + * the new identity after a partial multi-root pass: successful actions have + * current units, while failed and no-longer-requested actions do not. */ +static int +invalidate_workdir_units(const char *scratch) +{ + DIR *d = opendir(scratch); + if (d == NULL) return -1; + struct dirent *de; + int rc = 0; + while ((de = readdir(d)) != NULL) { + size_t n = strlen(de->d_name); + if (n < 8 || strcmp(de->d_name + n - 8, ".unit.ww") != 0) + continue; + char path[SEP_ARTIFACT_MAX]; + int pn = snprintf(path, sizeof path, "%s/%s", scratch, + de->d_name); + if (pn < 0 || (size_t)pn >= sizeof path + || (unlink(path) != 0 && errno != ENOENT)) { + rc = -1; + break; + } + } + if (closedir(d) != 0) rc = -1; + if (rc != 0) + fprintf(stderr, "ww: cannot invalidate stale package units\n"); + return rc; } /* build_sep_plan — discover dependencies for every requested product in one @@ -1312,8 +1676,8 @@ workdir_stamp_text(char *buf, size_t bufsz, int is_test, int emit_asm) static int build_one_sep_impl(const char *src, int entry_is_dir, const char *root_identity, const char *out, - const char *objstem, const char *extra_includes, const char *extra_libs, - const char *extra_libdirs, int package_only, int is_test, + const char *objstem, const char *extra_includes, + const struct seplinkflags *linkflags, int package_only, int is_test, struct sepproduct *products, int nproducts, int emit_asm, const char *workdir, char *scratchout, size_t scratchoutsz, struct sepgraph **graphout) @@ -1355,14 +1719,6 @@ build_one_sep_impl(const char *src, int entry_is_dir, srcd[n] = '\0'; } else { srcd[0] = '.'; srcd[1] = '\0'; } } - static char searchpath[4096]; - if (extra_includes && extra_includes[0]) - snprintf(searchpath, sizeof searchpath, "%s:%s:%s", - srcd, extra_includes, srcdir); - else - snprintf(searchpath, sizeof searchpath, "%s:%s", srcd, srcdir); - srcdir = searchpath; - char stem[1024]; if (entry_is_dir) { const char *b = strrchr(srcd, '/'); @@ -1417,16 +1773,39 @@ build_one_sep_impl(const char *src, int entry_is_dir, if (!stampok || !file_equal(toolc, c6) || (!emit_asm && !file_equal(toola, a6))) stale_all = 1; + if (stale_all && invalidate_workdir_units(scratch) != 0) + return 1; } struct sepgraph *g = calloc(1, sizeof *g); if (g == NULL) return 1; + g->support_context = -1; if (graphout) *graphout = g; const char *rootpath = package_only && root_identity ? root_identity : ""; for (int i = 0; i < nproducts; i++) { - products[i].root = sep_find_or_add_variant(g, rootpath, src, - entry_is_dir, products[i].variant, products[i].test_package, 1); + const char *entry = products[i].dir != NULL + ? products[i].dir : src; + char contextdir[1024]; + const char *contextroot = entry; + if (!entry_is_dir) { + const char *slash = strrchr(entry, '/'); + if (slash != NULL) { + size_t n = (size_t)(slash - entry); + if (n >= sizeof contextdir) return 1; + memcpy(contextdir, entry, n); + contextdir[n] = '\0'; + } else { + snprintf(contextdir, sizeof contextdir, "."); + } + contextroot = contextdir; + } + products[i].context = sep_context_for(g, contextroot, + extra_includes, toolsrcdir); + if (products[i].context < 0) return 1; + products[i].root = sep_find_or_add_variant(g, rootpath, entry, + entry_is_dir, products[i].variant, products[i].test_package, + SEP_ROLE_NORMAL, products[i].artifact, 1); if (products[i].root < 0) return 1; } const char *test_support_module = "test"; @@ -1439,43 +1818,53 @@ build_one_sep_impl(const char *src, int entry_is_dir, char tpath[1024]; int tdir = 0; if (locate_import(toolsrcdir, "test", tpath, sizeof tpath, &tdir)) { + g->support_context = sep_context_for(g, toolsrcdir, NULL, + toolsrcdir); + if (g->support_context < 0) return 1; char *tc = realpath(tpath, NULL); - char *rc = entry_is_dir ? realpath(src, NULL) : NULL; - int root_is_support = tc != NULL && rc != NULL - && strcmp(tc, rc) == 0; - free(tc); - free(rc); int collision = 0; - for (int i = 0; !root_is_support && i < nproducts; i++) { + for (int i = 0; i < nproducts; i++) { + int root_is_support = tc != NULL + && strcmp(g->pkg[products[i].root].canon, tc) == 0; const char *name = products[i].test_package; - if (name != NULL && (strcmp(name, "test") == 0 + if (!root_is_support && name != NULL + && (strcmp(name, "test") == 0 || strcmp(name, "test_test") == 0)) collision = 1; } - char userpath[1024]; - int userdir = 0; - if (!root_is_support && !collision - && locate_import(srcdir, "test", userpath, - sizeof userpath, &userdir)) { - (void)userdir; - tc = realpath(tpath, NULL); - char *uc = realpath(userpath, NULL); - if (tc != NULL && uc != NULL && strcmp(tc, uc) != 0) - collision = 1; - free(tc); - free(uc); + for (int i = 0; i < nproducts && !collision; i++) { + char userpath[1024]; + int userdir = 0; + if (locate_import(g->context[products[i].context].searchpath, + "test", userpath, sizeof userpath, &userdir)) { + (void)userdir; + char *uc = realpath(userpath, NULL); + if (tc != NULL && uc != NULL + && strcmp(tc, uc) != 0) + collision = 1; + free(uc); + } } if (collision) test_support_module = SEP_TEST_SUPPORT_MODULE; for (int i = 0; i < nproducts; i++) { int root = products[i].root; + int root_is_support = tc != NULL + && strcmp(g->pkg[root].canon, tc) == 0; /* A same-test build of the runtime package already owns run * and its source imports. An external test still needs the * 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) continue; - int ti = sep_find_or_add(g, test_support_module, tpath, - tdir); + int ti; + if (strcmp(test_support_module, + SEP_TEST_SUPPORT_MODULE) == 0) + ti = sep_find_or_add_role(g, test_support_module, + tpath, tdir, SEP_ROLE_TEST_SUPPORT, NULL); + else + ti = sep_find_or_add(g, test_support_module, tpath, + tdir); if (ti < 0) return 1; g->pkg[ti].test_support = 1; int seen = 0; @@ -1486,11 +1875,12 @@ build_one_sep_impl(const char *src, int entry_is_dir, if (!seen && g->pkg[root].ndeps < SEP_MAXPKG) g->pkg[root].deps[g->pkg[root].ndeps++] = ti; } + free(tc); } } for (int i = 0; i < nproducts; i++) { int root = products[i].root; - if (sep_load_pkg(g, root, srcdir) < 0) { + if (sep_load_pkg(g, root, products[i].context) < 0) { g->pkg[root].failed = 1; continue; } @@ -1503,6 +1893,8 @@ build_one_sep_impl(const char *src, int entry_is_dir, g->pkg[root].failed = 1; } } + if (sep_validate_artifact_paths(g, scratch) < 0) + return 1; int root_package = package_only; if (root_package && !g->pkg[products[0].root].failed && strcmp(g->pkg[products[0].root].name, "main") == 0) { @@ -1522,7 +1914,9 @@ build_one_sep_impl(const char *src, int entry_is_dir, if (g->pkg[root].failed) continue; 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) + if (sep_topo_visit(g, root, order, &ignored, stack, 0) < 0 + || sep_validate_module_closure(g, order, ignored, + root_package) < 0) g->pkg[root].failed = 1; } for (int pi = 0; pi < g->n; pi++) g->pkg[pi].color = 0; @@ -1551,9 +1945,11 @@ build_one_sep_impl(const char *src, int entry_is_dir, any_failed = 1; continue; } - char unitf[1024], wwi[1024], asmf[1024], obj[1024], apath[1024]; - char unitnew[1024], wwinew[1024], asmnew[1024], objnew[1024]; - char anew[1024], cmd[8192]; + char unitf[SEP_ARTIFACT_MAX], wwi[SEP_ARTIFACT_MAX]; + char asmf[SEP_ARTIFACT_MAX], obj[SEP_ARTIFACT_MAX]; + char apath[SEP_ARTIFACT_MAX], unitnew[SEP_ARTIFACT_MAX]; + char wwinew[SEP_ARTIFACT_MAX], asmnew[SEP_ARTIFACT_MAX]; + char objnew[SEP_ARTIFACT_MAX], anew[SEP_ARTIFACT_MAX], cmd[8192]; sep_fname(g, pi, scratch, ".unit.ww", unitf, sizeof unitf); sep_fname(g, pi, scratch, ".wwi", wwi, sizeof wwi); sep_fname(g, pi, scratch, ".s", asmf, sizeof asmf); @@ -1573,7 +1969,7 @@ build_one_sep_impl(const char *src, int entry_is_dir, 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, scratch, srcdir, cu) < 0) { + if (sep_compose_unit(g, pi, scratch, cu) < 0) { g->pkg[pi].failed = 1; any_failed = 1; continue; @@ -1664,10 +2060,11 @@ build_one_sep_impl(const char *src, int entry_is_dir, } } } - /* Tool identity commits only after every package artifact it vouches - * for is itself committed; a killed pass leaves the old identity and - * forces a full recompile, never a false reuse. */ - if (warm && !any_failed) { + /* Stale passes removed every old unit voucher before compiling. Current + * successful units are therefore safe to vouch for even when a sibling + * root failed; a killed pass leaves the old identity and forces another + * invalidating pass, never false reuse. */ + if (warm) { if (!file_equal(toolc, c6) && copy_file_atomic(c6, toolc) != 0) { fprintf(stderr, "ww: cannot record %s\n", toolc); @@ -1695,7 +2092,8 @@ build_one_sep_impl(const char *src, int entry_is_dir, if (root_package) { int root = products[0].root; if (g->pkg[root].failed) { free(order); return 1; } - char archive[1024], iface[1024], outiface[1100]; + char archive[SEP_ARTIFACT_MAX], iface[SEP_ARTIFACT_MAX]; + char outiface[SEP_ARTIFACT_MAX]; sep_fname(g, root, scratch, ".a", archive, sizeof archive); sep_fname(g, root, scratch, ".wwi", iface, sizeof iface); snprintf(outiface, sizeof outiface, "%s.wwi", out); @@ -1714,19 +2112,18 @@ build_one_sep_impl(const char *src, int entry_is_dir, * 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. */ - char rtargs[2048] = {0}; - char rtpath[1024]; - snprintf(rtpath, sizeof rtpath, "%s/libwwrt.a", libdir); - if (access(rtpath, 0) == 0) { - snprintf(rtargs, sizeof rtargs, "%s", rtpath); - } else { - char a1[1024], a2[1024]; - snprintf(a1, sizeof a1, "%s/../obj/rt/start.o", self_dir); - snprintf(a2, sizeof a2, "%s/../obj/rt/syscall.o", self_dir); - snprintf(rtargs, sizeof rtargs, "%s %s", a1, a2); + char rtpaths[2][1024]; + int nrt = 1; + snprintf(rtpaths[0], sizeof rtpaths[0], "%s/libwwrt.a", libdir); + if (access(rtpaths[0], 0) != 0) { + nrt = 2; + snprintf(rtpaths[0], sizeof rtpaths[0], + "%s/../obj/rt/start.o", self_dir); + snprintf(rtpaths[1], sizeof rtpaths[1], + "%s/../obj/rt/syscall.o", self_dir); } - const char *libargs = (extra_libs && extra_libs[0]) ? extra_libs : ""; - const char *libdirset = (extra_libdirs && extra_libdirs[0]) ? extra_libdirs : ""; + int nlibdirs = linkflags ? linkflags->nlibdirs : 0; + int nlibs = linkflags ? linkflags->nlibs : 0; for (int i = 0; i < nproducts; i++) { int root = products[i].root; if (g->pkg[root].failed) { any_failed = 1; continue; } @@ -1740,28 +2137,47 @@ build_one_sep_impl(const char *src, int entry_is_dir, free(linkstack); free(linkorder); return 1; } free(linkstack); - char objs[8192] = {0}; + size_t largvcap = (size_t)(3 + nlink + nrt + + 2 * nlibdirs + 2 * nlibs + 1); + char **largv = calloc(largvcap, sizeof *largv); + char (*linkpaths)[SEP_ARTIFACT_MAX] = calloc((size_t)nlink, + sizeof *linkpaths); + if (largv == NULL || linkpaths == NULL) { + free(linkpaths); free(largv); free(linkorder); + return 1; + } + int pos = 0, npath = 0; + largv[pos++] = "w6l"; + largv[pos++] = "-o"; + 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 && 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) continue; - char path[1024]; sep_fname(g, pi, scratch, - pi == root ? ".o" : ".a", path, sizeof path); - size_t n = strlen(objs); - snprintf(objs + n, sizeof objs - n, "%s%s", - n ? " " : "", path); + pi == root ? ".o" : ".a", linkpaths[npath], + sizeof linkpaths[npath]); + largv[pos++] = linkpaths[npath++]; } free(linkorder); - char cmd[16384]; - snprintf(cmd, sizeof cmd, "%s -o %s %s %s%s%s%s%s", - l6, products[i].out, objs, rtargs, - libdirset[0] ? " " : "", libdirset, - libargs[0] ? " " : "", libargs); - if (run(cmd) != 0) { + for (int ri = 0; ri < nrt; ri++) largv[pos++] = rtpaths[ri]; + for (int li = 0; li < nlibdirs; li++) { + largv[pos++] = "-L"; + largv[pos++] = (char *)linkflags->libdirs[li]; + } + for (int li = 0; li < nlibs; li++) { + largv[pos++] = "-l"; + largv[pos++] = (char *)linkflags->libs[li]; + } + largv[pos] = NULL; + int linkrc = run_argv(l6, largv); + free(linkpaths); + free(largv); + if (linkrc != 0) { fprintf(stderr, "ww: w6l failed\n"); g->pkg[root].failed = 1; any_failed = 1; @@ -1785,22 +2201,24 @@ build_one_sep_impl(const char *src, int entry_is_dir, static int build_one_sep(const char *src, int entry_is_dir, const char *root_identity, const char *out, - const char *objstem, const char *extra_includes, const char *extra_libs, - const char *extra_libdirs, int package_only, int is_test, + const char *objstem, const char *extra_includes, + const struct seplinkflags *linkflags, int package_only, int is_test, int root_variant, const char *test_package, int emit_asm, int keepscratch, const char *workdir) { char scratch[1100] = {0}; struct sepgraph *g = NULL; struct sepproduct product = { + .dir = src, .out = out, .test_package = test_package, .status = NULL, + .artifact = {0}, .variant = root_variant, .root = -1, }; int r = build_one_sep_impl(src, entry_is_dir, root_identity, out, objstem, - extra_includes, extra_libs, extra_libdirs, package_only, is_test, + extra_includes, linkflags, package_only, is_test, &product, 1, emit_asm, workdir, scratch, sizeof scratch, &g); sep_graph_free(g); @@ -1829,7 +2247,7 @@ build_one_sep(const char *src, int entry_is_dir, const char *root_identity, return r; } -/* The package coordinator submits all selected roots for one directory in one +/* The package coordinator submits every selected directory/variant root in one * request. Its first output owns the shared cold sepwork tree; every product * remains an independent root compile and link inside that tree. */ static int @@ -1839,7 +2257,7 @@ build_package_tests(const char *src, const char *extra_includes, char scratch[1100] = {0}; struct sepgraph *g = NULL; int r = build_one_sep_impl(src, 1, NULL, products[0].out, - products[0].out, extra_includes, "", "", 0, 1, + products[0].out, extra_includes, NULL, 0, 1, products, nproducts, 0, workdir, scratch, sizeof scratch, &g); sep_graph_free(g); return r; @@ -1920,8 +2338,7 @@ resolve_module(const char *name, const char *incs, char *out, size_t outsz, static int parse_build_flags(const char *cmd, int argc, char **argv, char *incs, size_t incsz, - char *libdirs, size_t libdirsz, - char *libs, size_t libsz, + struct seplinkflags *linkflags, char *outpath, size_t outsz, char *workdir, size_t workdirsz, const char **src_out, int *emit_asm_out, int *package_out) @@ -1961,31 +2378,39 @@ parse_build_flags(const char *cmd, int argc, char **argv, } snprintf(workdir, workdirsz, "%s", argv[i] + 2); } else if (strncmp(argv[i], "-l", 2) == 0 && argv[i][2]) { - size_t n = strlen(libs); - snprintf(libs + n, libsz - n, - "%s%s", n ? " " : "", argv[i]); + if (linkflags->nlibs >= SEP_MAXLFLAGS) { + fprintf(stderr, "ww %s: too many -l\n", cmd); + return -1; + } + linkflags->libs[linkflags->nlibs++] = argv[i] + 2; } else if (strcmp(argv[i], "-l") == 0) { if (i + 1 >= argc) { fprintf(stderr, "ww %s: -l needs an argument\n", cmd); return -1; } - size_t n = strlen(libs); - snprintf(libs + n, libsz - n, - "%s-l%s", n ? " " : "", argv[++i]); + if (linkflags->nlibs >= SEP_MAXLFLAGS) { + fprintf(stderr, "ww %s: too many -l\n", cmd); + return -1; + } + linkflags->libs[linkflags->nlibs++] = argv[++i]; } else if (strcmp(argv[i], "-L") == 0) { if (i + 1 >= argc) { fprintf(stderr, "ww %s: -L needs an argument\n", cmd); return -1; } - size_t n = strlen(libdirs); - snprintf(libdirs + n, libdirsz - n, - "%s-L%s", n ? " " : "", argv[++i]); + if (linkflags->nlibdirs >= SEP_MAXLFLAGS) { + fprintf(stderr, "ww %s: too many -L\n", cmd); + return -1; + } + linkflags->libdirs[linkflags->nlibdirs++] = argv[++i]; } else if (strncmp(argv[i], "-L", 2) == 0 && argv[i][2]) { - size_t n = strlen(libdirs); - snprintf(libdirs + n, libdirsz - n, - "%s%s", n ? " " : "", argv[i]); + if (linkflags->nlibdirs >= SEP_MAXLFLAGS) { + fprintf(stderr, "ww %s: too many -L\n", cmd); + return -1; + } + linkflags->libdirs[linkflags->nlibdirs++] = argv[i] + 2; } else if (strcmp(argv[i], "-I") == 0) { if (i + 1 >= argc) { fprintf(stderr, @@ -2024,15 +2449,14 @@ static int do_build(int argc, char **argv) { const char *src = NULL; - char libs[2048] = {0}; - char libdirs[2048] = {0}; + struct seplinkflags linkflags = {0}; char incs[2048] = {0}; char outflag[1024] = {0}; char workdir[1024] = {0}; int emit_asm = 0; int package_only = 0; if (parse_build_flags("build", argc, argv, incs, sizeof incs, - libdirs, sizeof libdirs, libs, sizeof libs, + &linkflags, outflag, sizeof outflag, workdir, sizeof workdir, &src, &emit_asm, &package_only) < 0) return 2; @@ -2071,8 +2495,8 @@ do_build(int argc, char **argv) basename_no_ext(resolved, out, sizeof out); } const char *root_identity = package_only && !literal ? src : NULL; - return build_one_sep(resolved, is_dir, root_identity, out, objstem, incs, libs, - libdirs, package_only, 0, SEP_VARIANT_PRODUCTION, NULL, emit_asm, + return build_one_sep(resolved, is_dir, root_identity, out, objstem, incs, + &linkflags, package_only, 0, SEP_VARIANT_PRODUCTION, NULL, emit_asm, 1, workdir); } @@ -2080,12 +2504,11 @@ static int do_run(int argc, char **argv) { const char *src = NULL; - char libs[2048] = {0}; - char libdirs[2048] = {0}; + struct seplinkflags linkflags = {0}; char incs[2048] = {0}; char outflag[1024] = {0}; /* -o accepted+ignored: run always uses the temp */ int next = parse_build_flags("run", argc, argv, incs, sizeof incs, - libdirs, sizeof libdirs, libs, sizeof libs, + &linkflags, outflag, sizeof outflag, NULL, 0, &src, NULL, NULL); if (next < 0) return 2; if (src == NULL) src = "."; @@ -2104,7 +2527,7 @@ do_run(int argc, char **argv) snprintf(tmp, sizeof tmp, "%s/main", tmpdir); /* The freshly acquired directory owns both the executable and the * adjacent main.sepwork tree. Nothing outside it is adopted or removed. */ - if (build_one_sep(resolved, is_dir, NULL, tmp, tmp, incs, libs, libdirs, + if (build_one_sep(resolved, is_dir, NULL, tmp, tmp, incs, &linkflags, 0, 0, SEP_VARIANT_PRODUCTION, NULL, 0, 0, NULL) != 0) { if (unlink(tmp) != 0 && errno != ENOENT) fputs("ww: cannot remove temporary output\n", stderr); @@ -2198,13 +2621,14 @@ do_test(int argc, char **argv) } else if (strcmp(argv[i], "-c") == 0) { compileonly = 1; } else if (strcmp(argv[i], "--ww-package-test") == 0) { - if (i + 4 >= argc || nproducts >= SEP_MAXPRODUCT) { + if (i + 5 >= argc || nproducts >= SEP_MAXPRODUCT) { fprintf(stderr, - "ww test: --ww-package-test needs kind, package, output, and status\n"); + "ww test: --ww-package-test needs kind, package, directory, output, and status\n"); return 2; } const char *kind = argv[++i]; const char *name = argv[++i]; + const char *dir = argv[++i]; const char *output = argv[++i]; const char *status = argv[++i]; size_t pn = strlen(name); @@ -2213,7 +2637,8 @@ do_test(int argc, char **argv) if ((strcmp(kind, "same") != 0 && strcmp(kind, "external") != 0) || pn == 0 || pn >= sizeof ((struct seppkg *)0)->name - || output[0] == '\0' || status[0] == '\0' + || dir[0] == '\0' || output[0] == '\0' + || status[0] == '\0' || (strcmp(kind, "external") == 0 && (pn <= 5 || strcmp(name + pn - 5, @@ -2222,15 +2647,11 @@ do_test(int argc, char **argv) "ww test: invalid --ww-package-test variant\n"); return 2; } - for (int p = 0; p < nproducts; p++) - if (products[p].variant == variant) { - fprintf(stderr, - "ww test: duplicate --ww-package-test variant\n"); - return 2; - } + products[nproducts].dir = dir; products[nproducts].out = output; products[nproducts].test_package = name; products[nproducts].status = status; + products[nproducts].artifact[0] = '\0'; products[nproducts].variant = variant; products[nproducts].root = -1; nproducts++; @@ -2287,12 +2708,26 @@ do_test(int argc, char **argv) for (int i = 1; i < nproducts; i++) { struct sepproduct p = products[i]; int j = i; - while (j > 0 && products[j - 1].variant > p.variant) { + while (j > 0 && (strcmp(products[j - 1].dir, p.dir) > 0 + || (strcmp(products[j - 1].dir, p.dir) == 0 + && products[j - 1].variant > p.variant))) { products[j] = products[j - 1]; j--; } products[j] = p; } + for (int i = 0; i < nproducts; i++) { + if (i > 0 && strcmp(products[i - 1].dir, products[i].dir) == 0 + && products[i - 1].variant == products[i].variant) { + fprintf(stderr, + "ww test: duplicate --ww-package-test variant for directory\n"); + return 2; + } + snprintf(products[i].artifact, sizeof products[i].artifact, + "__ww-test-%03d-%s", i, + products[i].variant == SEP_VARIANT_SAME_TEST + ? "same" : "external"); + } if (nproducts != 0 && packageopts) { fprintf(stderr, "ww test: package-test variant rejects package options\n"); @@ -2332,7 +2767,7 @@ do_test(int argc, char **argv) return 2; } /* -w forwards: the coordinator keys one persistent driver - * workdir per directory plan under the given root. */ + * workdir for the complete selected test request. */ return exec_package_tests(argc, argv, src, NULL, 0); } struct stat st; @@ -2405,7 +2840,7 @@ do_test(int argc, char **argv) /* No-o redirects internal scratch to /tmp rather than beside the * source. An explicit -o names the caller-owned artifact stem. */ int br = build_one_sep(resolved, is_dir, NULL, outp, - outstem[0] ? outstem : tmp, incs, "", "", 0, 1, + outstem[0] ? outstem : tmp, incs, NULL, 0, 1, SEP_VARIANT_PRODUCTION, NULL, emit_asm, outstem[0] ? 1 : 0, workdir); if (br != 0) { @@ -2468,7 +2903,7 @@ do_test(int argc, char **argv) } /* See module-mode note: no-o scratch is redirected to /tmp. */ int br = build_one_sep(target, 0, NULL, outp, outstem[0] ? outstem : tmp, - incs, "", "", 0, 1, SEP_VARIANT_PRODUCTION, NULL, emit_asm, + incs, NULL, 0, 1, SEP_VARIANT_PRODUCTION, NULL, emit_asm, outstem[0] ? 1 : 0, workdir); if (br != 0) { if (owntmp && unlink(outp) != 0 && errno != ENOENT) diff --git a/internal/wwpackage/package.ww b/internal/wwpackage/package.ww index 05b562c1..b02dcbde 100644 --- a/internal/wwpackage/package.ww +++ b/internal/wwpackage/package.ww @@ -106,8 +106,8 @@ fn pkgusage() void = { let s: str = strings.concat( "usage: wwtest package [-c] [-list] [-j N] [-I DIR] [-w DIR] [-run|-filter GLOB] [-timeout-ms=N] [DIR | DIR/...] [-- GLOB ...]\n", " *_test.ww is the sole test-source form; @test elsewhere is rejected\n", - " -c retains the compiled package binaries; -j N runs up to N directory builds or test binaries at once\n", - " -w DIR keys a persistent shared build workdir per package directory\n"); + " -c retains the compiled package binaries; -j N runs up to N build or test processes at once\n", + " -w DIR keys one persistent shared build workdir for the test request\n"); pkgput(os.STDERR_FILENO, s); }; @@ -530,8 +530,8 @@ fn pkgsetplanpaths(p: *pkgplan, groups: []pkggroup, root: str, index: i32, if (!pkgmakedir(p.root)) { return false; }; p.workdir = ""; if (workroot.len != 0) { - // The first byte-sorted product name is stable metadata in the - // injective directory key; every product in this plan shares the dir. + // The request root and first byte-sorted product name make one stable + // escaped key for the command-scoped driver workdir. p.workdir = strings.concat(workroot, "/", pkgworkkey(p.dir, groups[p.start].pkg)); match (os.mkdirs(p.workdir, 448)) { @@ -600,7 +600,7 @@ fn pkgreportcommand(kind: str, g: *pkggroup, r: *exec.result) void = { fn pkgstartbuild(p: *pkgplan, groups: []pkggroup, builder: str, includes: []str, h: *exec.process) void = { let ba: []str = alloc([], - (8 + (p.end - p.start) * 5 + includes.len * 2): u64)!; + (8 + (p.end - p.start) * 6 + includes.len * 2): u64)!; append(ba, builder); append(ba, "test"); append(ba, "-c"); @@ -611,6 +611,7 @@ fn pkgstartbuild(p: *pkgplan, groups: []pkggroup, builder: str, includes: []str, if (g.external) { append(ba, "external"); } else { append(ba, "same"); }; append(ba, g.pkg); + append(ba, g.dir); append(ba, g.bin); append(ba, g.buildok); i += 1; @@ -976,20 +977,13 @@ export fn packagecommand(args: []str) int = { "wwtest package: cannot use -o with multiple packages"); return 2; }; - let plans: []pkgplan = alloc([], groups.len: u64)!; - i = 0; - for (i < groups.len) { - let p: pkgplan; - p.dir = groups[i].dir; - p.start = i; - for (i < groups.len - && strings.compare(groups[i].dir, p.dir) == 0) { - i += 1; - }; - p.end = i; - p.state = PKGQUEUED; - append(plans, p); - }; + let plans: []pkgplan = alloc([], 1u64)!; + let plan: pkgplan; + plan.dir = discoverroot; + plan.start = 0; + plan.end = groups.len; + plan.state = PKGQUEUED; + append(plans, plan); let borrowed: str = temp.dir(); let tmproot: str = strings.dup(borrowed); @@ -1009,9 +1003,9 @@ export fn packagecommand(args: []str) int = { i += 1; }; - // Build one shared package plan per directory. As soon as a plan completes, - // its successful products may run under the same global -j bound while - // other directory builds remain active. Emission stays ordered below. + // Build the complete request in one command-owned package universe. Once + // the union build completes, successful products run under the same global + // -j bound. Emission stays in byte-sorted group order below. let handles: []exec.process = alloc([], plans.len: u64)!; i = 0; for (i < plans.len) { diff --git a/selfhost/cmd/ww/main.ww b/selfhost/cmd/ww/main.ww index 10f690d0..1f16ff22 100644 --- a/selfhost/cmd/ww/main.ww +++ b/selfhost/cmd/ww/main.ww @@ -417,8 +417,13 @@ 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_ROLE_NORMAL: i32 = 0; +def SEP_ROLE_EXTERNAL_PRODUCTION: i32 = 1; +def SEP_ROLE_TEST_SUPPORT: i32 = 2; def SEP_TEST_SUPPORT_MODULE: str = "__wwtest"; -def SEP_MAXPRODUCT: i32 = 2; +def SEP_MAXPRODUCT: i32 = 256; +def SEP_MAXCONTEXT: i32 = 257; +def SEP_ARTIFACT_MAX: i32 = 1024; // Classify a selected directory entry: 1 production, 2 test, 0 skipped, // -1 @test outside *_test.ww, -2 non-regular source. @@ -719,38 +724,62 @@ type lflags = struct { def SEP_MAXPKG: i32 = 256; +type sepbind = struct { + kind: u8, + name: str, + target: *u8, +}; + type seppkg = struct { path: *u8, // dotted import path, NUL-term; root path[0]==0 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 testpackage: *u8, sources: **u8, // owned, byte-sorted selected paths; dirs only nsources: i32, isdir: i32, variant: i32, + role: i32, root: bool, failed: bool, testsupport: bool, + loaded: bool, + emitcontext: i32, + contextstate: []u8, + bindings: []sepbind, deps: []i32, // direct-dep indices into sepgraph.pkg ndeps: i32, color: i32, // tri-color DFS: 0 white, 1 gray, 2 black }; +type sepcontext = struct { + root: *u8, + searchpath: *u8, +}; + type sepgraph = struct { pkg: []seppkg, // alloc'd SEP_MAXPKG n: i32, + context: []sepcontext, + ncontext: i32, + supportcontext: i32, }; type sepproduct = struct { + dir: *u8, out: *u8, testpackage: *u8, status: *u8, + artifact: *u8, variant: i32, + context: i32, root: i32, }; fn sepfindoraddvariant(g: *sepgraph, path: *u8, entry: *u8, - isdir: i32, variant: i32, testpackage: *u8, root: bool) i32 = { + isdir: i32, variant: i32, testpackage: *u8, role: i32, + artifact: *u8, root: bool) i32 = { if (cstrlen(path) >= 256u64) { cerr("ww: package path is too long (limit 255 bytes)\n"); return -1; @@ -759,23 +788,69 @@ fn sepfindoraddvariant(g: *sepgraph, path: *u8, entry: *u8, for (i < g.n) { let samelocation: bool = os.samefile(pathstr(g.pkg[i].entry), pathstr(entry)); - let testrootpair: bool = samelocation && isdir != 0 - && g.pkg[i].isdir != 0 - && ((root && g.pkg[i].root - && variant != g.pkg[i].variant) - || (root && !g.pkg[i].root - && variant != SEP_VARIANT_PRODUCTION - && g.pkg[i].variant == SEP_VARIANT_PRODUCTION) - || (!root && g.pkg[i].root + if (root || g.pkg[i].root) { + 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 "); + cerr(pathstr(entry)); cerr("\n"); + return -1; + }; + // A cycle back to an ordinary production root reuses that root after + // loading assigns its declared identity. Test roots stay distinct. + if (samelocation && variant == SEP_VARIANT_PRODUCTION - && g.pkg[i].variant != SEP_VARIANT_PRODUCTION)); - if (cstreq(g.pkg[i].path, path)) { - if (testrootpair) { + && g.pkg[i].variant == SEP_VARIANT_PRODUCTION + && cstreq(g.pkg[i].path, path)) { + return i; + }; + if (!samelocation) { i += 1; continue; }; + let rootvariant: i32 = variant; + let productionvariant: i32 = g.pkg[i].variant; + if (!root) { + rootvariant = g.pkg[i].variant; + productionvariant = variant; + }; + if (isdir != 0 && g.pkg[i].isdir != 0 + && rootvariant != SEP_VARIANT_PRODUCTION + && productionvariant == SEP_VARIANT_PRODUCTION) { i += 1; continue; }; - if (!os.samefile(pathstr(g.pkg[i].entry), pathstr(entry)) - || g.pkg[i].variant != variant) { + cerr("ww: package directory "); cerr(pathstr(entry)); + cerr(" has incompatible root and production variants\n"); + return -1; + }; + let samepath: bool = cstreq(g.pkg[i].path, path); + let sameartifact: bool = true; + if (role == SEP_ROLE_EXTERNAL_PRODUCTION) { + sameartifact = g.pkg[i].artifact != nil && artifact != nil + && cstreq(g.pkg[i].artifact, artifact); + }; + let sameaction: bool = samepath && samelocation + && g.pkg[i].variant == variant && g.pkg[i].role == role + && sameartifact; + if (sameaction) { return i; }; + // An external test consumes the one canonical production action for + // its directory. The role only separates physically different + // packages that happen to use the same source qualifier. + if (samepath && samelocation + && g.pkg[i].variant == SEP_VARIANT_PRODUCTION + && variant == SEP_VARIANT_PRODUCTION + && ((role == SEP_ROLE_EXTERNAL_PRODUCTION + && g.pkg[i].role == SEP_ROLE_NORMAL) + || (role == SEP_ROLE_NORMAL + && g.pkg[i].role == SEP_ROLE_EXTERNAL_PRODUCTION))) { + return i; + }; + if (samepath && (role == SEP_ROLE_EXTERNAL_PRODUCTION + || g.pkg[i].role == SEP_ROLE_EXTERNAL_PRODUCTION)) { + i += 1; + continue; + }; + if (samepath) { + if (!samelocation || g.pkg[i].variant != variant + || g.pkg[i].role != role) { cerr("ww: package "); cerr(pathstr(path)); cerr(" resolves to more than one location\n"); return -1; @@ -783,7 +858,8 @@ fn sepfindoraddvariant(g: *sepgraph, path: *u8, entry: *u8, return i; }; if (samelocation) { - if (testrootpair) { + if (role == SEP_ROLE_TEST_SUPPORT + || g.pkg[i].role == SEP_ROLE_TEST_SUPPORT) { i += 1; continue; }; @@ -807,6 +883,7 @@ fn sepfindoraddvariant(g: *sepgraph, path: *u8, entry: *u8, let elen: u64 = cstrlen(entry); g.pkg[g.n].path = arenadupcstr(path, plen); g.pkg[g.n].entry = arenadupcstr(entry, elen); + g.pkg[g.n].artifact = artifact; g.pkg[g.n].name = nil; g.pkg[g.n].testpackage = nil; if (testpackage != nil) { @@ -817,9 +894,17 @@ fn sepfindoraddvariant(g: *sepgraph, path: *u8, entry: *u8, g.pkg[g.n].nsources = 0; g.pkg[g.n].isdir = isdir; g.pkg[g.n].variant = variant; + g.pkg[g.n].role = role; g.pkg[g.n].root = root; g.pkg[g.n].failed = false; g.pkg[g.n].testsupport = false; + g.pkg[g.n].loaded = false; + g.pkg[g.n].emitcontext = -1; + let cslot: []u8 = alloc([], SEP_MAXCONTEXT: u64)!; + cslot.len = SEP_MAXCONTEXT; + g.pkg[g.n].contextstate = cslot; + let emptybindings: []sepbind; + g.pkg[g.n].bindings = emptybindings; let dslot: []i32 = alloc([], SEP_MAXPKG: u64)!; dslot.len = SEP_MAXPKG; g.pkg[g.n].deps = dslot; @@ -832,7 +917,13 @@ fn sepfindoraddvariant(g: *sepgraph, path: *u8, entry: *u8, fn sepfindoradd(g: *sepgraph, path: *u8, entry: *u8, isdir: i32) i32 = { return sepfindoraddvariant(g, path, entry, isdir, - SEP_VARIANT_PRODUCTION, nil, false); + SEP_VARIANT_PRODUCTION, nil, SEP_ROLE_NORMAL, nil, false); +}; + +fn sepfindoraddrole(g: *sepgraph, path: *u8, entry: *u8, isdir: i32, + role: i32, artifact: *u8) i32 = { + return sepfindoraddvariant(g, path, entry, isdir, + SEP_VARIANT_PRODUCTION, nil, role, artifact, false); }; // Release the package-owned directory-membership lists through one graph @@ -859,29 +950,83 @@ fn sepgraphfree(g: *sepgraph) void = { }; }; +fn sepcontextfor(g: *sepgraph, root: *u8, incs: *u8, + toolsrcdir: *u8) i32 = { + let cap: u64 = (os.PATH_MAX: u64) * 2u64; + let need: u64 = cstrlen(root) + 1u64 + cstrlen(toolsrcdir) + 1u64; + if (incs != nil && incs[0u64] != 0u8) { + need += cstrlen(incs) + 1u64; + }; + if (need > cap) { + cerr("ww: package import search path is too long\n"); + return -1; + }; + let search: []u8 = alloc([], cap)!; + search.len = cap: i32; + let off: u64 = cstrinto(search.ptr, 0u64, root); + off = byteinto(search.ptr, off, 58u8); + if (incs != nil && incs[0u64] != 0u8) { + off = cstrinto(search.ptr, off, incs); + off = byteinto(search.ptr, off, 58u8); + }; + off = cstrinto(search.ptr, off, toolsrcdir); + cstrseal(search.ptr, off); + let i: i32 = 0; + for (i < g.ncontext) { + if (cstreq(g.context[i].searchpath, search.ptr)) { return i; }; + i += 1; + }; + if (g.ncontext >= SEP_MAXCONTEXT) { + cerr("ww: too many package import contexts\n"); + return -1; + }; + g.context[g.ncontext].root = root; + g.context[g.ncontext].searchpath = search.ptr; + let result: i32 = g.ncontext; + g.ncontext += 1; + return result; +}; + // Build one artifact path. Test roots use distinct names even though both // compiler units reset to the bare executable namespace. fn sepfname(g: *sepgraph, pi: i32, scratch: *u8, suffix: str) *u8 = { - let buf: []u8 = alloc([], (os.PATH_MAX: u64))!; - buf.len = os.PATH_MAX; + let buf: []u8 = alloc([], SEP_ARTIFACT_MAX: u64)!; + buf.len = SEP_ARTIFACT_MAX; let off: u64 = cstrinto(buf.ptr, 0u64, scratch); off = byteinto(buf.ptr, off, 47u8); // '/' - if (g.pkg[pi].root - && g.pkg[pi].variant == SEP_VARIANT_SAME_TEST) { - off = strinto(buf.ptr, off, "__ww-test-same"); - } else { if (g.pkg[pi].root - && g.pkg[pi].variant == SEP_VARIANT_EXTERNAL) { - off = strinto(buf.ptr, off, "__ww-test-external"); + if (g.pkg[pi].artifact != nil) { + off = cstrinto(buf.ptr, off, g.pkg[pi].artifact); } else { if (g.pkg[pi].path[0u64] != 0u8) { off = cstrinto(buf.ptr, off, g.pkg[pi].path); } else { off = strinto(buf.ptr, off, "__root"); - }; }; }; + }; }; off = strinto(buf.ptr, off, suffix); cstrseal(buf.ptr, off); return buf.ptr; }; +// sepfname uses SEP_ARTIFACT_MAX storage. Validate the longest suffix once +// before opening files so two action identities can never alias by truncation. +fn sepvalidateartifactpaths(g: *sepgraph, scratch: *u8) i32 = { + let i: i32 = 0; + for (i < g.n) { + let base: *u8 = g.pkg[i].artifact; + if (base == nil) { + base = g.pkg[i].path; + if (base[0u64] == 0u8) { base = "__root\0".ptr; }; + }; + let need: u64 = cstrlen(scratch) + 1u64 + cstrlen(base) + + ".unit.new".len: u64 + 1u64; + if (need > SEP_ARTIFACT_MAX: u64) { + cerr("ww: package artifact path is too long\n"); + return -1; + }; + i += 1; + }; + return 0; +}; + fn sepexternalname(pkg: *seppkg, path: *u8, n: u64, leafonly: bool) bool = { if (pkg.variant != SEP_VARIANT_EXTERNAL || pkg.testpackage == nil) { @@ -912,13 +1057,59 @@ fn sepexternalproduction(pkg: *seppkg, path: *u8, n: u64) bool = { return sepexternalname(pkg, path, n, false); }; +fn sepbindadd(bindings: *[]sepbind, kind: u8, name: str, + target: *u8) void = { + let i: i32 = 0; + for (i < len(*bindings)) { + let b: sepbind = (*bindings)[i]; + if (b.kind == kind && syntax.streq(b.name, name)) { + if (target == nil && b.target == nil) { return; }; + if (target != nil && b.target != nil + && os.samefile(pathstr(target), pathstr(b.target))) { + return; + }; + }; + i += 1; + }; + append(*bindings, sepbind { + kind = kind, + name = strings.dup(name), + target = target, + }); +}; + +fn sepbindsame(a: []sepbind, b: []sepbind) bool = { + if (len(a) != len(b)) { return false; }; + let i: i32 = 0; + for (i < len(a)) { + let found: bool = false; + let j: i32 = 0; + for (j < len(b)) { + if (a[i].kind == b[j].kind + && syntax.streq(a[i].name, b[j].name)) { + if (a[i].target == nil && b[j].target == nil) { + found = true; + } else { if (a[i].target != nil && b[j].target != nil + && os.samefile(pathstr(a[i].target), + pathstr(b[j].target))) { + found = true; + }; }; + }; + j += 1; + }; + if (!found) { return false; }; + i += 1; + }; + return true; +}; + // Scan one already-selected source file for its leading package clause // (when it is an owned directory source) and top-level imports. A DIRECTORY // import is a package boundary: add as a direct dep of pi. A FILE import is an // intra-package split: fold its imports into pi. Mirrors cstage // sep_scan_file (collects PATHS, not bytes). fn sepscanfile(g: *sepgraph, pi: i32, file: *u8, searchpath: *u8, - fv: *expctx, ownedsource: i32) i32 = { + fv: *expctx, bindings: *[]sepbind, ownedsource: i32) i32 = { let fview: str; fview.ptr = file; fview.len = cstrlen(file): i32; @@ -1039,6 +1230,7 @@ fn sepscanfile(g: *sepgraph, pi: i32, file: *u8, searchpath: *u8, }; if (ipath != nil) { if (isdir != 0) { + sepbindadd(bindings, 'D': u8, u.usepath, ipath); let self: bool = os.samefile(pathstr(ipath), pathstr(g.pkg[pi].entry)); if (self && sepexternalname(&g.pkg[pi], idp, idn, true)) { @@ -1054,11 +1246,33 @@ fn sepscanfile(g: *sepgraph, pi: i32, file: *u8, searchpath: *u8, cerr("' cannot import itself\n"); return -1; }; + let runtimeproduction: bool = false; + if (externalproduction) { + let gi: i32 = 0; + for (gi < g.n) { + if (g.pkg[gi].testsupport + && syntax.streq(pathstr(g.pkg[gi].path), + u.usepath) + && os.samefile(pathstr(g.pkg[gi].entry), + pathstr(ipath))) { + runtimeproduction = true; + }; + gi += 1; + }; + }; let nm: []u8 = alloc([], idn + 1u64)!; let k: u64 = 0u64; for (k < idn) { nm[k] = idp[k]; k += 1u64; }; nm[idn] = 0u8; - let di: i32 = sepfindoradd(g, nm.ptr, ipath, 1); + let di: i32 = -1; + if (externalproduction && !runtimeproduction) { + let art: *u8 = appendlit(g.pkg[pi].artifact, + "-production"); + di = sepfindoraddrole(g, nm.ptr, ipath, 1, + SEP_ROLE_EXTERNAL_PRODUCTION, art); + } else { + di = sepfindoradd(g, nm.ptr, ipath, 1); + }; if (di < 0) { return -1; }; let seen: bool = false; let m: i32 = 0; @@ -1072,7 +1286,9 @@ fn sepscanfile(g: *sepgraph, pi: i32, file: *u8, searchpath: *u8, g.pkg[pi].ndeps += 1; }; } else { - if (sepscanfile(g, pi, ipath, searchpath, fv, 0) < 0) { + sepbindadd(bindings, 'F': u8, u.usepath, ipath); + if (sepscanfile(g, pi, ipath, searchpath, fv, + bindings, 0) < 0) { return -1; }; }; @@ -1098,6 +1314,8 @@ fn sepscanfile(g: *sepgraph, pi: i32, file: *u8, searchpath: *u8, os.write(2, idp, idn); cerr("\n"); return -1; + } else { + sepbindadd(bindings, 'I': u8, u.usepath, nil); }; }; }; @@ -1106,22 +1324,42 @@ fn sepscanfile(g: *sepgraph, pi: i32, file: *u8, searchpath: *u8, return 0; }; +fn sepdepcmp(g: *sepgraph, a: i32, b: i32) i32 = { + let r: i32 = strings.compare(pathstr(g.pkg[a].path), + pathstr(g.pkg[b].path)): i32; + if (r != 0) { return r; }; + if (g.pkg[a].role < g.pkg[b].role) { return -1; }; + if (g.pkg[a].role > g.pkg[b].role) { return 1; }; + if (g.pkg[a].artifact == nil && g.pkg[b].artifact == nil) { return 0; }; + if (g.pkg[a].artifact == nil) { return -1; }; + if (g.pkg[b].artifact == nil) { return 1; }; + return strings.compare(pathstr(g.pkg[a].artifact), + pathstr(g.pkg[b].artifact)): i32; +}; + // Load pi once: a directory node takes ownership of its sorted production -// paths, then the same stored list supplies package-name validation and -// dependency scanning. Recurse over the resulting edges. `color` doubles as -// a loaded marker (2); reset to white before topo. -fn seploadpkg(g: *sepgraph, pi: i32, searchpath: *u8) i32 = { - if (g.pkg[pi].color == 2) { +// paths, then every selected-root context verifies the same canonical import +// bindings before the package is compiled once. +fn seploadpkg(g: *sepgraph, pi: i32, context: i32) i32 = { + if (g.pkg[pi].testsupport + && g.supportcontext >= 0) { context = g.supportcontext; }; + if (context < 0 || context >= g.ncontext) { return -1; }; + if (g.pkg[pi].contextstate[context] == 2u8) { if (g.pkg[pi].failed) { return -1; }; return 0; }; - g.pkg[pi].color = 2; + if (g.pkg[pi].contextstate[context] == 1u8) { return 0; }; + g.pkg[pi].contextstate[context] = 1u8; + let searchpath: *u8 = g.context[context].searchpath; let fv: expctx; fv.out = -1; fv.dirs = searchpath; fv.visit = nil; + let bindings: []sepbind; let rc: i32 = 0; - if (g.pkg[pi].isdir != 0) { + if (!g.pkg[pi].loaded) { + g.pkg[pi].loaded = true; + if (g.pkg[pi].isdir != 0) { let sources: **u8; let nsources: i32; sources, nsources = enumeratedir(g.pkg[pi].entry, @@ -1142,11 +1380,14 @@ fn seploadpkg(g: *sepgraph, pi: i32, searchpath: *u8) i32 = { cerr(": directory contains no WW package sources\n"); rc = -1; }; }; }; + }; + }; + if (g.pkg[pi].isdir != 0) { let i: i32 = 0; for (i < g.pkg[pi].nsources) { if (rc == 0) { rc = sepscanfile(g, pi, g.pkg[pi].sources[i], - searchpath, &fv, 1); + searchpath, &fv, &bindings, 1); }; i += 1; }; @@ -1168,10 +1409,28 @@ fn seploadpkg(g: *sepgraph, pi: i32, searchpath: *u8) i32 = { rc = -1; }; }; - } else { - rc = sepscanfile(g, pi, g.pkg[pi].entry, searchpath, &fv, 0); + } else { if (rc == 0) { + rc = sepscanfile(g, pi, g.pkg[pi].entry, searchpath, + &fv, &bindings, 0); + }; }; + if (rc == 0 && g.pkg[pi].emitcontext < 0) { + g.pkg[pi].bindings = bindings; + g.pkg[pi].emitcontext = context; + } else { if (rc == 0 && !sepbindsame(g.pkg[pi].bindings, bindings)) { + cerr("ww: package "); + if (g.pkg[pi].path[0u64] != 0u8) { + cerr(pathstr(g.pkg[pi].path)); + } else { cerr(pathstr(g.pkg[pi].name)); }; + cerr(" resolves imports differently in "); + cerr(pathstr(g.context[g.pkg[pi].emitcontext].root)); + cerr(" and "); cerr(pathstr(g.context[context].root)); cerr("\n"); + rc = -1; + }; }; + if (rc < 0) { + g.pkg[pi].contextstate[context] = 2u8; + g.pkg[pi].failed = true; + return rc; }; - if (rc < 0) { g.pkg[pi].failed = true; return rc; }; if (g.pkg[pi].root && g.pkg[pi].path[0u64] == 0u8 && g.pkg[pi].name != nil) { g.pkg[pi].path = arenadupcstr(g.pkg[pi].name, @@ -1181,18 +1440,18 @@ fn seploadpkg(g: *sepgraph, pi: i32, searchpath: *u8) i32 = { for (si < g.pkg[pi].ndeps) { let v: i32 = g.pkg[pi].deps[si]; let sj: i32 = si; - for (sj > 0 && strings.compare( - pathstr(g.pkg[g.pkg[pi].deps[sj - 1]].path), - pathstr(g.pkg[v].path)) > 0) { + for (sj > 0 && sepdepcmp(g, + g.pkg[pi].deps[sj - 1], v) > 0) { g.pkg[pi].deps[sj] = g.pkg[pi].deps[sj - 1]; sj -= 1; }; g.pkg[pi].deps[sj] = v; si += 1; }; + g.pkg[pi].contextstate[context] = 2u8; let k: i32 = 0; for (k < g.pkg[pi].ndeps) { - if (seploadpkg(g, g.pkg[pi].deps[k], searchpath) < 0) { + if (seploadpkg(g, g.pkg[pi].deps[k], context) < 0) { g.pkg[pi].failed = true; return -1; }; @@ -1242,6 +1501,30 @@ fn septopovisit(g: *sepgraph, pi: i32, order: []i32, no: *i32, return 0; }; +fn sepvalidatemoduleclosure(g: *sepgraph, order: []i32, n: i32, + includeroot: bool) i32 = { + let i: i32 = 0; + for (i < n) { + let a: i32 = order[i]; + if ((includeroot || !g.pkg[a].root) + && g.pkg[a].path[0u64] != 0u8) { + let j: i32 = i + 1; + for (j < n) { + let b: i32 = order[j]; + if ((includeroot || !g.pkg[b].root) + && cstreq(g.pkg[a].path, g.pkg[b].path)) { + cerr("ww: product closure contains multiple packages named "); + cerr(pathstr(g.pkg[a].path)); cerr("\n"); + return -1; + }; + j += 1; + }; + }; + i += 1; + }; + return 0; +}; + // Emit one of pi's own source files into the sep-unit under the // //ww:module-reset primary boundary (so -c emits its decls, imported // ==0). DIRECTORY imports are skipped (provided as `.wwi` ahead); @@ -1357,7 +1640,10 @@ fn sepemitbody(fd: i32, path: *u8, visit: *expctx, searchpath: *u8, // //ww:module-reset. Compiler exports are self-contained for public type // facts; the linker separately retains the reachable archive closure. fn sepcomposeunit(g: *sepgraph, pi: i32, scratch: *u8, - searchpath: *u8, unitf: *u8) i32 = { + unitf: *u8) i32 = { + if (g.pkg[pi].emitcontext < 0 + || g.pkg[pi].emitcontext >= g.ncontext) { return -1; }; + let searchpath: *u8 = g.context[g.pkg[pi].emitcontext].searchpath; let u: i32 = os.open(pathstr(unitf), os.flag.WRONLY | os.flag.CREATE | os.flag.TRUNC, 420i32); // 0o644 if (u < 0) { cerr("ww: cannot open unit\n"); @@ -1607,9 +1893,9 @@ fn copyfileatomic(src: *u8, dst: *u8) i32 = { fn workdirstamptext(istest: i32, emitasm: i32) str = { if (istest != 0) { if (emitasm != 0) { - return "ww workdir fmt 4 mode test asm 1\n"; + return "ww workdir fmt 5 mode test asm 1\n"; }; - return "ww workdir fmt 4 mode test asm 0\n"; + return "ww workdir fmt 5 mode test asm 0\n"; }; if (emitasm != 0) { return "ww workdir fmt 4 mode build asm 1\n"; @@ -1669,6 +1955,45 @@ fn recordproductstatus(path: *u8) i32 = { return os.rename(pathstr(tmpp), pathstr(path)); }; +// Remove every committed unit voucher before a stale-tool pass. Remaining +// artifacts cannot be reused without their matching unit, so a partial pass +// may safely record its new tool identity: successful actions have current +// units and failed/no-longer-requested actions have none. +fn invalidateworkdirunits(scratch: *u8) i32 = { + let fd: i32 = os.open(pathstr(scratch), os.flag.RDONLY, 0i32); + if (fd < 0) { return -1; }; + let buf: []u8 = alloc([], 8192u64)!; + buf.len = 8192; + let rc: i32 = 0; + let r: i64 = os.getdents64(fd, buf.ptr, 8192u64); + for (r > 0i64 && rc == 0) { + let off: u64 = 0u64; + for (off < r: u64) { + let reclen: u64 = (buf[off + 16u64]): u64 + + ((buf[off + 17u64]): u64) * 256u64; + if (reclen == 0u64) { rc = -1; break; }; + let name: *u8 = buf.ptr + off + 19u64; + let ns: str = pathstr(name); + if (strings.hassuffix(ns, ".unit.ww")) { + if (cstrlen(scratch) + 1u64 + cstrlen(name) + 1u64 + > os.PATH_MAX: u64) { + rc = -1; + break; + }; + let path: *u8 = joinpath(scratch, name); + let rr: i32 = os.remove(pathstr(path)); + if (rr != 0 && rr != -2) { rc = -1; break; }; + }; + off += reclen; + }; + if (rc == 0) { r = os.getdents64(fd, buf.ptr, 8192u64); }; + }; + if (r < 0i64) { rc = -1; }; + if (os.close(fd) != 0) { rc = -1; }; + if (rc != 0) { cerr("ww: cannot invalidate stale package units\n"); }; + return rc; +}; + // cerrpath — the "ww: \n" diagnostic shape shared by the // workdir error sites; byte-identical wording to the cstage twin's // fprintf(..., "%s", path) forms. @@ -1735,20 +2060,6 @@ fn buildonesepimpl(selfdir: *u8, src: *u8, entryisdir: i32, }; }; - // searchpath = srcd + ':' + incs + ':' + dotdotlib. - let searchpath: []u8 = alloc([], (os.PATH_MAX: u64) * 3u64)!; - searchpath.len = ((os.PATH_MAX: u64) * 3u64): i32; - { - let off: u64 = cstrinto(searchpath.ptr, 0u64, srcd.ptr); - off = byteinto(searchpath.ptr, off, 58u8); // ':' - if (incs[0u64] != 0u8) { - off = cstrinto(searchpath.ptr, off, incs); - off = byteinto(searchpath.ptr, off, 58u8); - }; - off = cstrinto(searchpath.ptr, off, dotdotlib.ptr); - cstrseal(searchpath.ptr, off); - }; - let stem: []u8 = alloc([], (os.PATH_MAX: u64))!; stem.len = os.PATH_MAX; if (entryisdir != 0) { @@ -1820,6 +2131,7 @@ fn buildonesepimpl(selfdir: *u8, src: *u8, entryisdir: i32, if (!fileequal(toola, a6)) { staleall = true; }; }; }; + if (staleall && invalidateworkdirunits(scratch) != 0) { return 1; }; }; let libwwrt: []u8 = alloc([], (os.PATH_MAX: u64))!; @@ -1832,15 +2144,33 @@ fn buildonesepimpl(selfdir: *u8, src: *u8, entryisdir: i32, let pkgslot: []seppkg = alloc([], SEP_MAXPKG: u64)!; pkgslot.len = SEP_MAXPKG; - let g: *sepgraph = alloc(sepgraph{pkg = pkgslot, n = 0})!; + let contextslot: []sepcontext = alloc([], SEP_MAXCONTEXT: u64)!; + contextslot.len = SEP_MAXCONTEXT; + let g: *sepgraph = alloc(sepgraph{ + pkg = pkgslot, + n = 0, + context = contextslot, + ncontext = 0, + supportcontext = -1, + })!; if (graphout != nil) { *graphout = g; }; let rootpath: *u8 = "\0".ptr; if (packageonly != 0 && rootidentity != nil) { rootpath = rootidentity; }; let producti: i32 = 0; for (producti < nproducts) { - products[producti].root = sepfindoraddvariant(g, rootpath, src, + let entry: *u8 = src; + if (products[producti].dir != nil) { + entry = products[producti].dir; + }; + let contextroot: *u8 = entry; + if (entryisdir == 0) { contextroot = srcd.ptr; }; + products[producti].context = sepcontextfor(g, contextroot, + incs, dotdotlib.ptr); + if (products[producti].context < 0) { return 1; }; + products[producti].root = sepfindoraddvariant(g, rootpath, entry, entryisdir, products[producti].variant, - products[producti].testpackage, true); + products[producti].testpackage, + SEP_ROLE_NORMAL, products[producti].artifact, true); if (products[producti].root < 0) { return 1; }; producti += 1; }; @@ -1855,40 +2185,60 @@ fn buildonesepimpl(selfdir: *u8, src: *u8, entryisdir: i32, let tp: *u8 = locateimport(dotdotlib.ptr, "test".ptr, "test".len: u64, &td); if (tp != nil) { - let rootissupport: bool = entryisdir != 0 - && os.samefile(pathstr(tp), pathstr(src)); + g.supportcontext = sepcontextfor(g, dotdotlib.ptr, nil, + dotdotlib.ptr); + if (g.supportcontext < 0) { return 1; }; let collision: bool = false; producti = 0; - for (!rootissupport && producti < nproducts) { + for (producti < nproducts) { + let root: i32 = products[producti].root; + let rootissupport: bool = entryisdir != 0 + && os.samefile(pathstr(tp), + pathstr(g.pkg[root].entry)); let name: *u8 = products[producti].testpackage; - if (name != nil && (cstreqlit(name, "test") + if (!rootissupport && name != nil + && (cstreqlit(name, "test") || cstreqlit(name, "test_test"))) { collision = true; }; producti += 1; }; - if (!rootissupport && !collision) { + producti = 0; + for (producti < nproducts && !collision) { let ud: i32 = 0; - let up: *u8 = locateimport(searchpath.ptr, "test".ptr, + let up: *u8 = locateimport( + g.context[products[producti].context].searchpath, + "test".ptr, "test".len: u64, &ud); if (up != nil && !os.samefile(pathstr(tp), pathstr(up))) { collision = true; }; + producti += 1; }; if (collision) { testsupportmodule = SEP_TEST_SUPPORT_MODULE; }; producti = 0; for (producti < nproducts) { let root: i32 = products[producti].root; + let rootissupport: bool = entryisdir != 0 + && os.samefile(pathstr(tp), + pathstr(g.pkg[root].entry)); // A same-test build of the runtime package already owns run // and its source imports. An external test still needs the // colocated production node, also its support dependency. if (rootissupport + && syntax.streq(testsupportmodule, "test") && products[producti].variant != SEP_VARIANT_EXTERNAL) { producti += 1; continue; }; - let ti: i32 = sepfindoradd(g, - testsupportmodule.ptr, tp, td); + let ti: i32 = -1; + if (syntax.streq(testsupportmodule, + SEP_TEST_SUPPORT_MODULE)) { + ti = sepfindoraddrole(g, testsupportmodule.ptr, tp, + td, SEP_ROLE_TEST_SUPPORT, nil); + } else { + ti = sepfindoradd(g, testsupportmodule.ptr, tp, td); + }; if (ti < 0) { return 1; }; g.pkg[ti].testsupport = true; let seen: bool = false; @@ -1910,7 +2260,7 @@ fn buildonesepimpl(selfdir: *u8, src: *u8, entryisdir: i32, producti = 0; for (producti < nproducts) { let root: i32 = products[producti].root; - if (seploadpkg(g, root, searchpath.ptr) < 0) { + if (seploadpkg(g, root, products[producti].context) < 0) { g.pkg[root].failed = true; producti += 1; continue; @@ -1924,6 +2274,7 @@ fn buildonesepimpl(selfdir: *u8, src: *u8, entryisdir: i32, }; producti += 1; }; + if (sepvalidateartifactpaths(g, scratch) < 0) { return 1; }; let rootpackage: bool = packageonly != 0; if (rootpackage && !g.pkg[products[0].root].failed && cstreqlit(g.pkg[products[0].root].name, "main")) { @@ -1947,7 +2298,9 @@ fn buildonesepimpl(selfdir: *u8, src: *u8, entryisdir: i32, for (ci < g.n) { g.pkg[ci].color = 0; ci += 1; }; let ignored: i32 = 0; if (septopovisit(g, root, order, - &ignored, stack, 0) < 0) { + &ignored, stack, 0) < 0 + || sepvalidatemoduleclosure(g, order, ignored, + rootpackage) < 0) { g.pkg[root].failed = true; }; }; @@ -2016,7 +2369,7 @@ fn buildonesepimpl(selfdir: *u8, src: *u8, entryisdir: i32, }; let needsexport: bool = !g.pkg[pi].root || rootpackage; let needsarchive: bool = !g.pkg[pi].root || rootpackage; - if (sepcomposeunit(g, pi, scratch, searchpath.ptr, cu) < 0) { + if (sepcomposeunit(g, pi, scratch, cu) < 0) { g.pkg[pi].failed = true; anyfailed = true; oi += 1; @@ -2187,10 +2540,10 @@ fn buildonesepimpl(selfdir: *u8, src: *u8, entryisdir: i32, }; oi += 1; }; - // Tool identity commits only after every package artifact it vouches - // for is itself committed; a killed pass leaves the old identity and - // forces a full recompile, never a false reuse. - if (warm && !anyfailed) { + // A stale pass removed every old unit voucher before compiling. Current + // successful units remain safe to vouch for when a sibling root fails; + // a killed pass retains the old identity and invalidates again next time. + if (warm) { if (!fileequal(toolc, c6)) { if (copyfileatomic(c6, toolc) != 0) { cerrpath("ww: cannot record ", toolc, "\n"); @@ -2256,8 +2609,8 @@ fn buildonesepimpl(selfdir: *u8, src: *u8, entryisdir: i32, let nlink: i32 = 0; if (septopovisit(g, root, linkorder, &nlink, linkstack, 0) < 0) { return 1; }; - // argv: 3 fixed + closure + libwwrt + flags + nil. - let total: i32 = 3 + nlink + 1 + 2 * nldirs + 2 * nllibs + 1; + // argv: 3 fixed + closure + libwwrt + joined flags + nil. + let total: i32 = 3 + nlink + 1 + nldirs + nllibs + 1; let largv: []*u8 = alloc([], total: u64)!; largv.len = total; largv[0] = "w6l\0".ptr; @@ -2270,6 +2623,7 @@ fn buildonesepimpl(selfdir: *u8, src: *u8, entryisdir: i32, if (g.pkg[root].variant == SEP_VARIANT_SAME_TEST && pi != root && 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))) { li -= 1; @@ -2284,16 +2638,26 @@ fn buildonesepimpl(selfdir: *u8, src: *u8, entryisdir: i32, largv[pos] = libwwrt.ptr; pos += 1; let k: i32 = 0; for (k < nldirs) { - largv[pos] = "-L\0".ptr; - largv[pos + 1] = ldirs[k]; - pos += 2; + let n: u64 = cstrlen(ldirs[k]); + let flag: []u8 = alloc([], n + 3u64)!; + flag.len = (n + 3u64): i32; + flag[0] = 45u8; flag[1] = 76u8; + bytecpy(flag.ptr + 2u64, ldirs[k], n); + flag[n + 2u64] = 0u8; + largv[pos] = flag.ptr; + pos += 1; k += 1; }; k = 0; for (k < nllibs) { - largv[pos] = "-l\0".ptr; - largv[pos + 1] = llibs[k]; - pos += 2; + let n: u64 = cstrlen(llibs[k]); + let flag: []u8 = alloc([], n + 3u64)!; + flag.len = (n + 3u64): i32; + flag[0] = 45u8; flag[1] = 108u8; + bytecpy(flag.ptr + 2u64, llibs[k], n); + flag[n + 2u64] = 0u8; + largv[pos] = flag.ptr; + pos += 1; k += 1; }; largv[pos] = nil; @@ -2342,9 +2706,11 @@ fn buildonesep(selfdir: *u8, src: *u8, entryisdir: i32, let scratch: *u8 = nil; let g: *sepgraph = nil; let product: sepproduct; + product.dir = src; product.out = out; product.testpackage = testpackage; product.status = nil; + product.artifact = nil; product.variant = rootvariant; product.root = -1; let r: i32 = buildonesepimpl(selfdir, src, entryisdir, rootidentity, @@ -2376,8 +2742,8 @@ fn buildonesep(selfdir: *u8, src: *u8, entryisdir: i32, return r; }; -// Build every selected test root for one directory inside one command-owned -// package universe. The first output owns the shared cold sepwork tree. +// Build every selected directory/variant root inside one command-owned package +// universe. The first output owns the shared cold sepwork tree. fn buildpackagetests(selfdir: *u8, src: *u8, incs: *u8, workdir: *u8, products: *sepproduct, nproducts: i32) i32 = { let scratch: *u8 = nil; @@ -2396,6 +2762,23 @@ fn cstrendswithlit(p: *u8, lit: str) bool = { return strings.hassuffix(pathstr(p), lit); }; +fn productartifact(index: i32, variant: i32) *u8 = { + let buf: []u8 = alloc([], 64u64)!; + buf.len = 64; + let off: u64 = strinto(buf.ptr, 0u64, "__ww-test-"); + 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 = byteinto(buf.ptr, off, '-': u8); + if (variant == SEP_VARIANT_SAME_TEST) { + off = strinto(buf.ptr, off, "same"); + } else { + off = strinto(buf.ptr, off, "external"); + }; + cstrseal(buf.ptr, off); + return buf.ptr; +}; + fn basenameoff(p: *u8, plen: u64) u64 = { let start: u64 = 0u64; let i: u64 = 0u64; @@ -3030,14 +3413,15 @@ fn dotest(selfdir: *u8, argv: **u8, argc: i32, start: i32) i32 = { packageopts = true; afterdash = true; i += 1; continue; }; if (cstreqlit(p, "--ww-package-test")) { - if (i + 4 >= argc || products.len >= SEP_MAXPRODUCT) { - cerr("ww test: --ww-package-test needs kind, package, output, and status\n"); + if (i + 5 >= argc || products.len >= SEP_MAXPRODUCT) { + cerr("ww test: --ww-package-test needs kind, package, directory, output, and status\n"); return 2; }; let kind: *u8 = argv[i + 1]; let name: *u8 = argv[i + 2]; - let output: *u8 = argv[i + 3]; - let status: *u8 = argv[i + 4]; + let dir: *u8 = argv[i + 3]; + let output: *u8 = argv[i + 4]; + let status: *u8 = argv[i + 5]; let pn: u64 = cstrlen(name); let variant: i32 = SEP_VARIANT_EXTERNAL; if (cstreqlit(kind, "same")) { @@ -3046,7 +3430,8 @@ fn dotest(selfdir: *u8, argv: **u8, argc: i32, start: i32) i32 = { if ((!cstreqlit(kind, "same") && !cstreqlit(kind, "external")) || pn == 0u64 || pn >= 256u64 - || output[0u64] == 0u8 || status[0u64] == 0u8 + || dir[0u64] == 0u8 || output[0u64] == 0u8 + || status[0u64] == 0u8 || (cstreqlit(kind, "external") && (pn <= 5u64 || !cstrendswithlit(name, @@ -3054,26 +3439,16 @@ fn dotest(selfdir: *u8, argv: **u8, argc: i32, start: i32) i32 = { cerr("ww test: invalid --ww-package-test variant\n"); return 2; }; - let duplicate: bool = false; - let producti: i32 = 0; - for (producti < products.len) { - if (products[producti].variant == variant) { - duplicate = true; - }; - producti += 1; - }; - if (duplicate) { - cerr("ww test: duplicate --ww-package-test variant\n"); - return 2; - }; let product: sepproduct; + product.dir = dir; product.out = output; product.testpackage = name; product.status = status; + product.artifact = nil; product.variant = variant; product.root = -1; append(products, product); - i += 5; + i += 6; continue; }; if (p[1u64] == 73u8) { // '-I' @@ -3159,13 +3534,30 @@ fn dotest(selfdir: *u8, argv: **u8, argc: i32, start: i32) i32 = { for (producti < products.len) { let product: sepproduct = products[producti]; let j: i32 = producti; - for (j > 0 && products[j - 1].variant > product.variant) { + for (j > 0 && (strings.compare(pathstr(products[j - 1].dir), + pathstr(product.dir)) > 0 + || (strings.compare(pathstr(products[j - 1].dir), + pathstr(product.dir)) == 0 + && products[j - 1].variant > product.variant))) { products[j] = products[j - 1]; j -= 1; }; products[j] = product; producti += 1; }; + producti = 0; + for (producti < products.len) { + if (producti > 0 + && cstreq(products[producti - 1].dir, products[producti].dir) + && products[producti - 1].variant + == products[producti].variant) { + cerr("ww test: duplicate --ww-package-test variant for directory\n"); + return 2; + }; + products[producti].artifact = productartifact(producti, + products[producti].variant); + producti += 1; + }; if (products.len != 0 && packageopts) { cerr("ww test: package-test variant rejects package options\n"); return 2; @@ -3205,8 +3597,8 @@ fn dotest(selfdir: *u8, argv: **u8, argc: i32, start: i32) i32 = { cerr("ww test: pattern needs a single test file\n"); return 2; }; - // -w forwards: the coordinator keys one persistent driver - // workdir per directory plan under the given root. + // -w forwards: the coordinator keys one persistent driver workdir + // for the complete selected test request. return execpackagetests(selfdir, argv, argc, start, targetindex, nil, false); };