From b84fb3ff25bd0470246672ff40de69a2eda8a6fe Mon Sep 17 00:00:00 2001 From: Hojun-Cho Date: Thu, 13 Aug 2026 19:18:00 +0900 Subject: [PATCH] ww: implement local vendor package semantics --- cmd/w6c/main.c | 78 ++- cmd/ww/main.c | 1158 ++++++++++++++++++++++++++++----- internal/wwpackage/package.ww | 18 +- selfhost/cmd/w6c/main.ww | 141 +++- selfhost/cmd/ww/main.ww | 1026 +++++++++++++++++++++++++---- test/sep/localbuild_test.ww | 2 +- 6 files changed, 2128 insertions(+), 295 deletions(-) diff --git a/cmd/w6c/main.c b/cmd/w6c/main.c index 6c50e3ad..53ec7c45 100644 --- a/cmd/w6c/main.c +++ b/cmd/w6c/main.c @@ -41,6 +41,19 @@ struct importin { u64 len; }; +struct importmap { + const char *source; + const char *path; + int seen; +}; + +static const char * +importleaf(const char *path) +{ + const char *dot = strrchr(path, '.'); + return dot != NULL ? dot + 1 : path; +} + static Node * parseinput(Arena *a, const char *file, char *buf, u64 len, const char *mod, const char *testsupport, int commandpackage, int *bad) @@ -85,11 +98,15 @@ main(int argc, char **argv) * only codegen (emit imported==0 decls * only; treat `.wwi` deps as external) */ struct importin *imports = calloc((size_t)argc, sizeof *imports); - if (imports == NULL) { + struct importmap *maps = calloc((size_t)argc, sizeof *maps); + if (imports == NULL || maps == NULL) { fputs("w6c: out of memory\n", stderr); + free(maps); + free(imports); return 1; } int nimports = 0; + int nmaps = 0; for (int i = 1; i < argc; i++) { const char *a = argv[i]; if (strcmp(a, "-o") == 0 && i + 1 < argc) { @@ -120,6 +137,14 @@ main(int argc, char **argv) imports[nimports].path = argv[++i]; imports[nimports].file = argv[++i]; nimports++; + } else if (strcmp(a, "--import-map") == 0) { + if (i + 2 >= argc) { + fputs("w6c: --import-map requires source and path\n", stderr); + return 2; + } + maps[nmaps].source = argv[++i]; + maps[nmaps].path = argv[++i]; + nmaps++; } else if (a[0] == '-') { fprintf(stderr, "w6c: unknown flag %s\n", a); return 2; @@ -132,13 +157,17 @@ main(int argc, char **argv) } if (src == NULL) { fputs("usage: w6c [-T|--test-package] [--command-package] [--entry] [-c] [-I out.wwi] " - "[--import path dep.wwi]... [-o out.s] file.ww\n", stderr); + "[--import path dep.wwi]... [--import-map source path]... [-o out.s] file.ww\n", stderr); return 2; } if (nimports > 0 && !sepmode) { fputs("w6c: --import requires -c\n", stderr); return 2; } + if (nmaps > 0 && !sepmode) { + fputs("w6c: --import-map requires -c\n", stderr); + return 2; + } if ((entrymode || testpackage || commandpackage) && !sepmode) { fputs("w6c: --entry, --test-package, and --command-package require -c\n", stderr); return 2; @@ -157,6 +186,33 @@ main(int argc, char **argv) return 2; } } + for (int i = 0; i < nmaps; i++) { + if (maps[i].source[0] == '\0' || maps[i].path[0] == '\0') { + fputs("w6c: --import-map path is empty\n", stderr); + return 2; + } + if (i > 0 && strcmp(maps[i-1].source, maps[i].source) >= 0) { + fputs("w6c: --import-map sources must be sorted and unique\n", + stderr); + return 2; + } + if (strcmp(importleaf(maps[i].source), + importleaf(maps[i].path)) != 0) { + fputs("w6c: --import-map must preserve import leaf\n", stderr); + return 2; + } + int direct = 0; + for (int j = 0; j < nimports; j++) + if (strcmp(maps[i].path, imports[j].path) == 0) { + direct = 1; + break; + } + if (!direct) { + fputs("w6c: --import-map target is not a direct import\n", + stderr); + return 2; + } + } if (testsupport != NULL && (!sepmode || (strcmp(testsupport, "test") != 0 && strcmp(testsupport, "__wwtest") != 0))) { @@ -203,6 +259,23 @@ main(int argc, char **argv) Node *file = parseinput(a, src, buf, len, NULL, testsupport, commandpackage || entrymode, &bad); if (bad) return 1; + /* Source keeps its effective spelling and leaf alias, while package + * resolution supplies the expanded canonical owner. Rewrite only the + * primary import key before imported interface nodes are prepended. */ + for (Node *u = file->list; u; u = u->next) { + if (u->kind != N_USE || u->usepath == NULL) continue; + for (int i = 0; i < nmaps; i++) + if (strcmp(u->usepath, maps[i].source) == 0) { + u->usepath = maps[i].path; + maps[i].seen = 1; + break; + } + } + for (int i = 0; i < nmaps; i++) { + if (maps[i].seen) continue; + fputs("w6c: --import-map source is not in primary input\n", stderr); + return 2; + } if (head != NULL) { tail->next = file->list; file->list = head; @@ -250,6 +323,7 @@ main(int argc, char **argv) freearena(a); for (int i = 0; i < nimports; i++) free(imports[i].buf); free(imports); + free(maps); free(buf); return 0; } diff --git a/cmd/ww/main.c b/cmd/ww/main.c index 8f6346d0..d50eb562 100644 --- a/cmd/ww/main.c +++ b/cmd/ww/main.c @@ -234,8 +234,8 @@ locate_import_in(const char *dir, const char *path_form, char *out, * /.ww is neither a match nor a shadow: source imports * always create canonical directory-package nodes. */ static int -locate_import(const char *dirs, const char *path_form, char *out, - size_t outsz) +locate_import_root(const char *dirs, const char *path_form, char *out, + size_t outsz, char **root_out) { const char *p = dirs; while (*p) { @@ -243,13 +243,16 @@ locate_import(const char *dirs, const char *path_form, char *out, size_t n = e ? (size_t)(e - p) : strlen(p); if (n > 0) { char *dir = malloc(n + 1); - if (dir == NULL) return 0; + if (dir == NULL) return sep_fail_nomem(); memcpy(dir, p, n); dir[n] = '\0'; int found = locate_import_in(dir, path_form, out, outsz); - free(dir); - if (found) + if (found) { + if (root_out != NULL) *root_out = dir; + else free(dir); return 1; + } + free(dir); } if (!e) break; p = e + 1; @@ -257,6 +260,55 @@ locate_import(const char *dirs, const char *path_form, char *out, return 0; } +static int +locate_import(const char *dirs, const char *path_form, char *out, + size_t outsz) +{ + return locate_import_root(dirs, path_form, out, outsz, NULL) > 0; +} + +/* Allocation-sized source-import lookup. Package identity and search depth + * are not bounded by PATH_MAX; callers own both returned strings. */ +static int +locate_import_alloc(const char *dirs, const char *path_form, + char **entry_out, char **root_out) +{ + const char *p = dirs; + while (*p != '\0') { + const char *e = strchr(p, ':'); + size_t n = e != NULL ? (size_t)(e - p) : strlen(p); + if (n > 0) { + char *root = strndup(p, n); + if (root == NULL) return sep_fail_nomem(); + size_t pn = strlen(path_form); + if (n > (size_t)-1 - pn - 2) { + free(root); + return sep_fail_size(); + } + char *entry = malloc(n + pn + 2); + if (entry == NULL) { + free(root); + return sep_fail_nomem(); + } + memcpy(entry, root, n); + size_t off = n; + if (off == 0 || entry[off - 1] != '/') entry[off++] = '/'; + memcpy(entry + off, path_form, pn + 1); + struct stat st; + if (stat(entry, &st) == 0 && S_ISDIR(st.st_mode)) { + *entry_out = entry; + *root_out = root; + return 1; + } + free(entry); + free(root); + } + if (e == NULL) break; + p = e + 1; + } + return 0; +} + /* CLI target compatibility: directory packages still win globally, then a * bare target may resolve to /.ww. This function is never used * while loading a source import. */ @@ -374,6 +426,7 @@ source_has_test_decl(const char *path) #define SEP_ROLE_GENERATED_MAIN 2 #define SEP_TEST_SUPPORT_MODULE "__wwtest" #define SEP_LOAD_INTERNAL -3 +#define SEP_LOAD_VENDOR -4 /* Package-graph storage grows geometrically. Counts remain signed ints * because they are stable action/context indices throughout the existing @@ -624,6 +677,27 @@ enumerate_dir_ww(const char *dirpath, int variant, const char *test_package, * a `.wwi` relocates the public foreign type/const facts required by its own * API. Full transitive reachability remains a linker concern. */ +struct sepbind { + char kind; + char *name; + int dep; /* stable canonical action index; -1 for inline */ +}; + +struct sepbindset { + struct sepbind *v; + int n, cap; +}; + +struct sepchild { + int pkg; + int context; +}; + +struct sepchildren { + struct sepchild *v; + int n, cap; +}; + struct seppkg { char *path; /* complete compiler/import identity */ char *import_base; /* complete canonical ordinary import identity */ @@ -649,7 +723,7 @@ struct seppkg { int emit_context; /* first verified resolution context */ unsigned char *context_state; /* 0 new, 1 active, 2 checked */ int context_cap; - struct ImportSet bindings; /* first context's canonical import bindings */ + struct sepbindset bindings; /* first context's source-to-action bindings */ int *deps; /* stable direct-dep indices into sepgraph.pkg */ int ndeps; int depcap; @@ -659,6 +733,8 @@ struct seppkg { struct sepcontext { char *root; /* selected entry directory; diagnostic identity */ char *searchpath; /* root : explicit -I roots : toolchain source */ + char *route; /* this package's resolved lexical directory route */ + char *source_root; /* applicable lexical vendor-walk boundary */ }; struct sepgraph { @@ -1022,11 +1098,9 @@ sep_internal_parent_count(const char *path, size_t *parents) } static int -sep_internal_import_allowed(const struct seppkg *from, - const char *target_path, const char *target_entry) +sep_importer_within_owner(const struct seppkg *from, + const char *target_entry, size_t parents) { - size_t parents; - if (!sep_internal_parent_count(target_path, &parents)) return 1; const char *importer = from->canon; char *owned = NULL; if (!from->is_dir) { @@ -1073,6 +1147,84 @@ sep_internal_import_allowed(const struct seppkg *from, return allowed; } +static int +sep_internal_import_allowed(const struct seppkg *from, + const char *target_path, const char *target_entry) +{ + size_t parents; + if (!sep_internal_parent_count(target_path, &parents)) return 1; + return sep_importer_within_owner(from, target_entry, parents); +} + +/* Locate the final exact non-terminal dotted component named vendor. The + * returned suffix points into path; parents is the number of lexical target + * directory components to remove in order to obtain the owning tree. */ +static int +sep_vendor_suffix(const char *path, const char **suffix, size_t *parents) +{ + const char *p = path; + const char *final_suffix = NULL; + size_t components = 0, final_component = 0; + while (*p != '\0') { + const char *dot = strchr(p, '.'); + size_t n = dot != NULL ? (size_t)(dot - p) : strlen(p); + if (dot != NULL && dot[1] != '\0' + && n == sizeof "vendor" - 1 + && memcmp(p, "vendor", n) == 0) { + final_suffix = dot + 1; + final_component = components; + } + components++; + if (dot == NULL) break; + p = dot + 1; + } + if (final_suffix == NULL) return 0; + *suffix = final_suffix; + *parents = components - final_component; + return 1; +} + +/* Filesystem twin of sep_vendor_suffix for a directly selected literal test + * root whose ordinary import identity is not bound until after source scan. */ +static const char * +sep_vendor_route_suffix(const char *path) +{ + const char *p = path; + const char *final = NULL; + while (*p != '\0') { + while (*p == '/') p++; + if (*p == '\0') break; + const char *slash = strchr(p, '/'); + size_t n = slash != NULL ? (size_t)(slash - p) : strlen(p); + if (slash != NULL && slash[1] != '\0' + && n == sizeof "vendor" - 1 + && memcmp(p, "vendor", n) == 0) + final = slash + 1; + if (slash == NULL) break; + p = slash + 1; + } + return final; +} + +static int +sep_vendor_import_allowed(const struct seppkg *from, + const char *target_path, const char *target_entry) +{ + const char *suffix; + size_t parents; + if (!sep_vendor_suffix(target_path, &suffix, &parents)) return 1; + (void)suffix; + return sep_importer_within_owner(from, target_entry, parents); +} + +static int +sep_path_is_vendored(const char *path) +{ + const char *suffix; + size_t parents; + return path != NULL && sep_vendor_suffix(path, &suffix, &parents); +} + static int sep_command_compiler_marker(const struct sepgraph *g, int pi) { @@ -1117,11 +1269,17 @@ sep_bind_import_base(struct sepgraph *g, int pi, const char *base) if (same_location && !support_alias && g->pkg[i].import_base != NULL && strcmp(g->pkg[i].import_base, base) != 0) { - g->identity_failed = 1; - sep_diag_directory_identities(p->entry, - g->pkg[i].import_base, base); - free(path); - return -1; + /* Expanded vendor identity is part of the canonical package key. + * Different vendor routes may intentionally reach one physical + * directory and still denote distinct Go-like packages. */ + if (!sep_path_is_vendored(g->pkg[i].import_base) + && !sep_path_is_vendored(base)) { + g->identity_failed = 1; + sep_diag_directory_identities(p->entry, + g->pkg[i].import_base, base); + free(path); + return -1; + } } if (g->pkg[i].path != NULL && g->pkg[i].path[0] != '\0' && strcmp(g->pkg[i].path, path) == 0) { @@ -1195,6 +1353,17 @@ sep_find_or_add_variant(struct sepgraph *g, const char *path, return -1; } if (same_location && q->variant == variant && q->role == role) { + if (base[0] != '\0' && sep_path_is_vendored(base) + && q->root && q->import_base == NULL) + continue; + if (root && base[0] == '\0' && q->import_base != NULL + && sep_path_is_vendored(q->import_base)) + continue; + if (base[0] != '\0' && q->import_base != NULL + && strcmp(base, q->import_base) != 0 + && (sep_path_is_vendored(base) + || sep_path_is_vendored(q->import_base))) + continue; const char *selected = test_package ? test_package : ""; const char *existing = q->test_package ? q->test_package : ""; if (variant != SEP_VARIANT_PRODUCTION @@ -1220,6 +1389,9 @@ sep_find_or_add_variant(struct sepgraph *g, const char *path, if (support_alias) continue; if (q->import_base != NULL && base[0] != '\0' && strcmp(q->import_base, base) != 0) { + if (sep_path_is_vendored(q->import_base) + || sep_path_is_vendored(base)) + continue; g->identity_failed = 1; sep_diag_directory_identities(entry, q->import_base, base); @@ -1294,6 +1466,9 @@ sep_find_or_add_variant(struct sepgraph *g, const char *path, && role != SEP_ROLE_TEST_SUPPORT && strcmp(g->pkg[i].canon, p->canon) == 0 && g->pkg[i].import_base != NULL) { + if (root && sep_path_is_vendored( + g->pkg[i].import_base)) + continue; inherited = g->pkg[i].import_base; break; } @@ -1338,8 +1513,8 @@ sep_pkg_free_fields(struct seppkg *p) { for (int j = 0; j < p->nsources; j++) free(p->sources[j]); free(p->sources); - for (int j = 0; j < p->bindings.n; j++) free(p->bindings.paths[j]); - free(p->bindings.paths); + for (int j = 0; j < p->bindings.n; j++) free(p->bindings.v[j].name); + free(p->bindings.v); free(p->context_state); free(p->deps); free(p->path); @@ -1363,50 +1538,307 @@ sep_graph_free(struct sepgraph *g) for (int i = 0; i < g->ncontext; i++) { free(g->context[i].root); free(g->context[i].searchpath); + free(g->context[i].route); + free(g->context[i].source_root); } free(g->context); free(g->pkg); 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_import_path_from_relative(const char *, char *, size_t); + +static char * +sep_trimmed_path(const char *path) +{ + size_t n = strlen(path); + while (n > 1 && path[n - 1] == '/') n--; + if (n == 0) return strdup("."); + char *out = strndup(path, n); + if (out == NULL) sep_fail_nomem(); + return out; +} + +static char * +sep_parent_path(const char *path) +{ + size_t n = strlen(path); + while (n > 1 && path[n - 1] == '/') n--; + size_t slash = n; + while (slash > 0 && path[slash - 1] != '/') slash--; + if (slash == 0) return strdup("."); + size_t parent = slash - 1; + while (parent > 1 && path[parent - 1] == '/') parent--; + if (parent == 0) parent = 1; + char *out = strndup(path, parent); + if (out == NULL) sep_fail_nomem(); + return out; +} + +static char * +sep_join_route(const char *root, const char *rel) +{ + size_t n = strlen(root); + return sep_sprintf("%s%s%s", root, + n > 0 && root[n - 1] == '/' ? "" : "/", rel); +} + +static int +sep_lexical_relative(const char *root, const char *route, const char **rel) +{ + size_t rn = strlen(root), pn = strlen(route); + while (rn > 1 && root[rn - 1] == '/') rn--; + while (pn > 1 && route[pn - 1] == '/') pn--; + if (rn == 1 && root[0] == '/') { + if (pn > 1 && route[0] == '/') { *rel = route + 1; return 1; } + return 0; + } + if (rn == 1 && root[0] == '.' && pn > 2 + && route[0] == '.' && route[1] == '/') { + *rel = route + 2; + return 1; + } + if (pn > rn && strncmp(route, root, rn) == 0 + && route[rn] == '/') { + *rel = route + rn + 1; + return 1; + } + return 0; +} + +static int +sep_same_canonical_directory(const char *a, const char *b) +{ + errno = 0; + char *ac = realpath(a, NULL); + int ae = errno; + errno = 0; + char *bc = realpath(b, NULL); + int be = errno; + if ((ac == NULL && ae == ENOMEM) || (bc == NULL && be == ENOMEM)) { + free(ac); free(bc); + return sep_fail_nomem(); + } + int same = ac != NULL && bc != NULL && strcmp(ac, bc) == 0; + free(ac); free(bc); + return same; +} + +/* Determine the active source root before source scanning. An explicit + * logical identity reconstructs the exact root that selected it. A literal + * root uses the first precedence-valid strict ancestor from its own ordered + * search context; otherwise the selected directory itself is the boundary. */ +static int +sep_initial_route_root(const char *entry, const char *identity, + const char *searchpath, char **route_out, char **source_root_out) +{ + char *entry_trim = sep_trimmed_path(entry); + if (entry_trim == NULL) return -1; + if (identity != NULL && identity[0] != '\0') { + char *root = strdup(entry_trim); + if (root == NULL) { + free(entry_trim); + return sep_fail_nomem(); + } + size_t components = 1; + for (const char *p = identity; *p != '\0'; p++) + if (*p == '.') components++; + for (size_t i = 0; i < components; i++) { + char *parent = sep_parent_path(root); + free(root); + root = parent; + if (root == NULL) { free(entry_trim); return -1; } + } + char *pathform = malloc(strlen(identity) + 1); + if (pathform == NULL) { + free(root); free(entry_trim); + return sep_fail_nomem(); + } + if (import_path_form(identity, pathform, strlen(identity) + 1) < 0) { + free(pathform); free(root); free(entry_trim); + return -1; + } + char *route = sep_join_route(root, pathform); + free(pathform); + if (route == NULL) { free(root); free(entry_trim); return -1; } + int same = sep_same_canonical_directory(route, entry_trim); + if (same <= 0) { + if (same == 0) + fprintf(stderr, + "ww: package %s does not match resolved directory %s\n", + identity, entry); + free(route); free(root); free(entry_trim); + return -1; + } + free(entry_trim); + *route_out = route; + *source_root_out = root; + return 0; + } + + errno = 0; + char *entry_canon = realpath(entry_trim, NULL); + if (entry_canon == NULL && errno == ENOMEM) { + free(entry_trim); + return sep_fail_nomem(); + } + const char *p = searchpath; + while (*p != '\0') { + const char *e = strchr(p, ':'); + size_t n = e != NULL ? (size_t)(e - p) : strlen(p); + if (n > 0) { + char *candidate_root = strndup(p, n); + if (candidate_root == NULL) { + free(entry_canon); free(entry_trim); + return sep_fail_nomem(); + } + const char *rel = NULL; + char *physical_root = NULL; + if (!sep_lexical_relative(candidate_root, entry_trim, &rel) + && entry_canon != NULL) { + errno = 0; + physical_root = realpath(candidate_root, NULL); + if (physical_root == NULL && errno == ENOMEM) { + free(candidate_root); free(entry_canon); + free(entry_trim); + return sep_fail_nomem(); + } + if (physical_root != NULL) { + size_t rn = strlen(physical_root); + if (rn == 1 && physical_root[0] == '/' + && entry_canon[0] == '/' + && entry_canon[1] != '\0') { + rel = entry_canon + 1; + } else if (strncmp(entry_canon, physical_root, rn) == 0 + && entry_canon[rn] == '/' + && entry_canon[rn + 1] != '\0') { + rel = entry_canon + rn + 1; + } + } + } + if (rel != NULL && rel[0] != '\0') { + char *identitybuf = malloc(strlen(rel) + 1); + if (identitybuf == NULL) { + free(physical_root); free(candidate_root); + free(entry_canon); free(entry_trim); + return sep_fail_nomem(); + } + int valid = sep_import_path_from_relative(rel, identitybuf, + strlen(rel) + 1); + int reserved = valid > 0 + && reserved_import_path(identitybuf); + free(identitybuf); + if (valid > 0 && !reserved) { + char *located = NULL, *selected_root = NULL; + int found = locate_import_alloc(searchpath, rel, + &located, &selected_root); + if (found < 0) { + free(physical_root); free(candidate_root); + free(entry_canon); free(entry_trim); + return -1; + } + int same = found ? sep_same_canonical_directory( + located, entry_trim) : 0; + free(selected_root); + free(located); + if (same < 0) { + free(physical_root); free(candidate_root); + free(entry_canon); free(entry_trim); + return -1; + } + if (same) { + char *route = sep_join_route(candidate_root, rel); + free(physical_root); free(entry_canon); + free(entry_trim); + if (route == NULL) { + free(candidate_root); + return -1; + } + *route_out = route; + *source_root_out = candidate_root; + return 0; + } + } + } + free(physical_root); + free(candidate_root); + } + if (e == NULL) break; + p = e + 1; + } + free(entry_canon); + *route_out = entry_trim; + *source_root_out = strdup(entry_trim); + if (*source_root_out == NULL) { + free(entry_trim); + *route_out = NULL; + return sep_fail_nomem(); + } + return 0; +} + +static int +sep_context_add(struct sepgraph *g, const char *root, + const char *searchpath, const char *route, const char *source_root) +{ + for (int i = 0; i < g->ncontext; i++) + if (strcmp(g->context[i].root, root) == 0 + && strcmp(g->context[i].searchpath, searchpath) == 0 + && strcmp(g->context[i].route, route) == 0 + && strcmp(g->context[i].source_root, source_root) == 0) + return i; + if (g->ncontext == INT_MAX) return sep_fail_size(); + if (sep_reserve_contexts(g, g->ncontext + 1) < 0) return -1; + struct sepcontext *c = &g->context[g->ncontext]; + memset(c, 0, sizeof *c); + c->root = strdup(root); + c->searchpath = strdup(searchpath); + c->route = sep_trimmed_path(route); + c->source_root = sep_trimmed_path(source_root); + if (c->root == NULL || c->searchpath == NULL || c->route == NULL + || c->source_root == NULL) { + sep_fail_nomem(); + free(c->root); free(c->searchpath); + free(c->route); free(c->source_root); + memset(c, 0, sizeof *c); + return -1; + } + return g->ncontext++; +} + +/* One selected directory owns a base search order. Per-package child + * contexts retain their own lexical route and active source-root boundary; + * neither field participates in canonical package/action identity. */ static int sep_context_for(struct sepgraph *g, const char *root, - const char *extra_includes, const char *toolsrcdir) + const char *extra_includes, const char *toolsrcdir, + const char *identity) { char *searchpath; if (extra_includes != NULL && extra_includes[0] != '\0') searchpath = sep_sprintf("%s:%s:%s", root, extra_includes, toolsrcdir); else - searchpath = sep_sprintf("%s:%s", - root, toolsrcdir); + searchpath = sep_sprintf("%s:%s", root, toolsrcdir); if (searchpath == NULL) return -1; - for (int i = 0; i < g->ncontext; i++) - if (strcmp(g->context[i].searchpath, searchpath) == 0) { - free(searchpath); - return i; - } - if (g->ncontext == INT_MAX) { - sep_fail_size(); + char *route = NULL, *source_root = NULL; + if (sep_initial_route_root(root, identity, searchpath, + &route, &source_root) < 0) { free(searchpath); return -1; } - if (sep_reserve_contexts(g, g->ncontext + 1) < 0) { - free(searchpath); - return -1; - } - struct sepcontext *c = &g->context[g->ncontext]; - c->root = strdup(root); - c->searchpath = searchpath; - if (c->root == NULL) { - sep_fail_nomem(); - free(searchpath); - return -1; - } - return g->ncontext++; + int result = sep_context_add(g, root, searchpath, route, source_root); + free(source_root); free(route); free(searchpath); + return result; +} + +static int +sep_child_context_for(struct sepgraph *g, int parent, const char *route, + const char *source_root) +{ + if (parent < 0 || parent >= g->ncontext) return -1; + return sep_context_add(g, g->context[parent].root, + g->context[parent].searchpath, route, source_root); } /* Semantic package identity never enters a bounded filesystem component. @@ -1624,44 +2056,213 @@ sep_external_production_name(const struct seppkg *pkg, const char *path, && strcmp(pkg->test_package + n, "_test") == 0; } -/* Canonical bindings make a shared package independent of which selected - * root reaches it first. Directory bindings and the raw-file inline - * compatibility binding are part of the package action's source meaning. */ +static char *sep_local_import_base(const struct seppkg *); + +/* Canonical source bindings make a shared action independent of the request + * that reaches it first. A direct binding retains the stable target action + * index, hence both expanded identity and canonical directory; contextual + * route/root legality deliberately does not enter action identity. */ static int -sep_binding_add(struct ImportSet *bindings, char kind, const char *name, - const char *target) +sep_binding_add(struct sepbindset *bindings, char kind, const char *name, + int dep) { - size_t nn = strlen(name), tn = target ? strlen(target) : 0; - if (tn > (size_t)-1 - 4 || nn > (size_t)-1 - 4 - tn) { - sep_fail_size(); + for (int i = 0; i < bindings->n; i++) + if (bindings->v[i].kind == kind + && bindings->v[i].dep == dep + && strcmp(bindings->v[i].name, name) == 0) + return 0; + if (bindings->n == INT_MAX) return sep_fail_size(); + if (sep_reserve((void **)&bindings->v, &bindings->cap, + bindings->n + 1, sizeof *bindings->v) < 0) return -1; - } - char *binding = malloc(nn + tn + 4); - if (binding == NULL) return sep_fail_nomem(); - 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 == INT_MAX) { - sep_fail_size(); - free(binding); - return -1; - } - if (sep_reserve((void **)&bindings->paths, &bindings->cap, - bindings->n + 1, sizeof *bindings->paths) < 0) { - free(binding); - return -1; - } - bindings->paths[bindings->n++] = binding; + char *copy = strdup(name); + if (copy == NULL) return sep_fail_nomem(); + bindings->v[bindings->n++] = (struct sepbind){ kind, copy, dep }; return 0; } +static int +sep_binding_cmp(const void *a, const void *b) +{ + const struct sepbind *x = a, *y = b; + int r = strcmp(x->name, y->name); + if (r != 0) return r; + if (x->kind != y->kind) return (unsigned char)x->kind + - (unsigned char)y->kind; + return x->dep < y->dep ? -1 : x->dep > y->dep; +} + +static void +sep_bindset_free(struct sepbindset *bindings) +{ + for (int i = 0; i < bindings->n; i++) free(bindings->v[i].name); + free(bindings->v); + bindings->v = NULL; + bindings->n = bindings->cap = 0; +} + +static int +sep_children_add(struct sepchildren *children, int pkg, int context) +{ + for (int i = 0; i < children->n; i++) + if (children->v[i].pkg == pkg + && children->v[i].context == context) + return 0; + if (children->n == INT_MAX) return sep_fail_size(); + if (sep_reserve((void **)&children->v, &children->cap, + children->n + 1, sizeof *children->v) < 0) + return -1; + children->v[children->n++] = (struct sepchild){ pkg, context }; + return 0; +} + +static void +sep_children_free(struct sepchildren *children) +{ + free(children->v); + children->v = NULL; + children->n = children->cap = 0; +} + +struct sepresolved { + char *identity; /* expanded canonical package identity */ + char *entry; /* current edge's resolved lexical route */ + char *source_root; /* child vendor-walk boundary */ + int vendored; +}; + +static void +sep_resolved_free(struct sepresolved *r) +{ + free(r->identity); + free(r->entry); + free(r->source_root); + memset(r, 0, sizeof *r); +} + +/* A candidate shadows outer/ordinary lookup only when the directory contains + * at least one observed source filename. A source-named nonregular entry still + * selects the package so enumeration reports the real package error. Returns + * -1 only for deterministic loader-owned allocation failure. */ +static int +sep_vendor_candidate_has_sources(const char *candidate) +{ + struct stat st; + if (stat(candidate, &st) != 0) return 0; + if (!S_ISDIR(st.st_mode)) return 0; + DIR *d = opendir(candidate); + if (d == NULL) return 0; + int found = 0; + struct dirent *de; + while ((de = readdir(d)) != NULL) { + size_t n = strlen(de->d_name); + if (n < 3 || strcmp(de->d_name + n - 3, ".ww") != 0) + continue; + char *path = sep_join_route(candidate, de->d_name); + if (path == NULL) { (void)closedir(d); return -1; } + struct stat ent; + int directory = lstat(path, &ent) == 0 && S_ISDIR(ent.st_mode); + free(path); + if (directory) continue; + found = 1; + break; + } + (void)closedir(d); + return found; +} + +/* Expand one source import under its own package context. The nearest + * source-bearing vendor candidate wins; ordinary lookup additionally records + * the exact ordered root that selected the child. */ +static int +sep_resolve_source_import(struct sepgraph *g, int context, + const char *name, const char *path_form, struct sepresolved *out) +{ + memset(out, 0, sizeof *out); + if (context < 0 || context >= g->ncontext) return -1; + const char *route = g->context[context].route; + const char *source_root = g->context[context].source_root; + const char *ignored; + if (strcmp(route, source_root) != 0 + && !sep_lexical_relative(source_root, route, &ignored)) { + fprintf(stderr, "ww: package route %s is outside source root %s\n", + route, source_root); + return -1; + } + const char *direct_suffix; + size_t direct_parents; + int direct_expanded = sep_vendor_suffix(name, &direct_suffix, + &direct_parents); + (void)direct_suffix; + (void)direct_parents; + char *ancestor = NULL; + if (!direct_expanded) ancestor = sep_trimmed_path(route); + if (!direct_expanded && ancestor == NULL) return -1; + while (!direct_expanded) { + char *vendordir = sep_join_route(ancestor, "vendor"); + char *candidate = vendordir != NULL + ? sep_join_route(vendordir, path_form) : NULL; + free(vendordir); + if (candidate == NULL) { free(ancestor); return -1; } + int source_candidate = sep_vendor_candidate_has_sources(candidate); + if (source_candidate < 0) { + free(candidate); free(ancestor); + return -1; + } + if (source_candidate > 0) { + const char *rel = NULL; + if (!sep_lexical_relative(source_root, candidate, &rel)) { + free(candidate); free(ancestor); + return -1; + } + size_t n = strlen(rel); + char *identity = malloc(n + 1); + if (identity == NULL) { + free(candidate); free(ancestor); + return sep_fail_nomem(); + } + int valid = sep_import_path_from_relative(rel, identity, n + 1); + if (valid <= 0 || reserved_import_path(identity)) { + free(identity); free(candidate); free(ancestor); + if (valid < 0) sep_fail_size(); + else fprintf(stderr, + "ww: invalid vendored package path %s\n", rel); + return -1; + } + out->source_root = strdup(source_root); + if (out->source_root == NULL) { + free(identity); free(candidate); free(ancestor); + return sep_fail_nomem(); + } + out->identity = identity; + out->entry = candidate; + out->vendored = 1; + free(ancestor); + return 1; + } + free(candidate); + if (strcmp(ancestor, source_root) == 0) break; + char *parent = sep_parent_path(ancestor); + if (parent == NULL) { free(ancestor); return -1; } + if (strcmp(parent, ancestor) == 0) { + free(parent); free(ancestor); + return -1; + } + free(ancestor); + ancestor = parent; + } + free(ancestor); + int located = locate_import_alloc(g->context[context].searchpath, + path_form, &out->entry, &out->source_root); + if (located <= 0) return located; + out->identity = strdup(name); + if (out->identity == NULL) { + sep_resolved_free(out); + return sep_fail_nomem(); + } + return 1; +} + /* 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 @@ -1669,8 +2270,9 @@ sep_binding_add(struct ImportSet *bindings, char kind, const char *name, * (§1.1). */ static int sep_scan_file(struct sepgraph *g, int pi, const char *file, - const char *searchpath, struct ImportSet *filevisit, - struct ImportSet *bindings, int owned_source) + int context, struct ImportSet *filevisit, + struct sepbindset *bindings, struct sepchildren *children, + int owned_source) { if (import_seen(filevisit, file)) return 0; if (import_add(filevisit, file) < 0) return -1; @@ -1775,30 +2377,62 @@ sep_scan_file(struct sepgraph *g, int pi, const char *file, rc = -1; break; } - char path_form[PATH_MAX]; - if (import_path_form(name, path_form, sizeof path_form) < 0) { - errorf(u->pos, "import path is too long"); + size_t pathlen = strlen(name); + char *path_form = malloc(pathlen + 1); + if (path_form == NULL) { + sep_fail_nomem(); rc = -1; break; } - char ipath[PATH_MAX]; + if (import_path_form(name, path_form, pathlen + 1) < 0) { + free(path_form); + sep_fail_size(); + rc = -1; + break; + } + struct sepresolved resolved = {0}; int external_production = 0; - /* A selected external logical root is already an exact resolved - * path/directory pair. Its source import of that same full ordinary - * identity reuses the colocated production action. No declaration leaf - * or unrelated cached package can override normal context lookup. */ - const char *bound = g->pkg[pi].variant == SEP_VARIANT_EXTERNAL - && g->pkg[pi].import_base != NULL - && strcmp(g->pkg[pi].import_base, name) == 0 - ? g->pkg[pi].canon : NULL; - int located = bound != NULL; - if (located) - snprintf(ipath, sizeof ipath, "%s", bound); - else - located = locate_import(searchpath, path_form, ipath, - sizeof ipath); - const char *visibility_entry = bound != NULL - ? g->pkg[pi].entry : ipath; + /* External tests import their colocated production package through the + * same source spelling rules. Preserve the exact route/root of this edge; + * a direct expanded spelling is still rejected below. */ + int located = sep_resolve_source_import(g, context, name, + path_form, &resolved); + /* A directly selected literal external-test root may not yet have + * an ordinary identity. Only after normal source resolution has found + * no candidate may its short self import bind colocated production. */ + if (located == 0 && g->pkg[pi].variant == SEP_VARIANT_EXTERNAL) { + const char *route_suffix = sep_vendor_route_suffix( + g->context[context].route); + int literal_self = route_suffix != NULL + ? strcmp(path_form, route_suffix) == 0 + && sep_external_production_name(&g->pkg[pi], name, 1) + : strchr(name, '.') == NULL + && sep_external_production_name(&g->pkg[pi], name, 0); + if (literal_self) { + if (g->pkg[pi].import_base != NULL) + resolved.identity = strdup(g->pkg[pi].import_base); + else + resolved.identity = sep_local_import_base(&g->pkg[pi]); + resolved.entry = strdup(g->context[context].route); + resolved.source_root = strdup( + g->context[context].source_root); + if (resolved.identity == NULL || resolved.entry == NULL + || resolved.source_root == NULL) { + if (!sep_fatal_allocation) sep_fail_nomem(); + sep_resolved_free(&resolved); + free(path_form); + rc = -1; + break; + } + located = 1; + } + } + free(path_form); + if (located < 0) { + sep_resolved_free(&resolved); + rc = -1; + break; + } if (!located) { const char *dot = strrchr(name, '.'); const char *leaf = dot ? dot + 1 : name; @@ -1811,7 +2445,7 @@ sep_scan_file(struct sepgraph *g, int pi, const char *file, break; } if (inline_package) { - if (sep_binding_add(bindings, 'I', name, NULL) < 0) + if (sep_binding_add(bindings, 'I', name, -1) < 0) rc = -1; continue; } @@ -1821,14 +2455,16 @@ sep_scan_file(struct sepgraph *g, int pi, const char *file, } { errno = 0; - char *canon = realpath(ipath, NULL); + char *canon = realpath(resolved.entry, NULL); if (canon == NULL) { if (errno == ENOMEM) { sep_fail_nomem(); + sep_resolved_free(&resolved); rc = -1; break; } errorf(u->pos, "cannot canonicalize package '%s'", name); + sep_resolved_free(&resolved); rc = -1; break; } @@ -1844,31 +2480,73 @@ sep_scan_file(struct sepgraph *g, int pi, const char *file, errorf(u->pos, "self-import: package '%s' cannot import itself", owner[0] ? owner : "(root)"); free(canon); + sep_resolved_free(&resolved); rc = -1; break; } /* 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) { free(canon); rc = -1; break; } + int di = sep_find_or_add(g, resolved.identity, + resolved.entry, 1); + if (di < 0) { + free(canon); + sep_resolved_free(&resolved); + rc = -1; + break; + } int allowed = sep_internal_import_allowed(&g->pkg[pi], - g->pkg[di].path, visibility_entry); - if (allowed < 0) { free(canon); rc = -1; break; } + resolved.identity, resolved.entry); + if (allowed < 0) { + free(canon); sep_resolved_free(&resolved); + rc = -1; break; + } if (!allowed) { errorf(u->pos, "use of internal package %s not allowed", - g->pkg[di].path); + resolved.identity); free(canon); + sep_resolved_free(&resolved); rc = SEP_LOAD_INTERNAL; break; } - if (sep_binding_add(bindings, 'D', name, canon) < 0) { + allowed = sep_vendor_import_allowed(&g->pkg[pi], + resolved.identity, resolved.entry); + if (allowed < 0) { + free(canon); sep_resolved_free(&resolved); + rc = -1; break; + } + if (!allowed) { + errorf(u->pos, "use of vendored package not allowed"); free(canon); + sep_resolved_free(&resolved); + rc = SEP_LOAD_VENDOR; + break; + } + const char *suffix; + size_t parents; + if (sep_vendor_suffix(resolved.identity, &suffix, &parents) + && strcmp(name, suffix) != 0) { + (void)parents; + errorf(u->pos, "%s must be imported as %s", + resolved.identity, suffix); + free(canon); + sep_resolved_free(&resolved); + rc = SEP_LOAD_VENDOR; + break; + } + int child_context = sep_child_context_for(g, context, + resolved.entry, resolved.source_root); + if (child_context < 0 + || sep_binding_add(bindings, 'D', name, di) < 0 + || sep_add_dep(g, pi, di) < 0 + || sep_children_add(children, di, child_context) < 0) { + free(canon); + sep_resolved_free(&resolved); rc = -1; break; } free(canon); - if (sep_add_dep(g, pi, di) < 0) { rc = -1; break; } + sep_resolved_free(&resolved); } } free(uses); @@ -2006,12 +2684,13 @@ fail: * Dependency descent is iterative below so a valid deep graph consumes the * growable frame vector rather than the process call stack. */ static int -sep_prepare_pkg_context(struct sepgraph *g, int pi, int context) +sep_prepare_pkg_context(struct sepgraph *g, int pi, int context, + struct sepchildren *children) { if (sep_set_context_state(&g->pkg[pi], context, 1) < 0) return -1; - const char *searchpath = g->context[context].searchpath; - struct ImportSet fv = {0}, bindings = {0}; + struct ImportSet fv = {0}; + struct sepbindset bindings = {0}; int rc = 0; if (!g->pkg[pi].loaded) { g->pkg[pi].loaded = 1; @@ -2037,7 +2716,7 @@ sep_prepare_pkg_context(struct sepgraph *g, int pi, int context) 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, &bindings, 1); + context, &fv, &bindings, children, 1); if (rc == 0 && g->pkg[pi].path[0] != '\0' && !g->pkg[pi].test_support) { const char *dot = strrchr(g->pkg[pi].path, '.'); @@ -2051,33 +2730,40 @@ sep_prepare_pkg_context(struct sepgraph *g, int pi, int context) } } } else if (rc == 0) { - rc = sep_scan_file(g, pi, g->pkg[pi].entry, searchpath, - &fv, &bindings, 0); + rc = sep_scan_file(g, pi, g->pkg[pi].entry, context, + &fv, &bindings, children, 0); } sep_import_set_free(&fv); if (bindings.n > 1) - qsort(bindings.paths, (size_t)bindings.n, - sizeof *bindings.paths, strs_cmp); + qsort(bindings.v, (size_t)bindings.n, + sizeof *bindings.v, sep_binding_cmp); if (rc == 0 && g->pkg[pi].emit_context < 0) { g->pkg[pi].bindings = bindings; - bindings.paths = NULL; + bindings.v = NULL; bindings.n = bindings.cap = 0; g->pkg[pi].emit_context = context; } else if (rc == 0) { - struct ImportSet *want = &g->pkg[pi].bindings; + struct sepbindset *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) + if (want->v[i].kind != bindings.v[i].kind + || want->v[i].dep != bindings.v[i].dep + || strcmp(want->v[i].name, bindings.v[i].name) != 0) rc = -1; if (rc < 0) { + const char *first = + g->context[g->pkg[pi].emit_context].root; + const char *second = g->context[context].root; + if (strcmp(first, second) > 0) { + const char *swap = first; first = second; second = swap; + } 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].canon, - g->context[g->pkg[pi].emit_context].root, - g->context[context].root); + first, second); } } - sep_import_set_free(&bindings); + sep_bindset_free(&bindings); if (rc < 0) { g->pkg[pi].context_state[context] = 2; g->pkg[pi].failed = 1; @@ -2101,8 +2787,9 @@ sep_prepare_pkg_context(struct sepgraph *g, int pi, int context) struct seploadframe { int pkg; int context; - int next_dep; + int next_child; int pending_dep; + struct sepchildren children; }; /* Load one canonical package under one selected-root resolution context. @@ -2122,28 +2809,31 @@ sep_load_pkg(struct sepgraph *g, int pi, int context) if (sep_reserve((void **)&frames, &framecap, 1, sizeof *frames) < 0) return -2; - frames[nframe++] = (struct seploadframe){ pi, context, -1, -1 }; + frames[nframe++] = (struct seploadframe){ pi, context, -1, -1, {0} }; while (nframe > 0) { struct seploadframe *f = &frames[nframe - 1]; - if (f->next_dep < 0) { + if (f->next_child < 0) { unsigned char state = sep_context_state(&g->pkg[f->pkg], f->context); if (state == 2) { if (g->pkg[f->pkg].failed) goto failed; + sep_children_free(&f->children); nframe--; continue; } if (state == 1) { + sep_children_free(&f->children); nframe--; continue; } - int prepared = sep_prepare_pkg_context(g, f->pkg, f->context); + int prepared = sep_prepare_pkg_context(g, f->pkg, f->context, + &f->children); if (prepared < 0) { result = prepared; goto failed; } - f->next_dep = 0; + f->next_child = 0; } if (f->pending_dep >= 0) { int dep = f->pending_dep; @@ -2157,38 +2847,46 @@ sep_load_pkg(struct sepgraph *g, int pi, int context) goto failed; } } - if (f->next_dep >= g->pkg[f->pkg].ndeps) { + if (f->next_child >= f->children.n) { + sep_children_free(&f->children); nframe--; continue; } - int dep = g->pkg[f->pkg].deps[f->next_dep++]; + struct sepchild child = f->children.v[f->next_child++]; + int dep = child.pkg; f->pending_dep = dep; - int child_context = f->context; + int child_context = child.context; if (g->pkg[dep].test_support && g->support_context >= 0) child_context = g->support_context; if (nframe == INT_MAX) { sep_fail_size(); - for (int i = 0; i < nframe; i++) + for (int i = 0; i < nframe; i++) { g->pkg[frames[i].pkg].failed = 1; + sep_children_free(&frames[i].children); + } free(frames); return -2; } if (sep_reserve((void **)&frames, &framecap, nframe + 1, sizeof *frames) < 0) { - for (int i = 0; i < nframe; i++) + for (int i = 0; i < nframe; i++) { g->pkg[frames[i].pkg].failed = 1; + sep_children_free(&frames[i].children); + } free(frames); return -2; } frames[nframe++] = (struct seploadframe){ - dep, child_context, -1, -1 }; + dep, child_context, -1, -1, {0} }; } free(frames); return 0; failed: - for (int i = 0; i < nframe; i++) + for (int i = 0; i < nframe; i++) { g->pkg[frames[i].pkg].failed = 1; + sep_children_free(&frames[i].children); + } free(frames); return sep_fatal_allocation ? -2 : result; } @@ -2227,6 +2925,37 @@ sep_import_path_from_relative(const char *rel, char *out, size_t outsz) return off != 0; } +/* A literal directory selected below an applicable source root already has a + * complete lexical identity. Bind it before any source edge can reach the + * same physical directory under a different vendored route. */ +static int +sep_context_import_base(const struct sepgraph *g, int context, char **out) +{ + *out = NULL; + if (context < 0 || context >= g->ncontext) return -1; + const struct sepcontext *c = &g->context[context]; + if (strcmp(c->route, c->source_root) == 0) return 0; + const char *rel = NULL; + if (!sep_lexical_relative(c->source_root, c->route, &rel) + || rel == NULL || rel[0] == '\0') { + fprintf(stderr, "ww: package route %s is outside source root %s\n", + c->route, c->source_root); + return -1; + } + size_t n = strlen(rel); + char *base = malloc(n + 1); + if (base == NULL) return sep_fail_nomem(); + int converted = sep_import_path_from_relative(rel, base, n + 1); + if (converted <= 0 || reserved_import_path(base)) { + if (converted < 0) sep_fail_size(); + else fprintf(stderr, "ww: invalid package path %s\n", rel); + free(base); + return -1; + } + *out = base; + return 1; +} + /* A reverse candidate is authoritative only when the normal ordered forward * lookup selects this exact canonical directory. This prevents a later or * nested source root from manufacturing an alias shadowed by an earlier root. */ @@ -2404,7 +3133,9 @@ sep_finalize_directory_identities(struct sepgraph *g) || 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 == NULL) + || g->pkg[i].import_base == NULL + || (p->root && sep_path_is_vendored( + g->pkg[i].import_base))) continue; base = g->pkg[i].import_base; break; @@ -2591,6 +3322,19 @@ sep_emit_body(FILE *out, const char *path, const char *modpath) return 0; } +/* Filesystem paths are opaque byte strings. Preserve them in the persistent + * identity without letting a newline in a legal path escape its comment. */ +static int +sep_emit_hex(FILE *out, const char *value) +{ + static const char hex[] = "0123456789abcdef"; + for (const unsigned char *p = (const unsigned char *)value; *p; p++) + if (fputc(hex[*p >> 4], out) == EOF + || fputc(hex[*p & 15], out) == EOF) + return -1; + return 0; +} + /* Compose pi's sep-unit at `unitf` from only pi's byte-sorted sources. * Direct exports are separate compiler inputs; the linker separately retains * the reachable archive closure. */ @@ -2621,6 +3365,31 @@ sep_compose_unit(struct sepgraph *g, int pi, const char *unitf) } else { bodyrc = sep_emit_body(u, g->pkg[pi].entry, g->pkg[pi].path); } + const char *own_suffix; + size_t own_parents; + if (bodyrc == 0 && sep_vendor_suffix(g->pkg[pi].path, + &own_suffix, &own_parents)) { + (void)own_suffix; + (void)own_parents; + if (fputs("//ww:vendor-dir ", u) == EOF + || sep_emit_hex(u, g->pkg[pi].canon) < 0 + || fputc('\n', u) == EOF) + bodyrc = -1; + } + /* The opaque source-spelling map is also part of the persistent unit + * voucher. Include the canonical target directory so a vendored symlink + * retarget cannot reuse stale assembly even when export bytes are equal. */ + for (int i = 0; i < g->pkg[pi].bindings.n && bodyrc == 0; i++) { + struct sepbind *b = &g->pkg[pi].bindings.v[i]; + if (b->kind != 'D' || b->dep < 0 || b->dep >= g->n + || strcmp(b->name, g->pkg[b->dep].path) == 0) + continue; + if (fprintf(u, "//ww:import-map %s %s ", + b->name, g->pkg[b->dep].path) < 0 + || sep_emit_hex(u, g->pkg[b->dep].canon) < 0 + || fputc('\n', u) == EOF) + bodyrc = -1; + } if (fclose(u) != 0) { fprintf(stderr, "ww: cannot close package unit %s\n", unitf); return -1; @@ -2808,7 +3577,7 @@ static void workdir_stamp_text(char *buf, size_t bufsz, int is_test, int emit_asm) { snprintf(buf, bufsz, "ww workdir fmt %d mode %s asm %d\n", - is_test ? 11 : 12, is_test ? "test" : "build", emit_asm); + is_test ? 12 : 13, is_test ? "test" : "build", emit_asm); } /* A stale global builder identity invalidates every committed unit voucher in @@ -2861,10 +3630,6 @@ build_one_sep_impl(const char *src, int entry_is_dir, { sep_fatal_allocation = 0; if (nproducts < 1) return 1; - for (int i = 0; i < nproducts; i++) - if (products[i].status != NULL - && unlink(products[i].status) != 0 && errno != ENOENT) - return 1; const char *c6 = toolpath("WW_W6C", "w6c"); const char *a6 = toolpath("WW_W6A", "w6a"); const char *l6 = toolpath("WW_W6L", "w6l"); @@ -2933,17 +3698,7 @@ build_one_sep_impl(const char *src, int entry_is_dir, fprintf(stderr, "ww: scratch path is too long\n"); return 1; } - if (mkdir(scratch, 0755) != 0) { - fprintf(stderr, "ww: cannot create scratch %s\n", scratch); - return 1; - } - /* Hand the scratch path back only after mkdir succeeds. The - * wrapper therefore never removes a pre-existing path that this - * build failed to acquire. */ - if (scratchout) { - if (strlen(scratch) + 1 > scratchoutsz) return 1; - memcpy(scratchout, scratch, strlen(scratch) + 1); - } + if (scratchout && strlen(scratch) + 1 > scratchoutsz) return 1; } int stale_all = 0, stampok = 0; char toolw[PATH_MAX] = {0}, toolc[PATH_MAX] = {0}; @@ -3005,16 +3760,25 @@ build_one_sep_impl(const char *src, int entry_is_dir, } contextroot = contextdir; } - 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; - const char *rootpath = products[i].identity != NULL + const char *requested_path = products[i].identity != NULL ? products[i].identity : ""; + products[i].context = sep_context_for(g, contextroot, + extra_includes, toolsrcdir, requested_path); + if (products[i].context < 0) return 1; + char *inferred_path = NULL; + const char *rootpath = requested_path; + if (entry_is_dir && rootpath[0] == '\0') { + int inferred = sep_context_import_base(g, products[i].context, + &inferred_path); + if (inferred < 0) return 1; + if (inferred > 0) rootpath = inferred_path; + } products[i].root = sep_find_or_add_variant(g, rootpath, entry, entry_is_dir, products[i].variant, selector, SEP_ROLE_NORMAL, products[i].artifact, 1); + free(inferred_path); if (products[i].root < 0) return 1; products[i].variant_root = products[i].root; } @@ -3030,8 +3794,12 @@ build_one_sep_impl(const char *src, int entry_is_dir, int tdir = 0; if (locate_import(toolsrcdir, "test", tpath, sizeof tpath)) { tdir = 1; - g->support_context = sep_context_for(g, toolsrcdir, NULL, - toolsrcdir); + char *support_search = sep_sprintf("%s:%s", + toolsrcdir, toolsrcdir); + if (support_search == NULL) return 1; + g->support_context = sep_context_add(g, toolsrcdir, + support_search, tpath, toolsrcdir); + free(support_search); if (g->support_context < 0) return 1; errno = 0; char *tc = realpath(tpath, NULL); @@ -3112,7 +3880,7 @@ build_one_sep_impl(const char *src, int entry_is_dir, } int lr = sep_load_pkg(g, root, products[i].context); if (lr == -2) return 1; - if (lr == SEP_LOAD_INTERNAL) return 1; + if (lr == SEP_LOAD_INTERNAL || lr == SEP_LOAD_VENDOR) return 1; if (lr < 0) { g->pkg[root].failed = 1; continue; @@ -3124,14 +3892,15 @@ build_one_sep_impl(const char *src, int entry_is_dir, g->pkg[root].failed = 1; } } - if (is_test && entry_is_dir) { + if (is_test) { for (int i = 0; i < nproducts; i++) { int variant = products[i].variant_root; int support = products[i].support; if (support >= 0 && support != variant) { int lr = sep_load_pkg(g, support, products[i].context); if (lr == -2) return 1; - if (lr == SEP_LOAD_INTERNAL) return 1; + if (lr == SEP_LOAD_INTERNAL || lr == SEP_LOAD_VENDOR) + return 1; if (lr < 0) g->pkg[variant].failed = 1; } } @@ -3168,8 +3937,6 @@ build_one_sep_impl(const char *src, int entry_is_dir, return 1; if (warm && sep_validate_workdir_owners(g, scratch) < 0) return 1; - if (warm && stale_all && invalidate_workdir_units(scratch) != 0) - return 1; int *order = calloc((size_t)g->n, sizeof *order); int *stack = calloc((size_t)g->n, sizeof *stack); int norder = 0; @@ -3218,6 +3985,45 @@ build_one_sep_impl(const char *src, int entry_is_dir, } } free(stack); + /* Propagate already-known package-load failures through the union before + * acquiring scratch or completion state. Independent sibling roots remain + * viable, but an entirely rejected cold request leaves no empty tree. */ + for (int oi = 0; oi < norder; oi++) { + int pi = order[oi]; + for (int k = 0; k < g->pkg[pi].ndeps; k++) + if (g->pkg[g->pkg[pi].deps[k]].failed) + g->pkg[pi].failed = 1; + } + int viable_product = 0; + for (int i = 0; i < nproducts; i++) + if (!g->pkg[products[i].root].failed) viable_product = 1; + if (!viable_product) { + free(order); + return 1; + } + /* Product completion and persistent package state remain untouched until + * all source-derived imports, contextual legality, cycles, command kind, + * output paths, and action closures have passed their pre-tool checks. */ + if (!warm) { + if (mkdir(scratch, 0755) != 0) { + fprintf(stderr, "ww: cannot create scratch %s\n", scratch); + free(order); + return 1; + } + /* The wrapper owns only the directory this invocation acquired. */ + if (scratchout != NULL) + memcpy(scratchout, scratch, strlen(scratch) + 1); + } + for (int i = 0; i < nproducts; i++) + if (products[i].status != NULL + && unlink(products[i].status) != 0 && errno != ENOENT) { + free(order); + return 1; + } + if (warm && stale_all && invalidate_workdir_units(scratch) != 0) { + free(order); + return 1; + } int any_failed = 0; for (int i = 0; i < nproducts; i++) if (g->pkg[products[i].root].failed) any_failed = 1; @@ -3275,6 +4081,19 @@ build_one_sep_impl(const char *src, int entry_is_dir, } continue; } + int nmaps = 0; + for (int k = 0; k < g->pkg[pi].bindings.n; k++) { + struct sepbind *b = &g->pkg[pi].bindings.v[k]; + if (b->kind == 'D' && b->dep >= 0 && b->dep < g->n + && strcmp(b->name, g->pkg[b->dep].path) != 0) { + if (nmaps == INT_MAX) { + sep_fail_size(); + free(order); + return 1; + } + nmaps++; + } + } size_t cargvcap = 12; if ((size_t)g->pkg[pi].ndeps > ((size_t)-1 - cargvcap) / 3) { fprintf(stderr, "ww: package graph is too large\n"); @@ -3282,6 +4101,12 @@ build_one_sep_impl(const char *src, int entry_is_dir, return 1; } cargvcap += 3 * (size_t)g->pkg[pi].ndeps; + if ((size_t)nmaps > ((size_t)-1 - cargvcap) / 3) { + fprintf(stderr, "ww: package graph is too large\n"); + free(order); + return 1; + } + cargvcap += 3 * (size_t)nmaps; char **cargv = calloc(cargvcap, sizeof *cargv); char (*importfiles)[SEP_ARTIFACT_MAX] = NULL; if (g->pkg[pi].ndeps > 0) @@ -3325,6 +4150,15 @@ build_one_sep_impl(const char *src, int entry_is_dir, cargv[cpos++] = g->pkg[dj].path; cargv[cpos++] = importfiles[k]; } + for (int k = 0; k < g->pkg[pi].bindings.n; k++) { + struct sepbind *b = &g->pkg[pi].bindings.v[k]; + if (b->kind != 'D' || b->dep < 0 || b->dep >= g->n + || strcmp(b->name, g->pkg[b->dep].path) == 0) + continue; + cargv[cpos++] = "--import-map"; + cargv[cpos++] = b->name; + cargv[cpos++] = g->pkg[b->dep].path; + } cargv[cpos++] = "-I"; cargv[cpos++] = (char *)cw; cargv[cpos++] = "-o"; @@ -3515,6 +4349,10 @@ build_one_sep_impl(const char *src, int entry_is_dir, && pi != variant_root && g->pkg[pi].variant == SEP_VARIANT_PRODUCTION && g->pkg[pi].role != SEP_ROLE_TEST_SUPPORT + && g->pkg[pi].import_base != NULL + && g->pkg[variant_root].import_base != NULL + && strcmp(g->pkg[pi].import_base, + g->pkg[variant_root].import_base) == 0 && strcmp(g->pkg[pi].canon, g->pkg[variant_root].canon) == 0) continue; diff --git a/internal/wwpackage/package.ww b/internal/wwpackage/package.ww index ec384f1f..b76fc154 100644 --- a/internal/wwpackage/package.ww +++ b/internal/wwpackage/package.ww @@ -45,6 +45,7 @@ type pkgplan = struct { identity: str, root: str, workdir: str, + workdircreated: bool, buildout: str, builderr: str, start: i32, @@ -726,15 +727,21 @@ fn pkgsetplanpaths(p: *pkgplan, groups: []pkggroup, root: str, index: i32, p.root = strings.concat(root, "/plan-", num); if (!pkgmakedir(p.root)) { return false; }; p.workdir = ""; + p.workdircreated = false; if (workroot.len != 0) { // Canonical request-directory identity makes equivalent spellings and // product reorderings select one command-scoped driver workdir. p.workdir = strings.concat(workroot, "/", pkgworkkey(p.sourceroot)); - match (os.mkdirs(p.workdir, 448)) { + match (os.mkdirs(workroot, 448)) { case void => void; case let e: os.oserror => return false; }; + let mr: i32 = os.mkdir(p.workdir, 448i32); + if (mr == 0) { p.workdircreated = true; } + else if (mr != -17 || !pkgisdir(p.workdir)) { + return false; + }; }; p.buildout = strings.concat(p.root, "/build.stdout"); p.builderr = strings.concat(p.root, "/build.stderr"); @@ -1466,6 +1473,15 @@ export fn packagecommand(args: []str) int = { i = 0; for (i < plans.len) { failed += pkgemitplan(&plans[i], groups, compileonly); + // The driver commits its tool stamp only after semantic preflight. If + // this request minted the persistent directory and rejection left it + // unstamped, reclaim it only if it is still empty. A concurrent or + // interrupted writer's contents are never recursively removed. + if (plans[i].workdircreated + && !os.exists(strings.concat(plans[i].workdir, "/.wwtool.stamp"))) { + let rr: i32 = os.rmdir(plans[i].workdir); + if (rr != 0 && rr != -2) { failed += 1; }; + }; i += 1; }; if (!pkgremoveall(tmproot)) { diff --git a/selfhost/cmd/w6c/main.ww b/selfhost/cmd/w6c/main.ww index 715f7c3b..53113e9e 100644 --- a/selfhost/cmd/w6c/main.ww +++ b/selfhost/cmd/w6c/main.ww @@ -79,6 +79,31 @@ fn exportownermatches(buf: *u8, n: u64, path: *u8) bool = { return buf[need - 1u64] == 10u8; }; +type importmap = struct { + source: *u8, + path: *u8, + seen: bool, +}; + +fn allocimportmaps(count: i32) ([]importmap | nomem) = { + let value: []importmap = alloc([], count: u64)?; + return value; +}; + +fn allocimportptrs(count: i32) ([]*u8 | nomem) = { + let value: []*u8 = alloc([], count: u64)?; + return value; +}; + +fn importleaf(path: *u8) str = { + let whole: str = pathstr(path); + let (prefix, suffix) = strings.rcut(whole, "."); + // rcut returns (whole, empty) when absent and (prefix, empty) for a + // trailing delimiter. Preserve that distinction to match strrchr. + if (suffix.len != 0 || prefix.len != whole.len) { return suffix; }; + return whole; +}; + export fn main(argc: i32, argv: **u8) i32 = { let src: *u8 = nil; let out: *u8 = nil; @@ -91,11 +116,41 @@ export fn main(argc: i32, argv: **u8) i32 = { let sepmode: i32 = 0i32; // -c: #22 M3 separate-compile / primary- // only codegen (emit imported==0 decls // only; treat `.wwi` deps as external) - let importpaths: []*u8 = alloc([], argc: u64)!; + let pathallocation: ([]*u8 | nomem) = allocimportptrs(argc); + let fileallocation: ([]*u8 | nomem) = allocimportptrs(argc); + let importpaths: []*u8; + let importfiles: []*u8; + match (pathallocation) { + case let value: []*u8 => importpaths = value; + case nomem => { + let m: str = "w6c: out of memory\n"; + os.write(2, m.ptr, m.len: u64); + return 1; + }; + }; + match (fileallocation) { + case let value: []*u8 => importfiles = value; + case nomem => { + let m: str = "w6c: out of memory\n"; + os.write(2, m.ptr, m.len: u64); + return 1; + }; + }; importpaths.len = argc; - let importfiles: []*u8 = alloc([], argc: u64)!; importfiles.len = argc; let nimports: i32 = 0; + let mapallocation: ([]importmap | nomem) = allocimportmaps(argc); + let importmaps: []importmap; + match (mapallocation) { + case let value: []importmap => importmaps = value; + case nomem => { + let m: str = "w6c: out of memory\n"; + os.write(2, m.ptr, m.len: u64); + return 1; + }; + }; + importmaps.len = argc; + let nmaps: i32 = 0; let i: i32 = 1; for (i < argc) { @@ -144,6 +199,17 @@ export fn main(argc: i32, argv: **u8) i32 = { importfiles[nimports] = argv[i + 2]; nimports += 1; i += 2; + } else { if (cstreq(a, "--import-map")) { + if (i + 2 >= argc) { + let m: str = "w6c: --import-map requires source and path\n"; + os.write(2, m.ptr, m.len: u64); + return 2; + }; + importmaps[nmaps].source = argv[i + 1]; + importmaps[nmaps].path = argv[i + 2]; + importmaps[nmaps].seen = false; + nmaps += 1; + i += 2; } else { if (a[0u64] == 45u8) { let m: str = "w6c: unknown flag\n"; os.write(2, m.ptr, m.len: u64); @@ -155,12 +221,12 @@ export fn main(argc: i32, argv: **u8) i32 = { return 2; }; src = a; - }; }; }; }; }; }; }; }; }; }; + }; }; }; }; }; }; }; }; }; }; }; i += 1; }; if (src == nil) { - let m: str = "usage: w6c_ww [-T|--test-package] [--command-package] [--entry] [-c] [-I out.wwi] [--import path dep.wwi]... [-o out.s] file.ww\n"; + let m: str = "usage: w6c_ww [-T|--test-package] [--command-package] [--entry] [-c] [-I out.wwi] [--import path dep.wwi]... [--import-map source path]... [-o out.s] file.ww\n"; os.write(2, m.ptr, m.len: u64); return 2; }; @@ -169,6 +235,11 @@ export fn main(argc: i32, argv: **u8) i32 = { os.write(2, m.ptr, m.len: u64); return 2; }; + if (nmaps > 0 && sepmode == 0) { + let m: str = "w6c: --import-map requires -c\n"; + os.write(2, m.ptr, m.len: u64); + return 2; + }; if ((entrymode != 0 || testpackage != 0 || commandpackage != 0) && sepmode == 0) { let m: str = "w6c: --entry, --test-package, and --command-package require -c\n"; @@ -195,6 +266,43 @@ export fn main(argc: i32, argv: **u8) i32 = { }; importi += 1; }; + let mapi: i32 = 0; + for (mapi < nmaps) { + if (importmaps[mapi].source[0u64] == 0u8 + || importmaps[mapi].path[0u64] == 0u8) { + let m: str = "w6c: --import-map path is empty\n"; + os.write(2, m.ptr, m.len: u64); + return 2; + }; + if (mapi > 0 && strings.compare(pathstr(importmaps[mapi - 1].source), + pathstr(importmaps[mapi].source)) >= 0) { + let m: str = "w6c: --import-map sources must be sorted and unique\n"; + os.write(2, m.ptr, m.len: u64); + return 2; + }; + if (!syntax.streq(importleaf(importmaps[mapi].source), + importleaf(importmaps[mapi].path))) { + let m: str = "w6c: --import-map must preserve import leaf\n"; + os.write(2, m.ptr, m.len: u64); + return 2; + }; + let direct: bool = false; + let directi: i32 = 0; + for (directi < nimports) { + if (cstreq(importmaps[mapi].path, + pathstr(importpaths[directi]))) { + direct = true; + break; + }; + directi += 1; + }; + if (!direct) { + let m: str = "w6c: --import-map target is not a direct import\n"; + os.write(2, m.ptr, m.len: u64); + return 2; + }; + mapi += 1; + }; if (testsupport != nil && (sepmode == 0 || (!cstreq(testsupport, "test") && !cstreq(testsupport, "__wwtest")))) { @@ -275,6 +383,31 @@ export fn main(argc: i32, argv: **u8) i32 = { // `if (l.errs || p.errs) return 1;` — broken AST otherwise reaches // cgen and emits junk asm with a zero exit (silent miscompile). if (l.errs > 0 || ps.errs > 0) { return 1; }; + let use: *node = f.list; + for (use != nil) { + if (use.kind == syntax.nkind.N_USE && use.usepath.len != 0) { + mapi = 0; + for (mapi < nmaps) { + if (syntax.streq(use.usepath, + pathstr(importmaps[mapi].source))) { + use.usepath = pathstr(importmaps[mapi].path); + importmaps[mapi].seen = true; + break; + }; + mapi += 1; + }; + }; + use = use.next; + }; + mapi = 0; + for (mapi < nmaps) { + if (!importmaps[mapi].seen) { + let m: str = "w6c: --import-map source is not in primary input\n"; + os.write(2, m.ptr, m.len: u64); + return 2; + }; + mapi += 1; + }; if (importhead != nil) { importtail.next = f.list; f.list = importhead; diff --git a/selfhost/cmd/ww/main.ww b/selfhost/cmd/ww/main.ww index cefe89b9..72995eea 100644 --- a/selfhost/cmd/ww/main.ww +++ b/selfhost/cmd/ww/main.ww @@ -523,6 +523,7 @@ def SEP_ROLE_TEST_SUPPORT: i32 = 1; def SEP_ROLE_GENERATED_MAIN: i32 = 2; def SEP_TEST_SUPPORT_MODULE: str = "__wwtest"; def SEP_LOAD_INTERNAL: i32 = -3; +def SEP_LOAD_VENDOR: i32 = -4; def SEP_INITIAL_CAP: i32 = 8; def SEP_COUNT_MAX: i32 = 2147483647; @@ -840,7 +841,12 @@ type lflags = struct { type sepbind = struct { kind: u8, name: str, - target: *u8, + dep: i32, +}; + +type sepchild = struct { + pkg: i32, + context: i32, }; type seppkg = struct { @@ -876,6 +882,8 @@ type seppkg = struct { type sepcontext = struct { root: *u8, searchpath: *u8, + route: *u8, + sourceroot: *u8, }; type sepgraph = struct { @@ -953,6 +961,11 @@ fn sepallocbinds(cap: i32) ([]sepbind | nomem) = { return value; }; +fn sepallocchildren(cap: i32) ([]sepchild | nomem) = { + let value: []sepchild = alloc([], cap: u64)?; + return value; +}; + fn sepdupstr(s: str) (str | nomem) = { let out: str; out.ptr = nil; @@ -1265,6 +1278,29 @@ fn sepreservebinds(bindings: *[]sepbind, need: i32) bool = { return true; }; +fn sepreservechildren(children: *[]sepchild, need: i32) bool = { + if (need <= children.cap) { return true; }; + let cap: i32 = sepgrowcap(children.cap, need); + if (cap < 0) { return false; }; + let allocation: ([]sepchild | nomem) = sepallocchildren(cap); + let next: []sepchild; + match (allocation) { + case let value: []sepchild => next = value; + case nomem => { sepfailnomem(); return false; }; + }; + let n: i32 = children.len; + next.len = cap; + let i: i32 = 0; + for (i < n) { next[i] = (*children)[i]; i += 1; }; + next.len = n; + if (children.ptr != nil) { + os.free(children.ptr: *void, + (children.cap: u64) * (size(sepchild): u64)); + }; + *children = next; + return true; +}; + fn sepaddbytes(total: *u64, add: u64) bool = { if (*total > SEP_COUNT_MAX: u64 || add > (SEP_COUNT_MAX: u64) - *total) { @@ -1595,10 +1631,8 @@ fn sepcanonicalinternalowner(path: *u8, parents: u64) *u8 = { return canonicaldir(pathstr(lexical)); }; -fn sepinternalimportallowed(from: *seppkg, targetpath: *u8, - targetentry: *u8) i32 = { - let parents: u64 = 0u64; - if (!sepinternalparentcount(targetpath, &parents)) { return 1; }; +fn sepimporterwithinowner(from: *seppkg, targetentry: *u8, + parents: u64) i32 = { let importer: *u8 = from.canon; if (from.isdir == 0) { importer = seprawimporterdir(from); @@ -1632,6 +1666,79 @@ fn sepinternalimportallowed(from: *seppkg, targetpath: *u8, return 0; }; +fn sepinternalimportallowed(from: *seppkg, targetpath: *u8, + targetentry: *u8) i32 = { + let parents: u64 = 0u64; + if (!sepinternalparentcount(targetpath, &parents)) { return 1; }; + return sepimporterwithinowner(from, targetentry, parents); +}; + +// Find the final exact non-terminal dotted component named vendor. The +// effective suffix remains a view into path; parents removes vendor plus that +// suffix from the current edge's lexical target route to obtain its owner. +fn sepvendorsuffix(path: *u8, suffix: *str, parents: *u64) bool = { + let total: u64 = cstrlen(path); + let p: u64 = 0u64; + let components: u64 = 0u64; + let finalcomponent: u64 = 0u64; + let found: bool = false; + for (p < total) { + let end: u64 = p; + for (end < total && path[end] != '.': u8) { end += 1u64; }; + if (end < total && end + 1u64 < total + && end - p == "vendor".len: u64 + && bytecmp(path + p, end - p, "vendor".ptr, + "vendor".len: u64) == 0) { + suffix.ptr = path + end + 1u64; + suffix.len = (total - end - 1u64): i32; + finalcomponent = components; + found = true; + }; + components += 1u64; + p = end + 1u64; + }; + if (!found) { return false; }; + *parents = components - finalcomponent; + return true; +}; + +// Filesystem twin used only for a directly selected literal external-test +// root, whose ordinary import identity is finalized after source scanning. +fn sepvendorroutesuffix(path: *u8) *u8 = { + let total: u64 = cstrlen(path); + let p: u64 = 0u64; + let final: *u8 = nil; + for (p < total) { + for (p < total && path[p] == '/': u8) { p += 1u64; }; + if (p >= total) { break; }; + let end: u64 = p; + for (end < total && path[end] != '/': u8) { end += 1u64; }; + if (end < total && end + 1u64 < total + && end - p == "vendor".len: u64 + && bytecmp(path + p, end - p, "vendor".ptr, + "vendor".len: u64) == 0) { + final = path + end + 1u64; + }; + p = end + 1u64; + }; + return final; +}; + +fn sepvendorimportallowed(from: *seppkg, targetpath: *u8, + targetentry: *u8) i32 = { + let suffix: str = ""; + let parents: u64 = 0u64; + if (!sepvendorsuffix(targetpath, &suffix, &parents)) { return 1; }; + return sepimporterwithinowner(from, targetentry, parents); +}; + +fn seppathisvendored(path: *u8) bool = { + if (path == nil) { return false; }; + let suffix: str = ""; + let parents: u64 = 0u64; + return sepvendorsuffix(path, &suffix, &parents); +}; + fn sepcommandcompilermarker(g: *sepgraph, pi: i32) bool = { return sepcommanddeclaredname(&g.pkg[pi]) && !g.pkg[pi].linkentry; }; @@ -1670,9 +1777,12 @@ fn sepbindimportbase(g: *sepgraph, pi: i32, base: *u8) i32 = { 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 (!seppathisvendored(g.pkg[i].importbase) + && !seppathisvendored(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)) { @@ -1748,6 +1858,21 @@ fn sepfindoraddvariant(g: *sepgraph, path: *u8, entry: *u8, }; if (samelocation && g.pkg[i].variant == variant && g.pkg[i].role == role) { + if (path[0u64] != 0u8 && seppathisvendored(path) + && g.pkg[i].root && g.pkg[i].importbase == nil) { + i += 1; continue; + }; + if (root && path[0u64] == 0u8 + && g.pkg[i].importbase != nil + && seppathisvendored(g.pkg[i].importbase)) { + i += 1; continue; + }; + if (path[0u64] != 0u8 && g.pkg[i].importbase != nil + && !cstreq(path, g.pkg[i].importbase) + && (seppathisvendored(path) + || seppathisvendored(g.pkg[i].importbase))) { + i += 1; continue; + }; let sametest: bool = testpackage == nil && g.pkg[i].testpackage == nil; if (testpackage != nil && g.pkg[i].testpackage != nil) { @@ -1767,6 +1892,10 @@ fn sepfindoraddvariant(g: *sepgraph, path: *u8, entry: *u8, if (supportalias) { i += 1; continue; }; if (g.pkg[i].importbase != nil && path[0u64] != 0u8 && !cstreq(g.pkg[i].importbase, path)) { + if (seppathisvendored(g.pkg[i].importbase) + || seppathisvendored(path)) { + i += 1; continue; + }; g.identityfailed = true; sepdiagdiridentities(entry, g.pkg[i].importbase, path); return -1; @@ -1840,6 +1969,9 @@ fn sepfindoraddvariant(g: *sepgraph, path: *u8, entry: *u8, && role != SEP_ROLE_TEST_SUPPORT && cstreq(g.pkg[i].canon, canon) && g.pkg[i].importbase != nil) { + if (root && seppathisvendored(g.pkg[i].importbase)) { + i += 1; continue; + }; inherited = g.pkg[i].importbase; break; }; @@ -1915,10 +2047,22 @@ fn sepgraphfree(g: *sepgraph) void = { }; i = 0; for (i < g.ncontext) { + if (g.context[i].root != nil) { + os.free(g.context[i].root: *void, + cstrlen(g.context[i].root) + 1u64); + }; if (g.context[i].searchpath != nil) { os.free(g.context[i].searchpath: *void, cstrlen(g.context[i].searchpath) + 1u64); }; + if (g.context[i].route != nil) { + os.free(g.context[i].route: *void, + cstrlen(g.context[i].route) + 1u64); + }; + if (g.context[i].sourceroot != nil) { + os.free(g.context[i].sourceroot: *void, + cstrlen(g.context[i].sourceroot) + 1u64); + }; i += 1; }; if (g.context.ptr != nil) { @@ -1928,8 +2072,261 @@ fn sepgraphfree(g: *sepgraph) void = { os.free(g: *void, size(sepgraph): u64); }; +type seplocated = struct { + entry: *u8, + root: *u8, +}; + +fn septrimmedpath(path: *u8) *u8 = { + let n: u64 = cstrlen(path); + for (n > 1u64 && path[n - 1u64] == '/': u8) { n -= 1u64; }; + if (n == 0u64) { return sepdupcstr(".".ptr, 1u64); }; + return sepdupcstr(path, n); +}; + +fn seplexicalparent(path: *u8) *u8 = { + let n: u64 = cstrlen(path); + for (n > 1u64 && path[n - 1u64] == '/': u8) { n -= 1u64; }; + let slash: u64 = n; + for (slash > 0u64 && path[slash - 1u64] != '/': u8) { + slash -= 1u64; + }; + if (slash == 0u64) { return sepdupcstr(".".ptr, 1u64); }; + let parent: u64 = slash - 1u64; + for (parent > 1u64 && path[parent - 1u64] == '/': u8) { + parent -= 1u64; + }; + if (parent == 0u64) { parent = 1u64; }; + return sepdupcstr(path, parent); +}; + +fn sepjoinroute(root: *u8, rel: *u8) *u8 = { + let n: u64 = cstrlen(root); + if (n > 0u64 && root[n - 1u64] == '/': u8) { + return sepappendlit(root, pathstr(rel)); + }; + return sepjoinpath(root, rel); +}; + +fn seplexicalrelative(root: *u8, route: *u8, rel: *str) bool = { + let rn: u64 = cstrlen(root); + let pn: u64 = cstrlen(route); + for (rn > 1u64 && root[rn - 1u64] == '/': u8) { rn -= 1u64; }; + for (pn > 1u64 && route[pn - 1u64] == '/': u8) { pn -= 1u64; }; + if (rn == 1u64 && root[0u64] == '/': u8) { + if (pn > 1u64 && route[0u64] == '/': u8) { + rel.ptr = route + 1u64; + rel.len = (pn - 1u64): i32; + return true; + }; + return false; + }; + if (rn == 1u64 && root[0u64] == '.': u8 && pn > 2u64 + && route[0u64] == '.': u8 && route[1u64] == '/': u8) { + rel.ptr = route + 2u64; + rel.len = (pn - 2u64): i32; + return true; + }; + if (pn > rn && bytecmp(route, rn, root, rn) == 0 + && route[rn] == '/': u8) { + rel.ptr = route + rn + 1u64; + rel.len = (pn - rn - 1u64): i32; + return true; + }; + return false; +}; + +fn sepsamecanonicaldir(a: *u8, b: *u8) i32 = { + let ac: *u8 = canonicaldir(pathstr(a)); + let bc: *u8 = canonicaldir(pathstr(b)); + if (ac == nil || bc == nil) { + if (sepfatalallocation) { return -1; }; + return 0; + }; + if (cstreq(ac, bc)) { return 1; }; + return 0; +}; + +fn sepcheckedimportpathform(name: *u8) *u8 = { + let n: u64 = cstrlen(name); + let out: []u8; + if (!sepmakebytes(n + 1u64, &out)) { return nil; }; + let i: u64 = 0u64; + for (i < n) { + if (name[i] == '.': u8) { out[i] = '/': u8; } + else { out[i] = name[i]; }; + i += 1u64; + }; + out[n] = 0u8; + return out.ptr; +}; + +fn seplocateimportroot(dirs: *u8, pathform: *u8) seplocated = { + let result: seplocated; + result.entry = nil; + result.root = nil; + let total: u64 = cstrlen(dirs); + let p: u64 = 0u64; + for (p < total) { + let q: u64 = p; + for (q < total && dirs[q] != ':': u8) { q += 1u64; }; + let n: u64 = q - p; + if (n > 0u64) { + let root: *u8 = sepdupcstr(dirs + p, n); + if (root == nil) { return result; }; + let entry: *u8 = sepjoinroute(root, pathform); + if (entry == nil) { return result; }; + let fi: os.filestat; + match (os.stat(&fi, pathstr(entry))) { + case void => { + let typ: u32 = (fi.mode: u32) & 61440u32; + if (typ == os.mode.DIR: u32) { + result.entry = entry; + result.root = root; + return result; + }; + }; + case let e: os.oserror => void; + }; + }; + p = q + 1u64; + }; + return result; +}; + +fn sepinitialrouteroot(entry: *u8, identity: *u8, searchpath: *u8, + routeout: **u8, rootout: **u8) i32 = { + let entrytrim: *u8 = septrimmedpath(entry); + if (entrytrim == nil) { return -1; }; + if (identity != nil && identity[0u64] != 0u8) { + let root: *u8 = sepdupcstr(entrytrim, cstrlen(entrytrim)); + if (root == nil) { return -1; }; + let components: u64 = 1u64; + let ii: u64 = 0u64; + for (ii < cstrlen(identity)) { + if (identity[ii] == '.': u8) { components += 1u64; }; + ii += 1u64; + }; + let ci: u64 = 0u64; + for (ci < components) { + root = seplexicalparent(root); + if (root == nil) { return -1; }; + ci += 1u64; + }; + let pathform: *u8 = sepcheckedimportpathform(identity); + if (pathform == nil) { return -1; }; + let route: *u8 = sepjoinroute(root, pathform); + if (route == nil) { return -1; }; + let same: i32 = sepsamecanonicaldir(route, entrytrim); + if (same <= 0) { + if (same == 0) { + cerr("ww: package "); cerr(pathstr(identity)); + cerr(" does not match resolved directory "); + cerr(pathstr(entry)); cerr("\n"); + }; + return -1; + }; + *routeout = route; + *rootout = root; + return 0; + }; + + let entrycanon: *u8 = canonicaldir(pathstr(entrytrim)); + if (entrycanon == nil && sepfatalallocation) { return -1; }; + let total: u64 = cstrlen(searchpath); + let p: u64 = 0u64; + for (p < total) { + let q: u64 = p; + for (q < total && searchpath[q] != ':': u8) { q += 1u64; }; + let n: u64 = q - p; + if (n > 0u64) { + let candidate: *u8 = sepdupcstr(searchpath + p, n); + if (candidate == nil) { return -1; }; + let rel: str = ""; + let relative: bool = seplexicalrelative(candidate, + entrytrim, &rel); + if (!relative && entrycanon != nil) { + let canonroot: *u8 = canonicaldir(pathstr(candidate)); + if (canonroot == nil && sepfatalallocation) { return -1; }; + if (canonroot != nil) { + let rn: u64 = cstrlen(canonroot); + let en: u64 = cstrlen(entrycanon); + if (rn == 1u64 && canonroot[0u64] == '/': u8 + && en > 1u64 && entrycanon[0u64] == '/': u8) { + rel.ptr = entrycanon + 1u64; + rel.len = (en - 1u64): i32; + relative = true; + } else { if (en > rn + && bytecmp(entrycanon, rn, canonroot, rn) == 0 + && entrycanon[rn] == '/': u8) { + rel.ptr = entrycanon + rn + 1u64; + rel.len = (en - rn - 1u64): i32; + relative = true; + }; }; + }; + }; + if (relative && rel.len > 0) { + let ident: []u8; + if (!sepmakebytes((rel.len + 1): u64, &ident)) { return -1; }; + let valid: i32 = sepimportpathfromrelative(rel.ptr, + ident.ptr, (rel.len + 1): u64); + if (valid > 0 && !reservedimportpath(ident.ptr)) { + let selected: seplocated = seplocateimportroot( + searchpath, rel.ptr); + if (selected.entry == nil && sepfatalallocation) { + return -1; + }; + if (selected.entry != nil) { + let same: i32 = sepsamecanonicaldir( + selected.entry, entrytrim); + if (same < 0) { return -1; }; + if (same > 0) { + let route: *u8 = sepjoinroute(candidate, rel.ptr); + if (route == nil) { return -1; }; + *routeout = route; + *rootout = candidate; + return 0; + }; + }; + }; + }; + }; + p = q + 1u64; + }; + *routeout = entrytrim; + *rootout = sepdupcstr(entrytrim, cstrlen(entrytrim)); + if (*rootout == nil) { return -1; }; + return 0; +}; + +fn sepcontextadd(g: *sepgraph, root: *u8, searchpath: *u8, + route: *u8, sourceroot: *u8) i32 = { + let i: i32 = 0; + for (i < g.ncontext) { + if (cstreq(g.context[i].root, root) + && cstreq(g.context[i].searchpath, searchpath) + && cstreq(g.context[i].route, route) + && cstreq(g.context[i].sourceroot, sourceroot)) { return i; }; + i += 1; + }; + if (g.ncontext == SEP_COUNT_MAX) { sepfailsize(); return -1; }; + if (!sepreservecontexts(g, g.ncontext + 1)) { return -1; }; + g.context[g.ncontext].root = sepdupcstr(root, cstrlen(root)); + g.context[g.ncontext].searchpath = sepdupcstr(searchpath, + cstrlen(searchpath)); + g.context[g.ncontext].route = septrimmedpath(route); + g.context[g.ncontext].sourceroot = septrimmedpath(sourceroot); + if (g.context[g.ncontext].root == nil + || g.context[g.ncontext].searchpath == nil + || g.context[g.ncontext].route == nil + || g.context[g.ncontext].sourceroot == nil) { return -1; }; + let result: i32 = g.ncontext; + g.ncontext += 1; + return result; +}; + fn sepcontextfor(g: *sepgraph, root: *u8, incs: *u8, - toolsrcdir: *u8) i32 = { + toolsrcdir: *u8, identity: *u8) i32 = { let need: u64 = 0u64; if (!sepaddbytes(&need, cstrlen(root)) || !sepaddbytes(&need, 1u64) @@ -1949,24 +2346,18 @@ fn sepcontextfor(g: *sepgraph, root: *u8, incs: *u8, }; 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)) { - os.free(search.ptr: *void, - (search.cap: u64) * (size(u8): u64)); - return i; - }; - i += 1; - }; - if (g.ncontext == SEP_COUNT_MAX) { sepfailsize(); return -1; }; - if (!sepreservecontexts(g, g.ncontext + 1)) { - 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; + let route: *u8 = nil; + let sourceroot: *u8 = nil; + if (sepinitialrouteroot(root, identity, search.ptr, + &route, &sourceroot) < 0) { return -1; }; + return sepcontextadd(g, root, search.ptr, route, sourceroot); +}; + +fn sepchildcontextfor(g: *sepgraph, parent: i32, route: *u8, + sourceroot: *u8) i32 = { + if (parent < 0 || parent >= g.ncontext) { return -1; }; + return sepcontextadd(g, g.context[parent].root, + g.context[parent].searchpath, route, sourceroot); }; def SEP_NAME_MAX: u64 = 255u64; @@ -2140,17 +2531,12 @@ fn sepexternalname(pkg: *seppkg, path: *u8, n: u64, }; fn sepbindadd(bindings: *[]sepbind, kind: u8, name: str, - target: *u8) bool = { + dep: i32) bool = { 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 true; }; - if (target != nil && b.target != nil - && os.samefile(pathstr(target), pathstr(b.target))) { - return true; - }; - }; + if (b.kind == kind && b.dep == dep + && syntax.streq(b.name, name)) { return true; }; i += 1; }; if (bindings.len == SEP_COUNT_MAX) { sepfailsize(); return false; }; @@ -2166,43 +2552,190 @@ fn sepbindadd(bindings: *[]sepbind, kind: u8, name: str, append(*bindings, sepbind { kind = kind, name = copied, - target = target, + dep = dep, }); return true; }; +fn sepbindcmp(a: sepbind, b: sepbind) i32 = { + let r: i32 = strings.compare(a.name, b.name): i32; + if (r != 0) { return r; }; + if (a.kind < b.kind) { return -1; }; + if (a.kind > b.kind) { return 1; }; + if (a.dep < b.dep) { return -1; }; + if (a.dep > b.dep) { return 1; }; + return 0; +}; + +fn sepbindsort(bindings: *[]sepbind) void = { + let i: i32 = 1; + for (i < bindings.len) { + let value: sepbind = (*bindings)[i]; + let j: i32 = i; + for (j > 0 && sepbindcmp((*bindings)[j - 1], value) > 0) { + (*bindings)[j] = (*bindings)[j - 1]; + j -= 1; + }; + (*bindings)[j] = value; + i += 1; + }; +}; + 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; }; + if (a[i].kind != b[i].kind || a[i].dep != b[i].dep + || !syntax.streq(a[i].name, b[i].name)) { return false; }; i += 1; }; return true; }; +fn sepchildrenadd(children: *[]sepchild, pkg: i32, context: i32) bool = { + let i: i32 = 0; + for (i < children.len) { + if ((*children)[i].pkg == pkg + && (*children)[i].context == context) { return true; }; + i += 1; + }; + if (children.len == SEP_COUNT_MAX) { sepfailsize(); return false; }; + if (!sepreservechildren(children, children.len + 1)) { return false; }; + append(*children, sepchild { pkg = pkg, context = context }); + return true; +}; + +type sepresolved = struct { + identity: *u8, + entry: *u8, + sourceroot: *u8, + vendored: bool, +}; + +// Return 1 only after observing a non-directory source suffix, 0 for a +// non-candidate (including lookup/read failure), and -1 for loader allocation +// failure that must abort graph discovery before persistent state or tools. +fn sepvendorsourcecandidate(candidate: *u8) i32 = { + let fi: os.filestat; + match (os.stat(&fi, pathstr(candidate))) { + case let e: os.oserror => return 0; + case void => void; + }; + let typ: u32 = (fi.mode: u32) & 61440u32; + if (typ != os.mode.DIR: u32) { return 0; }; + let fd: i32 = os.open(pathstr(candidate), os.flag.RDONLY, 0i32); + if (fd < 0) { return 0; }; + let buf: []u8; + if (!sepmakebytes(8192u64, &buf)) { os.close(fd); return -1; }; + let r: i64 = os.getdents64(fd, buf.ptr, 8192u64); + for (r > 0i64) { + let off: u64 = 0u64; + let ru: u64 = r: u64; + for (off < ru) { + let reclen: u64 = (buf[off + 16u64]: u64) + + (buf[off + 17u64]: u64) * 256u64; + let name: *u8 = buf.ptr + off + 19u64; + let n: u64 = cstrlen(name); + if (n >= 3u64 && name[n - 3u64] == '.': u8 + && name[n - 2u64] == 'w': u8 + && name[n - 1u64] == 'w': u8) { + let entry: *u8 = sepjoinroute(candidate, name); + if (entry == nil) { os.close(fd); return -1; }; + let ent: os.filestat; + let directory: bool = false; + match (os.lstat(&ent, pathstr(entry))) { + case void => directory = ((ent.mode: u32) & 61440u32) + == os.mode.DIR: u32; + case let e: os.oserror => void; + }; + if (!directory) { os.close(fd); return 1; }; + }; + off += reclen; + }; + r = os.getdents64(fd, buf.ptr, 8192u64); + }; + os.close(fd); + return 0; +}; + +fn sepresolvesourceimport(g: *sepgraph, context: i32, name: *u8, + pathform: *u8, out: *sepresolved) i32 = { + out.identity = nil; + out.entry = nil; + out.sourceroot = nil; + out.vendored = false; + if (context < 0 || context >= g.ncontext) { return -1; }; + let route: *u8 = g.context[context].route; + let sourceroot: *u8 = g.context[context].sourceroot; + let ignored: str = ""; + if (!cstreq(route, sourceroot) + && !seplexicalrelative(sourceroot, route, &ignored)) { + cerr("ww: package route "); cerr(pathstr(route)); + cerr(" is outside source root "); cerr(pathstr(sourceroot)); + cerr("\n"); + return -1; + }; + let directsuffix: str = ""; + let directparents: u64 = 0u64; + let directexpanded: bool = sepvendorsuffix(name, &directsuffix, + &directparents); + let ancestor: *u8 = nil; + if (!directexpanded) { ancestor = septrimmedpath(route); }; + if (!directexpanded && ancestor == nil) { return -1; }; + for (!directexpanded) { + let vendordir: *u8 = sepjoinroute(ancestor, "vendor\0".ptr); + if (vendordir == nil) { return -1; }; + let candidate: *u8 = sepjoinroute(vendordir, pathform); + if (candidate == nil) { return -1; }; + let sourcecandidate: i32 = sepvendorsourcecandidate(candidate); + if (sourcecandidate < 0) { return -1; }; + if (sourcecandidate > 0) { + let rel: str = ""; + if (!seplexicalrelative(sourceroot, candidate, &rel)) { + return -1; + }; + let identity: []u8; + if (!sepmakebytes((rel.len + 1): u64, &identity)) { return -1; }; + let valid: i32 = sepimportpathfromrelative(rel.ptr, + identity.ptr, (rel.len + 1): u64); + if (valid <= 0 || reservedimportpath(identity.ptr)) { + if (valid == 0) { + cerr("ww: invalid vendored package path "); + cerr(rel); cerr("\n"); + }; + return -1; + }; + out.identity = identity.ptr; + out.entry = candidate; + out.sourceroot = sepdupcstr(sourceroot, cstrlen(sourceroot)); + if (out.sourceroot == nil) { return -1; }; + out.vendored = true; + return 1; + }; + if (cstreq(ancestor, sourceroot)) { break; }; + let parent: *u8 = seplexicalparent(ancestor); + if (parent == nil || cstreq(parent, ancestor)) { return -1; }; + ancestor = parent; + }; + let located: seplocated = seplocateimportroot( + g.context[context].searchpath, pathform); + if (located.entry == nil && sepfatalallocation) { return -1; }; + if (located.entry == nil) { return 0; }; + out.identity = sepdupcstr(name, cstrlen(name)); + out.entry = located.entry; + out.sourceroot = located.root; + if (out.identity == nil) { return -1; }; + return 1; +}; + // 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, bindings: *[]sepbind, ownedsource: i32) i32 = { +fn sepscanfile(g: *sepgraph, pi: i32, file: *u8, context: i32, + fv: *expctx, bindings: *[]sepbind, children: *[]sepchild, + ownedsource: i32) i32 = { let fview: str; fview.ptr = file; fview.len = cstrlen(file): i32; @@ -2322,22 +2855,59 @@ fn sepscanfile(g: *sepgraph, pi: i32, file: *u8, searchpath: *u8, cerr(u.usepath); cerr(" is reserved\n"); return -1; }; - let externalproduction: bool = false; - let ipath: *u8 = nil; - let visibilityentry: *u8 = nil; - if (g.pkg[pi].variant == SEP_VARIANT_EXTERNAL - && g.pkg[pi].importbase != nil - && cstrlen(g.pkg[pi].importbase) == idn - && bytecmp(g.pkg[pi].importbase, idn, idp, idn) == 0) { - ipath = g.pkg[pi].canon; - visibilityentry = g.pkg[pi].entry; + let nm: []u8; + if (!sepmakebytes(idn + 1u64, &nm)) { return -1; }; + let k: u64 = 0u64; + for (k < idn) { nm[k] = idp[k]; k += 1u64; }; + nm[idn] = 0u8; + let pathform: *u8 = sepcheckedimportpathform(nm.ptr); + if (pathform == nil) { return -1; }; + let resolved: sepresolved; + resolved.identity = nil; + resolved.entry = nil; + resolved.sourceroot = nil; + resolved.vendored = false; + let located: i32 = sepresolvesourceimport(g, context, nm.ptr, + pathform, &resolved); + // A directly selected literal external root gets a colocated + // production fallback only after ordinary source resolution misses. + if (located == 0 && g.pkg[pi].variant == SEP_VARIANT_EXTERNAL) { + let routesuffix: *u8 = sepvendorroutesuffix( + g.context[context].route); + let ordinaryleaf: bool = true; + let oi: u64 = 0u64; + for (oi < idn) { + if (idp[oi] == '.': u8) { ordinaryleaf = false; }; + oi += 1u64; + }; + let literalself: bool = routesuffix != nil + && cstreq(pathform, routesuffix) + && sepexternalname(&g.pkg[pi], idp, idn, true); + if (routesuffix == nil) { + literalself = ordinaryleaf + && sepexternalname(&g.pkg[pi], idp, idn, false); + }; + if (literalself) { + if (g.pkg[pi].importbase != nil) { + resolved.identity = sepdupcstr(g.pkg[pi].importbase, + cstrlen(g.pkg[pi].importbase)); + } else { + resolved.identity = seplocalimportbase(&g.pkg[pi]); + }; + resolved.entry = sepdupcstr(g.context[context].route, + cstrlen(g.context[context].route)); + resolved.sourceroot = sepdupcstr( + g.context[context].sourceroot, + cstrlen(g.context[context].sourceroot)); + if (resolved.identity == nil || resolved.entry == nil + || resolved.sourceroot == nil) { return -1; }; + located = 1; + }; }; - if (ipath == nil) { - ipath = locateimport(searchpath, idp, idn); - visibilityentry = ipath; - }; - if (ipath != nil) { - let self: bool = os.samefile(pathstr(ipath), + if (located < 0) { return -1; }; + if (located > 0) { + let externalproduction: bool = false; + let self: bool = os.samefile(pathstr(resolved.entry), pathstr(g.pkg[pi].entry)); if (self && (sepexternalname(&g.pkg[pi], idp, idn, true) || (g.pkg[pi].variant == SEP_VARIANT_EXTERNAL @@ -2353,28 +2923,46 @@ fn sepscanfile(g: *sepgraph, pi: i32, file: *u8, searchpath: *u8, cerr("' cannot import itself\n"); return -1; }; - let nm: []u8; - if (!sepmakebytes(idn + 1u64, &nm)) { return -1; }; - let k: u64 = 0u64; - for (k < idn) { nm[k] = idp[k]; k += 1u64; }; - nm[idn] = 0u8; // 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); + let di: i32 = sepfindoradd(g, resolved.identity, + resolved.entry, 1); if (di < 0) { return -1; }; let allowed: i32 = sepinternalimportallowed(&g.pkg[pi], - g.pkg[di].path, visibilityentry); + resolved.identity, resolved.entry); if (allowed < 0) { return -1; }; if (allowed == 0) { cerrpos(u.file, u.line, u.col); cerr(": error: use of internal package "); - cerr(pathstr(g.pkg[di].path)); cerr(" not allowed\n"); + cerr(pathstr(resolved.identity)); cerr(" not allowed\n"); return SEP_LOAD_INTERNAL; }; - if (!sepbindadd(bindings, 'D': u8, u.usepath, ipath)) { + allowed = sepvendorimportallowed(&g.pkg[pi], + resolved.identity, resolved.entry); + if (allowed < 0) { return -1; }; + if (allowed == 0) { + cerrpos(u.file, u.line, u.col); + cerr(": error: use of vendored package not allowed\n"); + return SEP_LOAD_VENDOR; + }; + let suffix: str = ""; + let parents: u64 = 0u64; + if (sepvendorsuffix(resolved.identity, &suffix, &parents) + && bytecmp(nm.ptr, idn, suffix.ptr, + suffix.len: u64) != 0) { + cerrpos(u.file, u.line, u.col); + cerr(": error: "); cerr(pathstr(resolved.identity)); + cerr(" must be imported as "); cerr(suffix); cerr("\n"); + return SEP_LOAD_VENDOR; + }; + let childcontext: i32 = sepchildcontextfor(g, context, + resolved.entry, resolved.sourceroot); + if (childcontext < 0 + || !sepbindadd(bindings, 'D': u8, u.usepath, di) + || !sepadddep(g, pi, di) + || !sepchildrenadd(children, di, childcontext)) { return -1; }; - if (!sepadddep(g, pi, di)) { return -1; }; } else { let lstart: u64 = 0u64; let lk: u64 = 0u64; @@ -2400,7 +2988,7 @@ fn sepscanfile(g: *sepgraph, pi: i32, file: *u8, searchpath: *u8, cerr("\n"); return -1; } else { - if (!sepbindadd(bindings, 'I': u8, u.usepath, nil)) { + if (!sepbindadd(bindings, 'I': u8, u.usepath, -1)) { return -1; }; }; @@ -2555,7 +3143,8 @@ fn sepaddgeneratedmain(g: *sepgraph, product: *sepproduct, ordinal: i32, // Load one action's owned sources and direct bindings. Dependency descent is // iterative below so a valid deep graph is not limited by the native stack. -fn seppreparepkgcontext(g: *sepgraph, pi: i32, context: i32) i32 = { +fn seppreparepkgcontext(g: *sepgraph, pi: i32, context: i32, + children: *[]sepchild) i32 = { if (!sepsetcontextstate(&g.pkg[pi], context, 1u8)) { return -1; }; let searchpath: *u8 = g.context[context].searchpath; let fv: expctx; @@ -2594,7 +3183,7 @@ fn seppreparepkgcontext(g: *sepgraph, pi: i32, context: i32) i32 = { for (i < g.pkg[pi].nsources) { if (rc == 0) { rc = sepscanfile(g, pi, g.pkg[pi].sources[i], - searchpath, &fv, &bindings, 1); + context, &fv, &bindings, children, 1); }; i += 1; }; @@ -2618,20 +3207,26 @@ fn seppreparepkgcontext(g: *sepgraph, pi: i32, context: i32) i32 = { }; }; } else { if (rc == 0) { - rc = sepscanfile(g, pi, g.pkg[pi].entry, searchpath, - &fv, &bindings, 0); + rc = sepscanfile(g, pi, g.pkg[pi].entry, context, + &fv, &bindings, children, 0); }; }; + sepbindsort(&bindings); 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)) { + let first: *u8 = g.context[g.pkg[pi].emitcontext].root; + let second: *u8 = g.context[context].root; + if (strings.compare(pathstr(first), pathstr(second)) > 0) { + let swap: *u8 = first; first = second; second = swap; + }; cerr("ww: package "); if (g.pkg[pi].path[0u64] != 0u8) { cerr(pathstr(g.pkg[pi].path)); } else { cerr(pathstr(g.pkg[pi].canon)); }; cerr(" resolves imports differently in "); - cerr(pathstr(g.context[g.pkg[pi].emitcontext].root)); - cerr(" and "); cerr(pathstr(g.context[context].root)); cerr("\n"); + cerr(pathstr(first)); + cerr(" and "); cerr(pathstr(second)); cerr("\n"); rc = -1; }; }; if (rc < 0) { @@ -2658,8 +3253,9 @@ fn seppreparepkgcontext(g: *sepgraph, pi: i32, context: i32) i32 = { type seploadframe = struct { pkg: i32, context: i32, - nextdep: i32, + nextchild: i32, pendingdep: i32, + children: []sepchild, }; fn sepallocloadframes(cap: i32) ([]seploadframe | nomem) = { @@ -2697,6 +3293,15 @@ fn sepfinishloadframes(frames: []seploadframe, result: i32) i32 = { return result; }; +fn sepclearchildren(children: *[]sepchild) void = { + if (children.ptr != nil) { + os.free(children.ptr: *void, + (children.cap: u64) * (size(sepchild): u64)); + }; + let empty: []sepchild; + *children = empty; +}; + // Load pi once: a directory node takes ownership of its sorted production // paths, then every selected-root context verifies the same canonical import // bindings before the package is compiled once. @@ -2711,34 +3316,41 @@ fn seploadpkg(g: *sepgraph, pi: i32, context: i32) i32 = { if (!sepreserveloadframes(&frames, nframe, 1)) { return -2; }; frames[0].pkg = pi; frames[0].context = context; - frames[0].nextdep = -1; + frames[0].nextchild = -1; frames[0].pendingdep = -1; + let rootchildren: []sepchild; + frames[0].children = rootchildren; nframe = 1; for (nframe > 0) { let f: *seploadframe = &frames[nframe - 1]; - if (f.nextdep < 0) { + if (f.nextchild < 0) { let state: u8 = sepcontextstate(&g.pkg[f.pkg], f.context); if (state == 2u8) { if (g.pkg[f.pkg].failed) { let fi: i32 = 0; for (fi < nframe) { g.pkg[frames[fi].pkg].failed = true; + sepclearchildren(&frames[fi].children); fi += 1; }; return sepfinishloadframes(frames, -1); }; + sepclearchildren(&f.children); nframe -= 1; continue; }; if (state == 1u8) { + sepclearchildren(&f.children); nframe -= 1; continue; }; - let prepared: i32 = seppreparepkgcontext(g, f.pkg, f.context); + let prepared: i32 = seppreparepkgcontext(g, f.pkg, + f.context, &f.children); if (prepared < 0) { let fi: i32 = 0; for (fi < nframe) { g.pkg[frames[fi].pkg].failed = true; + sepclearchildren(&frames[fi].children); fi += 1; }; if (sepfatalallocation) { @@ -2746,7 +3358,7 @@ fn seploadpkg(g: *sepgraph, pi: i32, context: i32) i32 = { }; return sepfinishloadframes(frames, prepared); }; - f.nextdep = 0; + f.nextchild = 0; }; if (f.pendingdep >= 0) { let dep: i32 = f.pendingdep; @@ -2760,19 +3372,22 @@ fn seploadpkg(g: *sepgraph, pi: i32, context: i32) i32 = { let fi: i32 = 0; for (fi < nframe) { g.pkg[frames[fi].pkg].failed = true; + sepclearchildren(&frames[fi].children); fi += 1; }; return sepfinishloadframes(frames, -1); }; }; - if (f.nextdep >= g.pkg[f.pkg].ndeps) { + if (f.nextchild >= f.children.len) { + sepclearchildren(&f.children); nframe -= 1; continue; }; - let dep: i32 = g.pkg[f.pkg].deps[f.nextdep]; - f.nextdep += 1; + let child: sepchild = f.children[f.nextchild]; + f.nextchild += 1; + let dep: i32 = child.pkg; f.pendingdep = dep; - let childcontext: i32 = f.context; + let childcontext: i32 = child.context; if (g.pkg[dep].testsupport && g.supportcontext >= 0) { childcontext = g.supportcontext; }; @@ -2781,6 +3396,7 @@ fn seploadpkg(g: *sepgraph, pi: i32, context: i32) i32 = { let fi: i32 = 0; for (fi < nframe) { g.pkg[frames[fi].pkg].failed = true; + sepclearchildren(&frames[fi].children); fi += 1; }; return sepfinishloadframes(frames, -2); @@ -2789,14 +3405,17 @@ fn seploadpkg(g: *sepgraph, pi: i32, context: i32) i32 = { let fi: i32 = 0; for (fi < nframe) { g.pkg[frames[fi].pkg].failed = true; + sepclearchildren(&frames[fi].children); fi += 1; }; return sepfinishloadframes(frames, -2); }; frames[nframe].pkg = dep; frames[nframe].context = childcontext; - frames[nframe].nextdep = -1; + frames[nframe].nextchild = -1; frames[nframe].pendingdep = -1; + let childchildren: []sepchild; + frames[nframe].children = childchildren; nframe += 1; }; return sepfinishloadframes(frames, 0); @@ -2845,6 +3464,35 @@ fn sepimportpathfromrelative(rel: *u8, out: *u8, outsz: u64) i32 = { return 1; }; +// Bind a literal directory's complete lexical identity before a source edge +// can reach the same physical directory under another vendored route. +fn sepcontextimportbase(g: *sepgraph, context: i32, out: **u8) i32 = { + *out = nil; + if (context < 0 || context >= g.ncontext) { return -1; }; + let c: *sepcontext = &g.context[context]; + if (cstreq(c.route, c.sourceroot)) { return 0; }; + let rel: str = ""; + if (!seplexicalrelative(c.sourceroot, c.route, &rel) + || rel.len == 0) { + cerr("ww: package route "); cerr(pathstr(c.route)); + cerr(" is outside source root "); cerr(pathstr(c.sourceroot)); + cerr("\n"); + return -1; + }; + let base: []u8; + if (!sepmakebytes((rel.len + 1): u64, &base)) { return -1; }; + let converted: i32 = sepimportpathfromrelative(rel.ptr, base.ptr, + (rel.len + 1): u64); + if (converted <= 0 || reservedimportpath(base.ptr)) { + if (converted == 0) { + cerr("ww: invalid package path "); cerr(rel); cerr("\n"); + }; + return -1; + }; + *out = base.ptr; + return 1; +}; + // A reverse candidate is authoritative only when the ordinary ordered lookup // selects this exact canonical directory. Later or nested roots therefore // cannot manufacture an alias hidden by an earlier source root. @@ -3005,6 +3653,9 @@ fn sepfinalizedirectoryidentities(g: *sepgraph) i32 = { && p.role != SEP_ROLE_TEST_SUPPORT && cstreq(g.pkg[i].canon, p.canon) && g.pkg[i].importbase != nil) { + if (p.root && seppathisvendored(g.pkg[i].importbase)) { + i += 1; continue; + }; base = g.pkg[i].importbase; break; }; @@ -3242,6 +3893,23 @@ fn sepemitbody(fd: i32, path: *u8, modpath: *u8) i32 = { return 0; }; +// Filesystem paths are opaque bytes. Hex encoding keeps their persistent +// identity inside one ignored comment even when a legal name contains '\n'. +fn sepwritehex(fd: i32, value: *u8) bool = { + let digits: str = "0123456789abcdef"; + let pair: [2]u8; + let i: u64 = 0u64; + for (value[i] != 0u8) { + let high: i32 = (value[i] / 16u8): i32; + let low: i32 = (value[i] % 16u8): i32; + pair[0] = digits[high]; + pair[1] = digits[low]; + if (!sepwriteall(fd, pair.ptr, 2u64)) { return false; }; + i += 1u64; + }; + return true; +}; + // Compose pi's sep-unit from only pi's byte-sorted sources. Direct exports are // separate compiler inputs; the linker retains the reachable archive closure. fn sepcomposeunit(g: *sepgraph, pi: i32, unitf: *u8) i32 = { @@ -3282,6 +3950,34 @@ fn sepcomposeunit(g: *sepgraph, pi: i32, unitf: *u8) i32 = { } else { bodyrc = sepemitbody(u, g.pkg[pi].entry, g.pkg[pi].path); }; }; + let ownsuffix: str = ""; + let ownparents: u64 = 0u64; + if (bodyrc == 0 && sepvendorsuffix(g.pkg[pi].path, + &ownsuffix, &ownparents)) { + let pre: str = "//ww:vendor-dir "; + if (!sepwriteall(u, pre.ptr, pre.len: u64) + || !sepwritehex(u, g.pkg[pi].canon) + || !sepwriteall(u, "\n".ptr, 1u64)) { bodyrc = -1; }; + }; + let bi: i32 = 0; + for (bi < g.pkg[pi].bindings.len && bodyrc == 0) { + let b: sepbind = g.pkg[pi].bindings[bi]; + if (b.kind == 'D': u8 && b.dep >= 0 && b.dep < g.n + && !syntax.streq(b.name, pathstr(g.pkg[b.dep].path))) { + let pre: str = "//ww:import-map "; + let space: str = " "; + let newline: str = "\n"; + if (!sepwriteall(u, pre.ptr, pre.len: u64) + || !sepwriteall(u, b.name.ptr, b.name.len: u64) + || !sepwriteall(u, space.ptr, 1u64) + || !sepwriteall(u, g.pkg[b.dep].path, + cstrlen(g.pkg[b.dep].path)) + || !sepwriteall(u, space.ptr, 1u64) + || !sepwritehex(u, g.pkg[b.dep].canon) + || !sepwriteall(u, newline.ptr, 1u64)) { bodyrc = -1; }; + }; + bi += 1; + }; if (os.close(u) != 0) { cerr("ww: cannot close package unit\n"); return -1; @@ -3575,14 +4271,14 @@ fn validatepackageoutputpath(out: *u8) i32 = { fn workdirstamptext(istest: i32, emitasm: i32) str = { if (istest != 0) { if (emitasm != 0) { - return "ww workdir fmt 11 mode test asm 1\n"; + return "ww workdir fmt 12 mode test asm 1\n"; }; - return "ww workdir fmt 11 mode test asm 0\n"; + return "ww workdir fmt 12 mode test asm 0\n"; }; if (emitasm != 0) { - return "ww workdir fmt 12 mode build asm 1\n"; + return "ww workdir fmt 13 mode build asm 1\n"; }; - return "ww workdir fmt 12 mode build asm 0\n"; + return "ww workdir fmt 13 mode build asm 0\n"; }; fn stampmatches(path: *u8, want: str) bool = { @@ -3695,14 +4391,6 @@ fn buildonesepimpl(selfdir: *u8, src: *u8, entryisdir: i32, graphout: **sepgraph) i32 = { sepfatalallocation = false; if (nproducts < 1) { return 1; }; - let statusi: i32 = 0; - for (statusi < nproducts) { - if (products[statusi].status != nil) { - let rr: i32 = os.remove(pathstr(products[statusi].status)); - if (rr != 0 && rr != -2) { return 1; }; - }; - statusi += 1; - }; let c6: *u8 = toolpath(selfdir, "WW_W6C", "w6c_ww"); let a6: *u8 = toolpath(selfdir, "WW_W6A", "w6a_ww"); let l6: *u8 = toolpath(selfdir, "WW_W6L", "w6l_ww"); @@ -3799,14 +4487,6 @@ fn buildonesepimpl(selfdir: *u8, src: *u8, entryisdir: i32, } else { scratch = sepappendlit(effstem, ".sepwork"); if (scratch == nil) { return 1; }; - if (os.mkdir(pathstr(scratch), 493i32) != 0) { - cerr("ww: cannot create scratch\n"); - return 1; - }; - // Hand the path back only after mkdir succeeds, so the wrapper - // never removes a pre-existing path that this invocation failed - // to acquire. - if (scratchout != nil) { *scratchout = scratch; }; }; let staleall: bool = false; let stampok: bool = false; @@ -3891,16 +4571,24 @@ fn buildonesepimpl(selfdir: *u8, src: *u8, entryisdir: i32, }; let contextroot: *u8 = entry; if (entryisdir == 0) { contextroot = srcd.ptr; }; - 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; }; - let rootpath: *u8 = "\0".ptr; + let requestedpath: *u8 = "\0".ptr; if (products[producti].identity != nil) { - rootpath = products[producti].identity; + requestedpath = products[producti].identity; + }; + products[producti].context = sepcontextfor(g, contextroot, + incs, toolsrcdir, requestedpath); + if (products[producti].context < 0) { return 1; }; + let inferredpath: *u8 = nil; + let rootpath: *u8 = requestedpath; + if (entryisdir != 0 && rootpath[0u64] == 0u8) { + let inferred: i32 = sepcontextimportbase(g, + products[producti].context, &inferredpath); + if (inferred < 0) { return 1; }; + if (inferred > 0) { rootpath = inferredpath; }; }; products[producti].root = sepfindoraddvariant(g, rootpath, entry, entryisdir, products[producti].variant, @@ -3921,8 +4609,13 @@ fn buildonesepimpl(selfdir: *u8, src: *u8, entryisdir: i32, let tp: *u8 = locateimport(toolsrcdir, "test".ptr, "test".len: u64); if (tp != nil) { - g.supportcontext = sepcontextfor(g, toolsrcdir, nil, - toolsrcdir); + let supportsearch: *u8 = sepappendlit(toolsrcdir, ":"); + if (supportsearch == nil) { return 1; }; + supportsearch = sepappendlit(supportsearch, + pathstr(toolsrcdir)); + if (supportsearch == nil) { return 1; }; + g.supportcontext = sepcontextadd(g, toolsrcdir, + supportsearch, tp, toolsrcdir); if (g.supportcontext < 0) { return 1; }; let collision: bool = false; producti = 0; @@ -3996,7 +4689,8 @@ fn buildonesepimpl(selfdir: *u8, src: *u8, entryisdir: i32, let loadresult: i32 = seploadpkg(g, root, products[producti].context); if (loadresult == -2) { return 1; }; - if (loadresult == SEP_LOAD_INTERNAL) { return 1; }; + if (loadresult == SEP_LOAD_INTERNAL + || loadresult == SEP_LOAD_VENDOR) { return 1; }; if (loadresult < 0) { g.pkg[root].failed = true; producti += 1; @@ -4010,7 +4704,7 @@ fn buildonesepimpl(selfdir: *u8, src: *u8, entryisdir: i32, }; producti += 1; }; - if (istest != 0 && entryisdir != 0) { + if (istest != 0) { producti = 0; for (producti < nproducts) { let variant: i32 = products[producti].variantroot; @@ -4019,7 +4713,8 @@ fn buildonesepimpl(selfdir: *u8, src: *u8, entryisdir: i32, let loadresult: i32 = seploadpkg(g, support, products[producti].context); if (loadresult == -2) { return 1; }; - if (loadresult == SEP_LOAD_INTERNAL) { return 1; }; + if (loadresult == SEP_LOAD_INTERNAL + || loadresult == SEP_LOAD_VENDOR) { return 1; }; if (loadresult < 0) { g.pkg[variant].failed = true; }; @@ -4063,7 +4758,6 @@ fn buildonesepimpl(selfdir: *u8, src: *u8, entryisdir: i32, && !seprootiscommand(&g.pkg[products[0].root]); if (sepvalidateartifactpaths(g, scratch) < 0) { return 1; }; if (warm && sepvalidateworkdirowners(g, scratch) < 0) { return 1; }; - if (warm && staleall && invalidateworkdirunits(scratch) != 0) { return 1; }; let ci: i32 = 0; let order: []i32; let stack: []i32; @@ -4116,6 +4810,51 @@ fn buildonesepimpl(selfdir: *u8, src: *u8, entryisdir: i32, }; producti += 1; }; + // Propagate already-known package-load failures before scratch or status + // acquisition. Good sibling roots remain viable, while an entirely failed + // cold request leaves no empty persistent or caller-visible tree. + let preoi: i32 = 0; + for (preoi < norder) { + let pi: i32 = order[preoi]; + let dk: i32 = 0; + for (dk < g.pkg[pi].ndeps) { + if (g.pkg[g.pkg[pi].deps[dk]].failed) { + g.pkg[pi].failed = true; + }; + dk += 1; + }; + preoi += 1; + }; + let viableproduct: bool = false; + producti = 0; + for (producti < nproducts) { + if (!g.pkg[products[producti].root].failed) { + viableproduct = true; + }; + producti += 1; + }; + if (!viableproduct) { return 1; }; + // Source-derived resolution and contextual legality are complete before + // coordinator completion markers or persistent vouchers are changed. + if (!warm) { + if (os.mkdir(pathstr(scratch), 493i32) != 0) { + cerr("ww: cannot create scratch\n"); + return 1; + }; + // The wrapper owns only the directory this invocation acquired. + if (scratchout != nil) { *scratchout = scratch; }; + }; + let statusi: i32 = 0; + for (statusi < nproducts) { + if (products[statusi].status != nil) { + let rr: i32 = os.remove(pathstr(products[statusi].status)); + if (rr != 0 && rr != -2) { return 1; }; + }; + statusi += 1; + }; + if (warm && staleall && invalidateworkdirunits(scratch) != 0) { + return 1; + }; let anyfailed: bool = false; producti = 0; for (producti < nproducts) { @@ -4210,6 +4949,18 @@ fn buildonesepimpl(selfdir: *u8, src: *u8, entryisdir: i32, let commandpkg: bool = sepcommandcompilermarker(g, pi); let entry: bool = g.pkg[pi].linkentry; let supportpkg: bool = g.pkg[pi].testsupport; + let nmaps: i32 = 0; + let mapk: i32 = 0; + for (mapk < g.pkg[pi].bindings.len) { + let b: sepbind = g.pkg[pi].bindings[mapk]; + if (b.kind == 'D': u8 && b.dep >= 0 && b.dep < g.n + && !syntax.streq(b.name, + pathstr(g.pkg[b.dep].path))) { + if (nmaps == SEP_COUNT_MAX) { sepfailsize(); return 1; }; + nmaps += 1; + }; + mapk += 1; + }; let alen: i32 = 8; if (gent) { alen += 4; } else { @@ -4223,6 +4974,11 @@ fn buildonesepimpl(selfdir: *u8, src: *u8, entryisdir: i32, return 1; }; alen += g.pkg[pi].ndeps * 3; + if (nmaps > (SEP_COUNT_MAX - alen) / 3) { + sepfailsize(); + return 1; + }; + alen += nmaps * 3; let allocation: ([]str | nomem) = sepallocstrs(alen); let argv: []str; match (allocation) { @@ -4255,6 +5011,18 @@ fn buildonesepimpl(selfdir: *u8, src: *u8, entryisdir: i32, append(argv, pathstr(depinterface)); importk += 1; }; + mapk = 0; + for (mapk < g.pkg[pi].bindings.len) { + let b: sepbind = g.pkg[pi].bindings[mapk]; + if (b.kind == 'D': u8 && b.dep >= 0 && b.dep < g.n + && !syntax.streq(b.name, + pathstr(g.pkg[b.dep].path))) { + append(argv, "--import-map"); + append(argv, b.name); + append(argv, pathstr(g.pkg[b.dep].path)); + }; + mapk += 1; + }; append(argv, "-I"); append(argv, pathstr(cw)); append(argv, "-o"); @@ -4498,6 +5266,10 @@ fn buildonesepimpl(selfdir: *u8, src: *u8, entryisdir: i32, && pi != variantroot && g.pkg[pi].variant == SEP_VARIANT_PRODUCTION && g.pkg[pi].role != SEP_ROLE_TEST_SUPPORT + && g.pkg[pi].importbase != nil + && g.pkg[variantroot].importbase != nil + && cstreq(g.pkg[pi].importbase, + g.pkg[variantroot].importbase) && os.samefile(pathstr(g.pkg[pi].entry), pathstr(g.pkg[variantroot].entry))) { li -= 1; diff --git a/test/sep/localbuild_test.ww b/test/sep/localbuild_test.ww index efe94d04..f036882c 100644 --- a/test/sep/localbuild_test.ww +++ b/test/sep/localbuild_test.ww @@ -907,7 +907,7 @@ fn writediamond(td: str, reverse: bool) str = { "/.wwtool.w6a")), testenv.readfile(assembler)) || !testenv.same(testenv.readfile(strings.concat(work, "/.wwtool.stamp")), - "ww workdir fmt 12 mode build asm 0\n")) { + "ww workdir fmt 13 mode build asm 0\n")) { fail("driver-identity", "persistent artifacts or identities are incomplete"); }; let coldwwi: str = testenv.readfile(strings.concat(work, "/dep.wwi"));