diff --git a/cmd/ww/main.c b/cmd/ww/main.c index b1fa0b33..a81b17d7 100644 --- a/cmd/ww/main.c +++ b/cmd/ww/main.c @@ -324,10 +324,10 @@ source_has_test_decl(const char *path) #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_ROLE_TEST_SUPPORT 1 +#define SEP_ROLE_GENERATED_MAIN 2 #define SEP_TEST_SUPPORT_MODULE "__wwtest" +#define SEP_IMPORT_PATH_MAX 256 #define SEP_MAXPRODUCT 256 #define SEP_MAXCONTEXT (SEP_MAXPRODUCT + 1) @@ -510,18 +510,19 @@ enumerate_dir_ww(const char *dirpath, int variant, const char *test_package, #define SEP_MAXPKG 256 struct seppkg { - char path[256]; /* canonical dotted package identity */ + char path[512]; /* compiler/import identity; derived from import_base */ + char import_base[SEP_IMPORT_PATH_MAX]; /* canonical directory import 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 */ + char artifact[512]; /* stable non-importable variant 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; /* requested package action (possibly a test variant) */ + int role; /* normal, reserved test support, or generated main */ + int root; /* requested usage; never package-action identity */ 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 */ @@ -547,6 +548,7 @@ struct sepgraph { struct sepcontext context[SEP_MAXCONTEXT]; int ncontext; int support_context; + int identity_failed; /* command-global canonical identity collision */ }; struct sepproduct { @@ -570,14 +572,116 @@ struct seplinkflags { int nlibs; }; +static int +sep_directory_variant(int variant) +{ + return variant == SEP_VARIANT_PRODUCTION + || variant == SEP_VARIANT_SAME_TEST + || variant == SEP_VARIANT_EXTERNAL; +} + +static int +sep_variant_path(int variant, const char *base, char *out, size_t outsz) +{ + int n; + if (variant == SEP_VARIANT_EXTERNAL) + n = snprintf(out, outsz, "%s_test", base); + else + n = snprintf(out, outsz, "%s", base); + return n >= 0 && (size_t)n < outsz ? 0 : -1; +} + +static void +sep_diag_path_locations(const char *path, const char *a, const char *b) +{ + if (strcmp(a, b) > 0) { const char *t = a; a = b; b = t; } + fprintf(stderr, "ww: package %s resolves to directories %s and %s\n", + path, a, b); +} + +static void +sep_diag_directory_identities(const char *entry, const char *a, const char *b) +{ + if (strcmp(a, b) > 0) { const char *t = a; a = b; b = t; } + fprintf(stderr, "ww: package directory %s has import identities %s and %s\n", + entry, a, b); +} + +/* Bind the canonical ordinary import identity of one provisional directory + * action. The action's compiler path is derived from that base and its semantic + * variant; neither requested-root state nor artifact naming participates. */ +static int +sep_bind_import_base(struct sepgraph *g, int pi, const char *base) +{ + struct seppkg *p = &g->pkg[pi]; + if (base == NULL || base[0] == '\0' + || strlen(base) >= SEP_IMPORT_PATH_MAX) { + fprintf(stderr, "ww: package path is too long (limit %d bytes)\n", + SEP_IMPORT_PATH_MAX - 1); + return -1; + } + if (p->import_base[0] != '\0') { + if (strcmp(p->import_base, base) == 0) return 0; + g->identity_failed = 1; + sep_diag_directory_identities(p->entry, p->import_base, base); + return -1; + } + char path[sizeof p->path]; + if (sep_variant_path(p->variant, base, path, sizeof path) < 0) { + fprintf(stderr, "ww: package variant path is too long\n"); + return -1; + } + for (int i = 0; i < g->n; i++) { + if (i == pi || !g->pkg[i].is_dir || g->pkg[i].generated_main) + continue; + int same_location = strcmp(g->pkg[i].canon, p->canon) == 0; + int support_alias = p->role == SEP_ROLE_TEST_SUPPORT + || g->pkg[i].role == SEP_ROLE_TEST_SUPPORT; + if (!support_alias && !same_location + && g->pkg[i].import_base[0] != '\0' + && strcmp(g->pkg[i].import_base, base) == 0) { + g->identity_failed = 1; + sep_diag_path_locations(base, g->pkg[i].entry, p->entry); + return -1; + } + if (same_location && !support_alias + && g->pkg[i].import_base[0] != '\0' + && strcmp(g->pkg[i].import_base, base) != 0) { + g->identity_failed = 1; + sep_diag_directory_identities(p->entry, + g->pkg[i].import_base, base); + return -1; + } + if (g->pkg[i].path[0] != '\0' + && strcmp(g->pkg[i].path, path) == 0) { + if (!same_location) { + g->identity_failed = 1; + sep_diag_path_locations(path, g->pkg[i].entry, p->entry); + return -1; + } + if (support_alias + || g->pkg[i].variant == p->variant) { + g->identity_failed = 1; + fprintf(stderr, + "ww: package action identity collision for %s in %s\n", + path, p->entry); + return -1; + } + } + } + snprintf(p->import_base, sizeof p->import_base, "%s", base); + snprintf(p->path, sizeof p->path, "%s", path); + return 0; +} + 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 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", - sizeof g->pkg[0].path - 1); + if (path != NULL && strlen(path) >= SEP_IMPORT_PATH_MAX) { + fprintf(stderr, "ww: package path is too long (limit %d bytes)\n", + SEP_IMPORT_PATH_MAX - 1); return -1; } char *canon = realpath(entry, NULL); @@ -590,104 +694,83 @@ sep_find_or_add_variant(struct sepgraph *g, const char *path, free(canon); return -1; } + const char *base = path ? path : ""; + char incoming_path[sizeof g->pkg[0].path]; + incoming_path[0] = '\0'; + if (is_dir && base[0] != '\0' + && sep_variant_path(variant, base, incoming_path, + sizeof incoming_path) < 0) { + fprintf(stderr, "ww: package variant path is too long\n"); + free(canon); + return -1; + } for (int i = 0; i < g->n; i++) { - int same_location = strcmp(g->pkg[i].canon, canon) == 0; - if (root || g->pkg[i].root) { - 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: incompatible package-test roots %s\n", entry); + struct seppkg *q = &g->pkg[i]; + int same_location = strcmp(q->canon, canon) == 0; + if (is_dir && q->is_dir && !q->generated_main) { + int support_alias = role == SEP_ROLE_TEST_SUPPORT + || q->role == SEP_ROLE_TEST_SUPPORT; + if (!support_alias && !same_location && base[0] != '\0' + && q->import_base[0] != '\0' + && strcmp(base, q->import_base) == 0) { + g->identity_failed = 1; + sep_diag_path_locations(base, q->entry, 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) { + if (incoming_path[0] != '\0' && q->path[0] != '\0' + && strcmp(incoming_path, q->path) == 0 + && !same_location) { + g->identity_failed = 1; + sep_diag_path_locations(incoming_path, q->entry, entry); + free(canon); + return -1; + } + if (same_location && q->variant == variant && q->role == role) { + if (variant != SEP_VARIANT_PRODUCTION + && strcmp(q->test_package, + test_package ? test_package : "") != 0) { + fprintf(stderr, + "ww: incompatible package-test roots %s\n", entry); + free(canon); + return -1; + } + if (base[0] != '\0' + && sep_bind_import_base(g, i, base) < 0) { + free(canon); + return -1; + } + q->root = q->root || root; 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) { + if (same_location) { + if (support_alias) continue; + if (q->import_base[0] != '\0' && base[0] != '\0' + && strcmp(q->import_base, base) != 0) { + g->identity_failed = 1; + sep_diag_directory_identities(entry, + q->import_base, base); + free(canon); + return -1; + } + if (sep_directory_variant(q->variant) + && sep_directory_variant(variant)) + continue; fprintf(stderr, - "ww: package %s resolves to more than one location\n", - path[0] ? path : "(root)"); + "ww: package directory %s has incompatible variants\n", + entry); free(canon); return -1; } + continue; + } + if (same_location && strcmp(q->path, base) == 0 + && q->variant == variant && q->role == role) { + q->root = q->root || root; free(canon); return i; } - if (same_location) { - /* 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", - entry, g->pkg[i].path[0] ? g->pkg[i].path : "(root)", - path[0] ? path : "(root)"); - free(canon); - return -1; - } } if (g->n >= SEP_MAXPKG) { fprintf(stderr, "ww: too many packages (limit %d)\n", @@ -695,39 +778,43 @@ sep_find_or_add_variant(struct sepgraph *g, const char *path, free(canon); return -1; } - struct seppkg *p = &g->pkg[g->n]; - snprintf(p->path, sizeof p->path, "%s", path); + int ni = g->n++; + struct seppkg *p = &g->pkg[ni]; + memset(p, 0, sizeof *p); 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->link_entry = 0; - p->generated_main = 0; - p->failed = 0; - p->test_support = 0; - p->loaded = 0; - p->export_changed = 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) snprintf(p->test_package, sizeof p->test_package, "%s", test_package); - p->sources = NULL; - p->nsources = 0; - p->ndeps = 0; - p->color = 0; - return g->n++; + if (is_dir) { + const char *inherited = base; + if (inherited[0] == '\0') + for (int i = 0; i < ni; i++) + if (g->pkg[i].is_dir && !g->pkg[i].generated_main + && g->pkg[i].role != SEP_ROLE_TEST_SUPPORT + && role != SEP_ROLE_TEST_SUPPORT + && strcmp(g->pkg[i].canon, p->canon) == 0 + && g->pkg[i].import_base[0] != '\0') { + inherited = g->pkg[i].import_base; + break; + } + if (inherited[0] != '\0' + && sep_bind_import_base(g, ni, inherited) < 0) { + g->n--; + return -1; + } + } else { + snprintf(p->path, sizeof p->path, "%s", base); + if (artifact != NULL) + snprintf(p->artifact, sizeof p->artifact, "%s", artifact); + } + return ni; } static int @@ -823,6 +910,7 @@ static int sep_validate_artifact_paths(const struct sepgraph *g, const char *scratch) { for (int i = 0; i < g->n; i++) { + if (g->pkg[i].failed || !g->pkg[i].loaded) continue; const char *base = g->pkg[i].artifact[0] != '\0' ? g->pkg[i].artifact : (g->pkg[i].path[0] != '\0' ? g->pkg[i].path : "__root"); @@ -832,6 +920,19 @@ sep_validate_artifact_paths(const struct sepgraph *g, const char *scratch) fprintf(stderr, "ww: package artifact path is too long\n"); return -1; } + for (int j = i + 1; j < g->n; j++) { + if (g->pkg[j].failed || !g->pkg[j].loaded) continue; + const char *other = g->pkg[j].artifact[0] != '\0' + ? g->pkg[j].artifact + : (g->pkg[j].path[0] != '\0' + ? g->pkg[j].path : "__root"); + if (strcmp(base, other) == 0) { + fprintf(stderr, + "ww: package actions share artifact identity %s\n", + base); + return -1; + } + } } return 0; } @@ -1082,15 +1183,6 @@ sep_scan_file(struct sepgraph *g, int pi, const char *file, 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; @@ -1102,20 +1194,10 @@ sep_scan_file(struct sepgraph *g, int pi, const char *file, rc = -1; break; } - 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); - } + /* The external package's self-production import is the ordinary + * canonical production action for this directory. Discovery role and + * the external product's artifact name never create another action. */ + int 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++) @@ -1146,50 +1228,75 @@ 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].variant != g->pkg[b].variant) + return g->pkg[a].variant - g->pkg[b].variant; 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); + return strcmp(g->pkg[a].canon, g->pkg[b].canon); } -/* 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. */ +/* Add the compiler-owned test main as a real package action. Its identity is a + * pure function of the selected variant (and its one command-global support + * edge), never a product ordinal. Equivalent products therefore reuse it. */ 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; - } + (void)ordinal; 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) { + const char *kind = "production"; + if (g->pkg[variant].variant == SEP_VARIANT_SAME_TEST) + kind = "internal"; + else if (g->pkg[variant].variant == SEP_VARIANT_EXTERNAL) + kind = "external"; + char path[sizeof g->pkg[0].path]; + char artifact[sizeof g->pkg[0].artifact]; + char canon[sizeof g->pkg[0].canon]; + char entry[sizeof g->pkg[0].entry]; + snprintf(entry, sizeof entry, "%s", g->pkg[variant].entry); + const char *variant_artifact = g->pkg[variant].artifact[0] + ? g->pkg[variant].artifact : g->pkg[variant].path; + int pn = snprintf(path, sizeof path, "__wwtestmain.%s.%s.main", + g->pkg[variant].path, kind); + int an = snprintf(artifact, sizeof artifact, "%s-main", variant_artifact); + int cn = snprintf(canon, sizeof canon, "%s#%s-test-main", + g->pkg[variant].canon, kind); + if (pn < 0 || (size_t)pn >= sizeof path + || an < 0 || (size_t)an >= sizeof artifact + || cn < 0 || (size_t)cn >= sizeof canon) { fprintf(stderr, "ww: generated test-main identity is too long\n"); return -1; } for (int i = 0; i < g->n; i++) { - if (strcmp(g->pkg[i].path, p->path) != 0) continue; + if (strcmp(g->pkg[i].path, path) != 0) continue; + if (g->pkg[i].generated_main) { + int wants_support = support >= 0 && support != variant; + int has_variant = 0, has_support = 0; + for (int k = 0; k < g->pkg[i].ndeps; k++) { + if (g->pkg[i].deps[k] == variant) has_variant = 1; + if (wants_support && g->pkg[i].deps[k] == support) + has_support = 1; + } + if (has_variant && has_support == wants_support + && g->pkg[i].ndeps == 1 + wants_support) + return i; + } fprintf(stderr, "ww: generated test-main package identity collides with source import %s\n", - p->path); + path); return -1; } - snprintf(p->entry, sizeof p->entry, "%s", variantentry); + if (g->n >= SEP_MAXPKG) { + fprintf(stderr, "ww: too many packages (limit %d)\n", SEP_MAXPKG); + return -1; + } + struct seppkg *p = &g->pkg[g->n]; + memset(p, 0, sizeof *p); + snprintf(p->path, sizeof p->path, "%s", path); + snprintf(p->artifact, sizeof p->artifact, "%s", artifact); + snprintf(p->canon, sizeof p->canon, "%s", canon); + snprintf(p->entry, sizeof p->entry, "%s", entry); snprintf(p->name, sizeof p->name, "main"); p->variant = SEP_VARIANT_TEST_MAIN; p->role = SEP_ROLE_GENERATED_MAIN; @@ -1303,15 +1410,6 @@ sep_load_pkg(struct sepgraph *g, int pi, int context) g->pkg[pi].failed = 1; return -1; } - /* 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); - } for (int i = 1; i < g->pkg[pi].ndeps; i++) { int v = g->pkg[pi].deps[i]; int j = i; @@ -1332,6 +1430,85 @@ sep_load_pkg(struct sepgraph *g, int pi, int context) return 0; } +/* Finalize every provisional directory root after source discovery but before + * generated-main construction or compilation. A source import binding wins; + * otherwise a manifest-free literal root uses its validated declared package + * name (external p_test roots bind the ordinary base p). */ +static int +sep_finalize_directory_identities(struct sepgraph *g) +{ + for (int pi = 0; pi < g->n; pi++) { + struct seppkg *p = &g->pkg[pi]; + if (!p->is_dir || p->generated_main || p->failed || !p->loaded + || p->import_base[0] != '\0') + continue; + const char *base = NULL; + for (int i = 0; i < g->n; i++) { + if (i == pi || !g->pkg[i].is_dir || g->pkg[i].generated_main + || g->pkg[i].role == SEP_ROLE_TEST_SUPPORT + || p->role == SEP_ROLE_TEST_SUPPORT + || strcmp(g->pkg[i].canon, p->canon) != 0 + || g->pkg[i].import_base[0] == '\0') + continue; + base = g->pkg[i].import_base; + break; + } + char fallback[SEP_IMPORT_PATH_MAX]; + if (base == NULL) { + if (p->name[0] == '\0') { + fprintf(stderr, + "ww: package directory %s has no canonical import identity\n", + p->entry); + return -1; + } + if (p->variant == SEP_VARIANT_EXTERNAL) { + size_t n = strlen(p->name); + if (n <= 5 || strcmp(p->name + n - 5, "_test") != 0) { + fprintf(stderr, + "ww: package-test selector does not name an external package\n"); + return -1; + } + memcpy(fallback, p->name, n - 5); + fallback[n - 5] = '\0'; + } else { + snprintf(fallback, sizeof fallback, "%s", p->name); + } + base = fallback; + } + if (sep_bind_import_base(g, pi, base) < 0) return -1; + } + for (int pi = 0; pi < g->n; pi++) { + struct seppkg *p = &g->pkg[pi]; + if (!p->is_dir || p->generated_main || p->failed || !p->loaded) + continue; + const char *dot = strrchr(p->path, '.'); + const char *leaf = dot ? dot + 1 : p->path; + int support_alias = p->role == SEP_ROLE_TEST_SUPPORT + && strcmp(p->path, SEP_TEST_SUPPORT_MODULE) == 0 + && strcmp(p->name, "test") == 0; + if (!support_alias && strcmp(p->name, leaf) != 0) { + fprintf(stderr, "ww: package %s does not match import path %s\n", + p->name, p->path); + return -1; + } + p->artifact[0] = '\0'; + int n = 0; + char action_path[sizeof p->path]; + snprintf(action_path, sizeof action_path, "%s", p->path); + if (p->variant == SEP_VARIANT_SAME_TEST) + n = snprintf(p->artifact, sizeof p->artifact, + "%s-internal-test", action_path); + else if (p->variant == SEP_VARIANT_EXTERNAL) + n = snprintf(p->artifact, sizeof p->artifact, + "%s-external-test", action_path); + if (n < 0 || (size_t)n >= sizeof p->artifact) { + fprintf(stderr, "ww: package variant artifact identity is too long\n"); + return -1; + } + } + return 0; +} + /* DFS post-order over the dep DAG → reverse-topo (deps before importer), * cite Hare gather (deps.ha:123). Tri-color: a gray back-edge is a loud * dep-cycle reject naming the chain (Hare deps.ha:243); `stack[0..depth)` @@ -1364,10 +1541,9 @@ 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. */ +/* Internal test variants replace their colocated production action in the + * corresponding test link closure. Canonical package identity has already + * made every remaining compiler qualifier globally unambiguous. */ static int sep_internal_replaces_production(const struct sepgraph *g, int a, int b) { @@ -1822,8 +1998,10 @@ build_one_sep_impl(const char *src, int entry_is_dir, products[i].context = sep_context_for(g, contextroot, extra_includes, toolsrcdir); if (products[i].context < 0) return 1; + const char *selector = products[i].variant == SEP_VARIANT_PRODUCTION + ? NULL : products[i].test_package; products[i].root = sep_find_or_add_variant(g, rootpath, entry, - entry_is_dir, products[i].variant, products[i].test_package, + entry_is_dir, products[i].variant, selector, SEP_ROLE_NORMAL, products[i].artifact, 1); if (products[i].root < 0) return 1; products[i].variant_root = products[i].root; @@ -1919,10 +2097,8 @@ build_one_sep_impl(const char *src, int entry_is_dir, g->pkg[root].failed = 1; continue; } - if (products[i].variant != SEP_VARIANT_PRODUCTION - && (products[i].test_package == NULL - || strcmp(g->pkg[root].name, - products[i].test_package) != 0)) { + if (products[i].test_package != NULL + && strcmp(g->pkg[root].name, products[i].test_package) != 0) { fprintf(stderr, "ww: package-test selector does not match loaded package\n"); g->pkg[root].failed = 1; @@ -1935,12 +2111,22 @@ build_one_sep_impl(const char *src, int entry_is_dir, if (support >= 0 && support != variant && sep_load_pkg(g, support, products[i].context) < 0) g->pkg[variant].failed = 1; + } + } + if (g->identity_failed) return 1; + if (sep_finalize_directory_identities(g) < 0) return 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 (g->pkg[variant].failed + || (support >= 0 && g->pkg[support].failed)) { + products[i].root = variant; + continue; + } 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; } } @@ -2058,7 +2244,8 @@ build_one_sep_impl(const char *src, int entry_is_dir, cargv[cpos++] = "--test-support-module"; cargv[cpos++] = (char *)test_support_module; } else { - if (is_test && g->pkg[pi].root) + if (g->pkg[pi].variant == SEP_VARIANT_SAME_TEST + || g->pkg[pi].variant == SEP_VARIANT_EXTERNAL) cargv[cpos++] = "--test-package"; if (g->pkg[pi].link_entry) cargv[cpos++] = "--entry"; @@ -2297,7 +2484,7 @@ build_one_sep(const char *src, int entry_is_dir, const char *root_identity, .root = -1, .variant_root = -1, }; - if (!package_only) + if (!package_only && !entry_is_dir) 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, @@ -2714,9 +2901,13 @@ do_test(int argc, char **argv) const char *output = argv[++i]; const char *status = argv[++i]; size_t pn = strlen(name); - int variant = strcmp(kind, "same") == 0 - ? SEP_VARIANT_SAME_TEST : SEP_VARIANT_EXTERNAL; - if ((strcmp(kind, "same") != 0 + int variant = SEP_VARIANT_EXTERNAL; + if (strcmp(kind, "production") == 0) + variant = SEP_VARIANT_PRODUCTION; + else if (strcmp(kind, "same") == 0) + variant = SEP_VARIANT_SAME_TEST; + if ((strcmp(kind, "production") != 0 + && strcmp(kind, "same") != 0 && strcmp(kind, "external") != 0) || pn == 0 || pn >= sizeof ((struct seppkg *)0)->name || dir[0] == '\0' || output[0] == '\0' @@ -2806,11 +2997,10 @@ do_test(int argc, char **argv) "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"); - } + /* Artifact identity is derived from canonical package identity after + * discovery; product position is deliberately not an action key. */ + products[i].artifact[0] = '\0'; + } if (nproducts != 0 && packageopts) { fprintf(stderr, "ww test: package-test variant rejects package options\n"); diff --git a/internal/wwpackage/package.ww b/internal/wwpackage/package.ww index b02dcbde..68c83ab2 100644 --- a/internal/wwpackage/package.ww +++ b/internal/wwpackage/package.ww @@ -25,6 +25,7 @@ type pkggroup = struct { dir: str, pkg: str, external: bool, + production: bool, plan: i32, root: str, bin: str, @@ -608,8 +609,10 @@ fn pkgstartbuild(p: *pkgplan, groups: []pkggroup, builder: str, includes: []str, for (i < p.end) { let g: *pkggroup = &groups[i]; append(ba, "--ww-package-test"); - if (g.external) { append(ba, "external"); } + if (g.production) { append(ba, "production"); } + else { if (g.external) { append(ba, "external"); } else { append(ba, "same"); }; + }; append(ba, g.pkg); append(ba, g.dir); append(ba, g.bin); @@ -953,6 +956,7 @@ export fn packagecommand(args: []str) int = { g.pkg = srcs[j].pkg; g.external = f.prodpkg.len != 0 && strings.compare(f.prodpkg, g.pkg) != 0; + g.production = false; append(groups, g); }; }; @@ -964,6 +968,7 @@ export fn packagecommand(args: []str) int = { g.pkg = f.prodpkg; if (g.pkg.len == 0) { g.pkg = srcs[f.start].pkg; }; g.external = false; + g.production = true; append(groups, g); }; i += 1; diff --git a/selfhost/cmd/ww/main.ww b/selfhost/cmd/ww/main.ww index 2270a0a3..d4e1f117 100644 --- a/selfhost/cmd/ww/main.ww +++ b/selfhost/cmd/ww/main.ww @@ -449,9 +449,8 @@ def SEP_VARIANT_SAME_TEST: i32 = 1; def SEP_VARIANT_EXTERNAL: i32 = 2; def SEP_VARIANT_TEST_MAIN: i32 = 3; def SEP_ROLE_NORMAL: i32 = 0; -def SEP_ROLE_EXTERNAL_PRODUCTION: i32 = 1; -def SEP_ROLE_TEST_SUPPORT: i32 = 2; -def SEP_ROLE_GENERATED_MAIN: i32 = 3; +def SEP_ROLE_TEST_SUPPORT: i32 = 1; +def SEP_ROLE_GENERATED_MAIN: i32 = 2; def SEP_TEST_SUPPORT_MODULE: str = "__wwtest"; def SEP_MAXPRODUCT: i32 = 256; def SEP_MAXCONTEXT: i32 = 257; @@ -759,9 +758,10 @@ type sepbind = struct { }; type seppkg = struct { - path: *u8, // canonical dotted package identity, NUL-term + path: *u8, // compiler/import identity derived from importbase + importbase: *u8, // canonical ordinary directory import identity entry: *u8, // resolved package dir (or file, file root), NUL-term - artifact: *u8, // non-importable product-root artifact key + artifact: *u8, // stable non-importable variant artifact key name: *u8, // validated declared name; directory packages only testpackage: *u8, sources: **u8, // owned, byte-sorted selected paths; dirs only @@ -769,7 +769,7 @@ type seppkg = struct { isdir: i32, variant: i32, role: i32, - root: bool, + root: bool, // requested usage; never package-action identity linkentry: bool, generatedmain: bool, failed: bool, @@ -795,6 +795,7 @@ type sepgraph = struct { context: []sepcontext, ncontext: i32, supportcontext: i32, + identityfailed: bool, }; type sepproduct = struct { @@ -809,6 +810,99 @@ type sepproduct = struct { variantroot: i32, }; +fn sepdirectoryvariant(variant: i32) bool = { + return variant == SEP_VARIANT_PRODUCTION + || variant == SEP_VARIANT_SAME_TEST + || variant == SEP_VARIANT_EXTERNAL; +}; + +fn sepvariantpath(variant: i32, base: *u8) *u8 = { + if (variant == SEP_VARIANT_EXTERNAL) { return appendlit(base, "_test"); }; + return arenadupcstr(base, cstrlen(base)); +}; + +fn sepdiagpathlocations(path: *u8, a: *u8, b: *u8) void = { + let first: *u8 = a; + let second: *u8 = b; + if (strings.compare(pathstr(first), pathstr(second)) > 0) { + first = b; second = a; + }; + cerr("ww: package "); cerr(pathstr(path)); + cerr(" resolves to directories "); cerr(pathstr(first)); + cerr(" and "); cerr(pathstr(second)); cerr("\n"); +}; + +fn sepdiagdiridentities(entry: *u8, a: *u8, b: *u8) void = { + let first: *u8 = a; + let second: *u8 = b; + if (strings.compare(pathstr(first), pathstr(second)) > 0) { + first = b; second = a; + }; + cerr("ww: package directory "); cerr(pathstr(entry)); + cerr(" has import identities "); cerr(pathstr(first)); + cerr(" and "); cerr(pathstr(second)); cerr("\n"); +}; + +// Bind a provisional directory action to its canonical ordinary import +// identity. The compiler path is derived from that base and the semantic +// variant; root/product/artifact state never participates. +fn sepbindimportbase(g: *sepgraph, pi: i32, base: *u8) i32 = { + if (base == nil || base[0u64] == 0u8 || cstrlen(base) >= 256u64) { + cerr("ww: package path is too long (limit 255 bytes)\n"); + return -1; + }; + let p: *seppkg = &g.pkg[pi]; + if (p.importbase != nil) { + if (cstreq(p.importbase, base)) { return 0; }; + g.identityfailed = true; + sepdiagdiridentities(p.entry, p.importbase, base); + return -1; + }; + let candidate: *u8 = sepvariantpath(p.variant, base); + let i: i32 = 0; + for (i < g.n) { + if (i != pi && g.pkg[i].isdir != 0 && !g.pkg[i].generatedmain) { + let samelocation: bool = os.samefile(pathstr(g.pkg[i].entry), + pathstr(p.entry)); + let supportalias: bool = p.role == SEP_ROLE_TEST_SUPPORT + || g.pkg[i].role == SEP_ROLE_TEST_SUPPORT; + if (!supportalias && !samelocation + && g.pkg[i].importbase != nil + && cstreq(g.pkg[i].importbase, base)) { + g.identityfailed = true; + sepdiagpathlocations(base, g.pkg[i].entry, p.entry); + return -1; + }; + if (samelocation && !supportalias + && g.pkg[i].importbase != nil + && !cstreq(g.pkg[i].importbase, base)) { + g.identityfailed = true; + sepdiagdiridentities(p.entry, g.pkg[i].importbase, base); + return -1; + }; + if (g.pkg[i].path != nil && g.pkg[i].path[0u64] != 0u8 + && cstreq(g.pkg[i].path, candidate)) { + if (!samelocation) { + g.identityfailed = true; + sepdiagpathlocations(candidate, g.pkg[i].entry, p.entry); + return -1; + }; + if (supportalias || g.pkg[i].variant == p.variant) { + g.identityfailed = true; + cerr("ww: package action identity collision for "); + cerr(pathstr(candidate)); cerr(" in "); + cerr(pathstr(p.entry)); cerr("\n"); + return -1; + }; + }; + }; + i += 1; + }; + p.importbase = arenadupcstr(base, cstrlen(base)); + p.path = candidate; + return 0; +}; + fn sepfindoraddvariant(g: *sepgraph, path: *u8, entry: *u8, isdir: i32, variant: i32, testpackage: *u8, role: i32, artifact: *u8, root: bool) i32 = { @@ -816,104 +910,71 @@ fn sepfindoraddvariant(g: *sepgraph, path: *u8, entry: *u8, cerr("ww: package path is too long (limit 255 bytes)\n"); return -1; }; + let incoming: *u8 = nil; + if (isdir != 0 && path[0u64] != 0u8) { + incoming = sepvariantpath(variant, path); + }; let i: i32 = 0; for (i < g.n) { let samelocation: bool = os.samefile(pathstr(g.pkg[i].entry), pathstr(entry)); - 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; }; + if (isdir != 0 && g.pkg[i].isdir != 0 + && !g.pkg[i].generatedmain) { + let supportalias: bool = role == SEP_ROLE_TEST_SUPPORT + || g.pkg[i].role == SEP_ROLE_TEST_SUPPORT; + if (!supportalias && !samelocation && path[0u64] != 0u8 + && g.pkg[i].importbase != nil + && cstreq(path, g.pkg[i].importbase)) { + g.identityfailed = true; + sepdiagpathlocations(path, g.pkg[i].entry, entry); + return -1; + }; + if (incoming != nil && g.pkg[i].path != nil + && g.pkg[i].path[0u64] != 0u8 + && cstreq(incoming, g.pkg[i].path) && !samelocation) { + g.identityfailed = true; + sepdiagpathlocations(incoming, g.pkg[i].entry, entry); + return -1; + }; + if (samelocation && g.pkg[i].variant == variant + && g.pkg[i].role == role) { let sametest: bool = testpackage == nil - && g.pkg[i].testpackage == nil; + && g.pkg[i].testpackage == nil; if (testpackage != nil && g.pkg[i].testpackage != nil) { sametest = cstreq(testpackage, g.pkg[i].testpackage); }; - if (g.pkg[i].role == role && cstreq(g.pkg[i].path, path) - && sametest) { - // One directory variant is one compile action across products. - return i; + if (variant != SEP_VARIANT_PRODUCTION && !sametest) { + cerr("ww: incompatible package-test roots "); + cerr(pathstr(entry)); cerr("\n"); + return -1; }; - cerr("ww: incompatible package-test roots "); - 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 - && cstreq(g.pkg[i].path, path)) { + if (path[0u64] != 0u8 + && sepbindimportbase(g, i, path) < 0) { return -1; }; + g.pkg[i].root = g.pkg[i].root || root; 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; - }; - 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"); + if (samelocation) { + if (supportalias) { i += 1; continue; }; + if (g.pkg[i].importbase != nil && path[0u64] != 0u8 + && !cstreq(g.pkg[i].importbase, path)) { + g.identityfailed = true; + sepdiagdiridentities(entry, g.pkg[i].importbase, path); + return -1; + }; + if (sepdirectoryvariant(g.pkg[i].variant) + && sepdirectoryvariant(variant)) { + i += 1; continue; + }; + cerr("ww: package directory "); cerr(pathstr(entry)); + cerr(" has incompatible variants\n"); return -1; }; - return i; + i += 1; continue; }; - if (samelocation) { - if (role == SEP_ROLE_TEST_SUPPORT - || g.pkg[i].role == SEP_ROLE_TEST_SUPPORT) { - i += 1; - continue; - }; - cerr("ww: package directory "); cerr(pathstr(entry)); - cerr(" has identities "); - if (g.pkg[i].path[0u64] == 0u8) { cerr("(root)"); } - else { cerr(pathstr(g.pkg[i].path)); }; - cerr(" and "); - if (path[0u64] == 0u8) { cerr("(root)"); } - else { cerr(pathstr(path)); }; - cerr("\n"); - return -1; + if (samelocation && cstreq(g.pkg[i].path, path) + && g.pkg[i].variant == variant && g.pkg[i].role == role) { + g.pkg[i].root = g.pkg[i].root || root; + return i; }; i += 1; }; @@ -924,8 +985,9 @@ fn sepfindoraddvariant(g: *sepgraph, path: *u8, entry: *u8, let plen: u64 = cstrlen(path); let elen: u64 = cstrlen(entry); g.pkg[g.n].path = arenadupcstr(path, plen); + g.pkg[g.n].importbase = nil; g.pkg[g.n].entry = arenadupcstr(entry, elen); - g.pkg[g.n].artifact = artifact; + g.pkg[g.n].artifact = nil; g.pkg[g.n].name = nil; g.pkg[g.n].testpackage = nil; if (testpackage != nil) { @@ -955,6 +1017,27 @@ fn sepfindoraddvariant(g: *sepgraph, path: *u8, entry: *u8, g.pkg[g.n].deps = dslot; g.pkg[g.n].ndeps = 0; g.pkg[g.n].color = 0; + if (isdir != 0) { + let inherited: *u8 = path; + if (inherited[0u64] == 0u8) { + i = 0; + for (i < g.n) { + if (g.pkg[i].isdir != 0 && !g.pkg[i].generatedmain + && g.pkg[i].role != SEP_ROLE_TEST_SUPPORT + && role != SEP_ROLE_TEST_SUPPORT + && os.samefile(pathstr(g.pkg[i].entry), pathstr(entry)) + && g.pkg[i].importbase != nil) { + inherited = g.pkg[i].importbase; + break; + }; + i += 1; + }; + }; + if (inherited[0u64] != 0u8 + && sepbindimportbase(g, g.n, inherited) < 0) { return -1; }; + } else { + g.pkg[g.n].artifact = artifact; + }; let r: i32 = g.n; g.n += 1; return r; @@ -1056,6 +1139,7 @@ fn sepfname(g: *sepgraph, pi: i32, scratch: *u8, suffix: str) *u8 = { fn sepvalidateartifactpaths(g: *sepgraph, scratch: *u8) i32 = { let i: i32 = 0; for (i < g.n) { + if (g.pkg[i].failed || !g.pkg[i].loaded) { i += 1; continue; }; let base: *u8 = g.pkg[i].artifact; if (base == nil) { base = g.pkg[i].path; @@ -1067,6 +1151,21 @@ fn sepvalidateartifactpaths(g: *sepgraph, scratch: *u8) i32 = { cerr("ww: package artifact path is too long\n"); return -1; }; + let j: i32 = i + 1; + for (j < g.n) { + if (g.pkg[j].failed || !g.pkg[j].loaded) { j += 1; continue; }; + let other: *u8 = g.pkg[j].artifact; + if (other == nil) { + other = g.pkg[j].path; + if (other[0u64] == 0u8) { other = "__root\0".ptr; }; + }; + if (cstreq(base, other)) { + cerr("ww: package actions share artifact identity "); + cerr(pathstr(base)); cerr("\n"); + return -1; + }; + j += 1; + }; i += 1; }; return 0; @@ -1287,33 +1386,13 @@ 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 = -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); - }; + // External self-production is the ordinary canonical production + // action. Discovery role and product artifact never create another. + let di: i32 = sepfindoradd(g, nm.ptr, ipath, 1); if (di < 0) { return -1; }; let seen: bool = false; let m: i32 = 0; @@ -1364,22 +1443,29 @@ 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].variant < g.pkg[b].variant) { return -1; }; + if (g.pkg[a].variant > g.pkg[b].variant) { return 1; }; 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; + return strings.compare(pathstr(g.pkg[a].entry), + pathstr(g.pkg[b].entry)): i32; }; -fn generatedmainpath(index: i32) *u8 = { - let buf: []u8 = alloc([], 64u64)!; - buf.len = 64; +fn generatedmainkind(variant: i32) str = { + if (variant == SEP_VARIANT_SAME_TEST) { return "internal"; }; + if (variant == SEP_VARIANT_EXTERNAL) { return "external"; }; + return "production"; +}; + +fn generatedmainpath(pkgpath: *u8, kind: str) *u8 = { + let need: u64 = "__wwtestmain.".len: u64 + cstrlen(pkgpath) + + 1u64 + kind.len: u64 + ".main".len: u64 + 1u64; + let buf: []u8 = alloc([], need)!; + buf.len = need: i32; let off: u64 = strinto(buf.ptr, 0u64, "__wwtestmain."); - buf[off] = (((index / 100) % 10) + 48): u8; off += 1u64; - buf[off] = (((index / 10) % 10) + 48): u8; off += 1u64; - buf[off] = ((index % 10) + 48): u8; off += 1u64; + off = cstrinto(buf.ptr, off, pkgpath); + off = byteinto(buf.ptr, off, '.': u8); + off = strinto(buf.ptr, off, kind); off = strinto(buf.ptr, off, ".main"); cstrseal(buf.ptr, off); return buf.ptr; @@ -1387,27 +1473,52 @@ fn generatedmainpath(index: i32) *u8 = { // Materialize the generated test dispatcher as a normal package action. It // owns a generated source unit and imports exactly its test variant/support. +// Its identity is variant-derived, never product-ordinal-derived. fn sepaddgeneratedmain(g: *sepgraph, product: *sepproduct, ordinal: i32, support: i32) i32 = { - if (g.n >= SEP_MAXPKG) { - cerr("ww: too many packages\n"); - return -1; - }; let variant: i32 = product.variantroot; if (variant < 0 || variant >= g.n) { return -1; }; - let p: *seppkg = &g.pkg[g.n]; - p.path = generatedmainpath(ordinal); + let kind: str = generatedmainkind(g.pkg[variant].variant); + let mainpath: *u8 = generatedmainpath(g.pkg[variant].path, kind); let pathi: i32 = 0; for (pathi < g.n) { - if (cstreq(g.pkg[pathi].path, p.path)) { + if (cstreq(g.pkg[pathi].path, mainpath)) { + if (g.pkg[pathi].generatedmain) { + let wantssupport: bool = support >= 0 && support != variant; + let wanteddeps: i32 = 1; + if (wantssupport) { wanteddeps = 2; }; + let hasvariant: bool = false; + let hassupport: bool = false; + let dk: i32 = 0; + for (dk < g.pkg[pathi].ndeps) { + if (g.pkg[pathi].deps[dk] == variant) { hasvariant = true; }; + if (wantssupport && g.pkg[pathi].deps[dk] == support) { + hassupport = true; + }; + dk += 1; + }; + if (hasvariant && hassupport == wantssupport + && g.pkg[pathi].ndeps == wanteddeps) { + return pathi; + }; + }; cerr("ww: generated test-main package identity collides with source import "); - cerr(pathstr(p.path)); cerr("\n"); + cerr(pathstr(mainpath)); cerr("\n"); return -1; }; pathi += 1; }; + if (g.n >= SEP_MAXPKG) { + cerr("ww: too many packages\n"); + return -1; + }; + let p: *seppkg = &g.pkg[g.n]; + p.path = mainpath; + p.importbase = nil; p.entry = g.pkg[variant].entry; - p.artifact = productartifact(ordinal, SEP_VARIANT_TEST_MAIN); + let variantartifact: *u8 = g.pkg[variant].artifact; + if (variantartifact == nil) { variantartifact = g.pkg[variant].path; }; + p.artifact = appendlit(variantartifact, "-main"); p.name = arenadupcstr("main\0".ptr, 4u64); p.testpackage = nil; p.sources = nil; @@ -1549,14 +1660,6 @@ fn seploadpkg(g: *sepgraph, pi: i32, context: i32) i32 = { g.pkg[pi].failed = true; return rc; }; - // Directory roots acquire their canonical package identity. Explicit raw - // single-file compiler fixtures retain their anonymous multi-package reset. - if (g.pkg[pi].root && g.pkg[pi].isdir != 0 - && g.pkg[pi].path[0u64] == 0u8 - && g.pkg[pi].name != nil) { - g.pkg[pi].path = arenadupcstr(g.pkg[pi].name, - cstrlen(g.pkg[pi].name)); - }; let si: i32 = 1; for (si < g.pkg[pi].ndeps) { let v: i32 = g.pkg[pi].deps[si]; @@ -1581,6 +1684,88 @@ fn seploadpkg(g: *sepgraph, pi: i32, context: i32) i32 = { return 0; }; +// Finalize provisional directory identities after source discovery and before +// generated-main construction or compilation. Source-import bindings win; +// otherwise a literal manifest-free root falls back to its validated package +// name (external p_test roots bind the ordinary base p). +fn sepfinalizedirectoryidentities(g: *sepgraph) i32 = { + let pi: i32 = 0; + for (pi < g.n) { + let p: *seppkg = &g.pkg[pi]; + if (p.isdir != 0 && !p.generatedmain && !p.failed && p.loaded + && p.importbase == nil) { + let base: *u8 = nil; + let i: i32 = 0; + for (i < g.n) { + if (i != pi && g.pkg[i].isdir != 0 + && !g.pkg[i].generatedmain + && g.pkg[i].role != SEP_ROLE_TEST_SUPPORT + && p.role != SEP_ROLE_TEST_SUPPORT + && os.samefile(pathstr(g.pkg[i].entry), pathstr(p.entry)) + && g.pkg[i].importbase != nil) { + base = g.pkg[i].importbase; + break; + }; + i += 1; + }; + if (base == nil) { + if (p.name == nil || p.name[0u64] == 0u8) { + cerr("ww: package directory "); cerr(pathstr(p.entry)); + cerr(" has no canonical import identity\n"); + return -1; + }; + if (p.variant == SEP_VARIANT_EXTERNAL) { + let n: u64 = cstrlen(p.name); + if (n <= 5u64 || !cstrendswithlit(p.name, "_test")) { + cerr("ww: package-test selector does not name an external package\n"); + return -1; + }; + let fallback: []u8 = alloc([], n - 5u64 + 1u64)!; + fallback.len = (n - 5u64 + 1u64): i32; + let k: u64 = 0u64; + for (k < n - 5u64) { fallback[k] = p.name[k]; k += 1u64; }; + fallback[n - 5u64] = 0u8; + base = fallback.ptr; + } else { + base = p.name; + }; + }; + if (sepbindimportbase(g, pi, base) < 0) { return -1; }; + }; + pi += 1; + }; + pi = 0; + for (pi < g.n) { + let p: *seppkg = &g.pkg[pi]; + if (p.isdir != 0 && !p.generatedmain && !p.failed && p.loaded) { + let plen: u64 = cstrlen(p.path); + let leaf: *u8 = p.path; + let j: u64 = 0u64; + for (j < plen) { + if (p.path[j] == '.') { leaf = p.path + j + 1u64; }; + j += 1u64; + }; + let supportalias: bool = p.role == SEP_ROLE_TEST_SUPPORT + && cstreqlit(p.path, SEP_TEST_SUPPORT_MODULE) + && cstreqlit(p.name, "test"); + if (!supportalias && !cstreq(p.name, leaf)) { + cerr("ww: package "); cerr(pathstr(p.name)); + cerr(" does not match import path "); cerr(pathstr(p.path)); + cerr("\n"); + return -1; + }; + p.artifact = nil; + if (p.variant == SEP_VARIANT_SAME_TEST) { + p.artifact = appendlit(p.path, "-internal-test"); + } else { if (p.variant == SEP_VARIANT_EXTERNAL) { + p.artifact = appendlit(p.path, "-external-test"); + }; }; + }; + pi += 1; + }; + return 0; +}; + fn sepcyclenode(p: *u8) void = { if (p[0] == 0u8) { cerr("(root)"); } else { cerr(pathstr(p)); }; }; @@ -2230,6 +2415,7 @@ fn buildonesepimpl(selfdir: *u8, src: *u8, entryisdir: i32, context = contextslot, ncontext = 0, supportcontext = -1, + identityfailed = false, })!; if (graphout != nil) { *graphout = g; }; let rootpath: *u8 = "\0".ptr; @@ -2249,9 +2435,13 @@ fn buildonesepimpl(selfdir: *u8, src: *u8, entryisdir: i32, products[producti].context = sepcontextfor(g, contextroot, incs, toolsrcdir); if (products[producti].context < 0) { return 1; }; + let selector: *u8 = products[producti].testpackage; + if (products[producti].variant == SEP_VARIANT_PRODUCTION) { + selector = nil; + }; products[producti].root = sepfindoraddvariant(g, rootpath, entry, entryisdir, products[producti].variant, - products[producti].testpackage, + selector, SEP_ROLE_NORMAL, products[producti].artifact, true); if (products[producti].root < 0) { return 1; }; products[producti].variantroot = products[producti].root; @@ -2358,10 +2548,9 @@ fn buildonesepimpl(selfdir: *u8, src: *u8, entryisdir: i32, producti += 1; continue; }; - if (products[producti].variant != SEP_VARIANT_PRODUCTION - && (products[producti].testpackage == nil - || !cstreq(g.pkg[root].name, - products[producti].testpackage))) { + if (products[producti].testpackage != nil + && !cstreq(g.pkg[root].name, + products[producti].testpackage)) { cerr("ww: package-test selector does not match loaded package\n"); g.pkg[root].failed = true; }; @@ -2377,13 +2566,25 @@ fn buildonesepimpl(selfdir: *u8, src: *u8, entryisdir: i32, g.pkg[variant].failed = true; }; }; + producti += 1; + }; + }; + if (g.identityfailed) { return 1; }; + if (sepfinalizedirectoryidentities(g) < 0) { return 1; }; + if (istest != 0 && entryisdir != 0) { + producti = 0; + for (producti < nproducts) { + let variant: i32 = products[producti].variantroot; + let support: i32 = supportfor[producti]; + if (g.pkg[variant].failed + || (support >= 0 && g.pkg[support].failed)) { + products[producti].root = variant; + producti += 1; + continue; + }; let mainpkg: i32 = sepaddgeneratedmain(g, &products[producti], producti, support); if (mainpkg < 0) { return 1; }; - if (g.pkg[variant].failed - || (support >= 0 && g.pkg[support].failed)) { - g.pkg[mainpkg].failed = true; - }; products[producti].root = mainpkg; producti += 1; }; @@ -2515,7 +2716,8 @@ fn buildonesepimpl(selfdir: *u8, src: *u8, entryisdir: i32, let rawtest: bool = (istest != 0) && g.pkg[pi].root && g.pkg[pi].isdir == 0; let gent: bool = g.pkg[pi].generatedmain || rawtest; - let testpkg: bool = g.pkg[pi].root && (istest != 0) && !gent; + let testpkg: bool = g.pkg[pi].variant == SEP_VARIANT_SAME_TEST + || g.pkg[pi].variant == SEP_VARIANT_EXTERNAL; let entry: bool = g.pkg[pi].linkentry; let supportpkg: bool = g.pkg[pi].testsupport; let alen: u64 = 8u64; @@ -2845,7 +3047,9 @@ fn buildonesep(selfdir: *u8, src: *u8, entryisdir: i32, product.testpackage = testpackage; product.status = nil; product.artifact = nil; - if (packageonly == 0) { product.artifact = "__root\0".ptr; }; + if (packageonly == 0 && entryisdir == 0) { + product.artifact = "__root\0".ptr; + }; product.variant = rootvariant; product.root = -1; product.variantroot = -1; @@ -2898,25 +3102,6 @@ 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 { if (variant == SEP_VARIANT_TEST_MAIN) { - off = strinto(buf.ptr, off, "main"); - } else { - off = strinto(buf.ptr, off, "external"); - }; }; - cstrseal(buf.ptr, off); - return buf.ptr; -}; - fn basenameoff(p: *u8, plen: u64) u64 = { let start: u64 = 0u64; let i: u64 = 0u64; @@ -3578,10 +3763,13 @@ fn dotest(selfdir: *u8, argv: **u8, argc: i32, start: i32) i32 = { let status: *u8 = argv[i + 5]; let pn: u64 = cstrlen(name); let variant: i32 = SEP_VARIANT_EXTERNAL; - if (cstreqlit(kind, "same")) { + if (cstreqlit(kind, "production")) { + variant = SEP_VARIANT_PRODUCTION; + } else { if (cstreqlit(kind, "same")) { variant = SEP_VARIANT_SAME_TEST; - }; - if ((!cstreqlit(kind, "same") + }; }; + if ((!cstreqlit(kind, "production") + && !cstreqlit(kind, "same") && !cstreqlit(kind, "external")) || pn == 0u64 || pn >= 256u64 || dir[0u64] == 0u8 || output[0u64] == 0u8 @@ -3709,8 +3897,8 @@ fn dotest(selfdir: *u8, argv: **u8, argc: i32, start: i32) i32 = { cerr("ww test: duplicate --ww-package-test variant for directory\n"); return 2; }; - products[producti].artifact = productartifact(producti, - products[producti].variant); + // Artifact identity is finalized from canonical package identity. + products[producti].artifact = nil; producti += 1; }; if (products.len != 0 && packageopts) {