diff --git a/cmd/ww/main.c b/cmd/ww/main.c index 8157b5b4..9f74588d 100644 --- a/cmd/ww/main.c +++ b/cmd/ww/main.c @@ -25,7 +25,7 @@ static const char *usage = "usage: ww [-V] [args...]\n" " -V print version and exit\n" -" build [-S] [-w DIR] [-I DIR] [-o FILE] [path ...] build local package graphs\n" +" build [-S] [-w DIR] [-I DIR] [-o FILE|DIR] [path ...] build local package graphs\n" " run [path] ... build then exec, passing extra args to the program\n" " test [-S -o STEM] [-w DIR] [options] [path ...] build/run tests; -S emits package asm\n" " version print version and exit\n" @@ -34,7 +34,8 @@ static const char *usage = " foo.ww literal file\n" " foo search cwd, -I dirs, then the source library for foo.ww or foo/\n" " lib/foo directory: build its package sources\n" -" -o publishes a non-main archive FILE + FILE.wwi\n" +" -o FILE publishes a non-main archive FILE + FILE.wwi\n" +" -o DIR publishes each selected command beneath DIR\n" " lib/... every eligible package under lib, recursively\n" " . build the cwd's .ww\n"; @@ -1256,6 +1257,7 @@ struct seppkg { int ngenerated_targets; int generated_targetcap; int failed; /* discovery/compile failure reaches this action */ + int action; /* reached by this request's semantic action list */ int test_support; /* compiler-generated -T support package */ int loaded; /* directory membership/name loaded exactly once */ int export_changed; /* staged export differs from committed export */ @@ -1306,6 +1308,7 @@ struct sepproduct { int variant; int directory_product; int no_tests; + int build_action; /* loaded product retained in the action list */ int context; int root; int variant_root; /* retained single-unit root outside directory products */ @@ -2847,7 +2850,8 @@ static int sep_validate_artifact_paths(struct sepgraph *g, const char *scratch) { for (int i = 0; i < g->n; i++) { - if (g->pkg[i].failed || !g->pkg[i].loaded) continue; + if (g->pkg[i].failed || !g->pkg[i].loaded || !g->pkg[i].action) + continue; if (g->pkg[i].storage == NULL && sep_assign_storage(&g->pkg[i], scratch) < 0) return -1; } @@ -2855,9 +2859,11 @@ sep_validate_artifact_paths(struct sepgraph *g, const char *scratch) do { changed = 0; for (int i = 0; i < g->n && !changed; i++) { - if (g->pkg[i].failed || !g->pkg[i].loaded) continue; + if (g->pkg[i].failed || !g->pkg[i].loaded + || !g->pkg[i].action) continue; for (int j = i + 1; j < g->n; j++) { if (g->pkg[j].failed || !g->pkg[j].loaded + || !g->pkg[j].action || strcmp(g->pkg[i].storage, g->pkg[j].storage) != 0) continue; @@ -2932,7 +2938,8 @@ sep_validate_workdir_owners(const struct sepgraph *g, const char *scratch) { char unit[SEP_ARTIFACT_MAX]; for (int i = 0; i < g->n; i++) { - if (g->pkg[i].failed || !g->pkg[i].loaded) continue; + if (g->pkg[i].failed || !g->pkg[i].loaded || !g->pkg[i].action) + continue; if (sep_fname(g, i, scratch, ".unit.ww", unit, sizeof unit) < 0) return -1; if (sep_validate_unit_owner(unit, &g->pkg[i]) < 0) return -1; @@ -3833,6 +3840,13 @@ sep_clone_for_test(struct sepgraph *g, int original, const char *owner, p->is_dir = src->is_dir; p->variant = SEP_VARIANT_TEST_COPY; p->role = src->role; + /* A test copy is a product-scoped action even when its replaced source + * node is not in the final action closure. Preserve its established + * complete-action locator without relying on a collision with that + * inactive source node during artifact validation. */ + p->storage = sep_storage_digest(p); + p->storage_hashed = 1; + if (p->storage == NULL) goto fail; p->loaded = src->loaded; p->failed = src->failed; p->test_support = src->test_support; @@ -5222,7 +5236,8 @@ sep_validate_request_staging(struct sepgraph *g, const char *scratch, int warm, ".o.new", ".a.new", ".init.unit.new", ".init.s.new", ".init.o.new" }; for (int pi = 0; pi < g->n; pi++) { - if (g->pkg[pi].failed || !g->pkg[pi].loaded) continue; + if (g->pkg[pi].failed || !g->pkg[pi].loaded + || !g->pkg[pi].action) continue; for (size_t si = 0; si < nelem(suffix); si++) { char path[SEP_ARTIFACT_MAX]; if (sep_fname(g, pi, scratch, suffix[si], path, @@ -5365,14 +5380,16 @@ sep_discard_request_staging(struct sepgraph *g, const char *scratch, int warm, ".init.unit.ww", ".init.s", ".init.o" }; const char **suffix = warm ? warm_suffix : cold_suffix; int rc = 0; - for (int pi = 0; pi < g->n; pi++) + for (int pi = 0; pi < g->n; pi++) { + if (!g->pkg[pi].action) continue; for (size_t i = 0; i < nelem(warm_suffix); i++) { char path[SEP_ARTIFACT_MAX]; if (sep_fname(g, pi, scratch, suffix[i], path, sizeof path) < 0 || (unlink(path) != 0 && errno != ENOENT)) - rc = -1; + rc = -1; } + } for (int i = 0; i < nproducts; i++) { const char *path[] = { products[i].stage_out, products[i].stage_publish, products[i].stage_iface, @@ -5474,6 +5491,8 @@ build_one_sep_impl(const char *src, int entry_is_dir, int require_command, int is_test, struct sepproduct *products, int nproducts, int emit_asm, const char *workdir, int create_workdir, const char *create_output_dir, + const char *default_output_dir, int output_path_error, + const char *output_collision_base, const char *output_collision_dir, char *scratchout, size_t scratchoutsz, struct sepgraph **graphout) { @@ -5927,10 +5946,6 @@ build_one_sep_impl(const char *src, int entry_is_dir, int root_package = !is_test && nproducts == 1 && !g->pkg[products[0].root].failed && !sep_root_is_command(&g->pkg[products[0].root]); - if (sep_validate_artifact_paths(g, scratch) < 0) - return 1; - if (warm && workdir_exists && sep_validate_workdir_owners(g, 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; @@ -5980,6 +5995,24 @@ build_one_sep_impl(const char *src, int entry_is_dir, return 1; } } + if (!is_test && create_output_dir != NULL) { + int actions = 0; + for (int i = 0; i < nproducts; i++) { + int root = products[i].root; + if (g->pkg[root].failed) { + free(stack); free(order); + return 1; + } + products[i].build_action = + sep_root_is_command(&g->pkg[root]); + if (products[i].build_action) actions++; + } + if (actions == 0) { + fputs("ww: no main packages to build\n", stderr); + free(stack); free(order); + return 1; + } + } if (!g->pkg[products[0].root].failed && root_package && publish_package && !emit_asm && validate_package_output_path(out) < 0) { @@ -5989,12 +6022,23 @@ build_one_sep_impl(const char *src, int entry_is_dir, for (int pi = 0; pi < g->n; pi++) g->pkg[pi].color = 0; for (int i = 0; i < nproducts; i++) { int root = products[i].root; - if (!g->pkg[root].failed + if (products[i].build_action && !g->pkg[root].failed && sep_topo_visit(g, root, order, &norder, stack, 0) < 0) { free(stack); free(order); return 1; } } free(stack); + for (int pi = 0; pi < g->n; pi++) g->pkg[pi].action = 0; + for (int oi = 0; oi < norder; oi++) g->pkg[order[oi]].action = 1; + if (sep_validate_artifact_paths(g, scratch) < 0) { + free(order); + return 1; + } + if (warm && workdir_exists + && sep_validate_workdir_owners(g, scratch) < 0) { + free(order); + return 1; + } /* Propagate already-known package-load failures through the union before * acquiring scratch or completion state. Independent sibling roots may * remain viable for deterministic staging/diagnosis, but any failed product @@ -6007,11 +6051,34 @@ build_one_sep_impl(const char *src, int entry_is_dir, } int viable_product = 0; for (int i = 0; i < nproducts; i++) - if (!g->pkg[products[i].root].failed) viable_product = 1; + if (products[i].build_action + && !g->pkg[products[i].root].failed) viable_product = 1; if (!viable_product) { free(order); return 1; } + if (!is_test && !emit_asm && output_path_error) { + fputs("ww: command output path is too long\n", stderr); + free(order); + return 1; + } + if (!is_test && !emit_asm && output_collision_base != NULL) { + fputs("ww: multiple commands produce output basename ", stderr); + sep_put_quoted(output_collision_base); + fputs(" in directory ", stderr); + sep_put_quoted(output_collision_dir); + fputc('\n', stderr); + free(order); + return 1; + } + if (!is_test && !emit_asm && default_output_dir != NULL && nproducts == 1 + && sep_root_is_command(&g->pkg[products[0].root])) { + fprintf(stderr, + "ww: build output \"%s\" already exists and is a directory\n", + default_output_dir); + free(order); + return 1; + } if (sep_validate_request_staging(g, scratch, warm, products, nproducts, root_package, publish_package, emit_asm, is_test) < 0) { sep_free_product_staging(products, nproducts); @@ -6023,8 +6090,8 @@ build_one_sep_impl(const char *src, int entry_is_dir, * output paths, and action closures have passed their pre-tool checks. */ struct sep_created_dirs created_output = {0}; struct sep_created_dirs created_work = {0}; - if (create_output_dir != NULL && create_output_dir[0] != '\0' - && sep_mkdirs(create_output_dir, is_test ? 0777 : 0700, + if (!emit_asm && create_output_dir != NULL && create_output_dir[0] != '\0' + && sep_mkdirs(create_output_dir, 0777, &created_output) != 0) { fprintf(stderr, is_test ? "ww: cannot create test output directory %s\n" @@ -6704,7 +6771,8 @@ build_one_sep(const char *src, int entry_is_dir, const char *root_identity, const struct seplinkflags *linkflags, int publish_package, int require_command, int is_test, int root_variant, const char *test_package, int emit_asm, - int keepscratch, const char *workdir) + int keepscratch, const char *workdir, const char *create_output_dir, + const char *default_output_dir, int output_path_error) { char scratch[PATH_MAX] = {0}; struct sepgraph *g = NULL; @@ -6717,6 +6785,7 @@ build_one_sep(const char *src, int entry_is_dir, const char *root_identity, .publish = NULL, .artifact = NULL, .variant = root_variant, + .build_action = 1, .root = -1, .variant_root = -1, .support = -1, @@ -6725,7 +6794,8 @@ build_one_sep(const char *src, int entry_is_dir, const char *root_identity, product.artifact = "__root"; int r = build_one_sep_impl(src, entry_is_dir, out, objstem, extra_includes, linkflags, publish_package, require_command, is_test, - &product, 1, emit_asm, workdir, 0, NULL, scratch, + &product, 1, emit_asm, workdir, 0, create_output_dir, + default_output_dir, output_path_error, NULL, NULL, scratch, sizeof scratch, &g); sep_graph_free(g); if (!keepscratch && scratch[0]) { @@ -6761,7 +6831,9 @@ build_package_tests(const char *src, const char *root_identity, const char *extra_includes, const char *workdir, struct sepproduct *products, int nproducts, int is_test, int publish_package, const struct seplinkflags *linkflags, int emit_asm, - int create_workdir, const char *create_output_dir) + int create_workdir, const char *create_output_dir, + const char *default_output_dir, int output_path_error, + const char *output_collision_base, const char *output_collision_dir) { char scratch[PATH_MAX] = {0}; struct sepgraph *g = NULL; @@ -6770,7 +6842,9 @@ build_package_tests(const char *src, const char *root_identity, int r = build_one_sep_impl(src, 1, products[0].out, products[0].out, extra_includes, linkflags, publish_package, 0, is_test, products, nproducts, emit_asm, workdir, create_workdir, - create_output_dir, scratch, sizeof scratch, &g); + create_output_dir, default_output_dir, output_path_error, + output_collision_base, output_collision_dir, + scratch, sizeof scratch, &g); sep_graph_free(g); return r; } @@ -6818,6 +6892,39 @@ basename_no_ext(const char *path, char *out, size_t outsz) if (dot && strcmp(dot, ".ww") == 0) *dot = '\0'; } +/* Go's build -o directory branch follows an existing destination through + * stat, and a trailing platform separator declares a directory which the + * request may need to create. WW's platform separator is '/'. */ +static int +build_output_dir(const char *path) +{ + struct stat st; + size_t n = strlen(path); + if (n != 0 && path[n - 1] == '/') return 1; + return stat(path, &st) == 0 && S_ISDIR(st.st_mode); +} + +static int +build_output_path(const char *dir, const char *src, char *out, size_t outsz) +{ + char base[PATH_MAX]; + size_t n = strlen(dir); + int written; + basename_no_ext(src, base, sizeof base); + written = snprintf(out, outsz, "%s%s%s", dir, + n != 0 && dir[n - 1] == '/' ? "" : "/", base); + if (written < 0 || (size_t)written >= outsz) return -1; + return 0; +} + +static void +build_import_leaf(const char *identity, char *out, size_t outsz) +{ + const char *leaf = strrchr(identity, '.'); + leaf = leaf ? leaf + 1 : identity; + snprintf(out, outsz, "%s", leaf); +} + static int resolve_module(const char *name, const char *incs, char *out, size_t outsz, int *is_dir) @@ -7043,6 +7150,7 @@ do_build(int argc, char **argv) free(incs); return 2; } + int add_dot = src == NULL; if (src == NULL) src = "."; if (strstr(src, "...") != NULL || next < argc || saw_terminator) { free(incs); @@ -7060,11 +7168,32 @@ do_build(int argc, char **argv) char out[PATH_MAX]; const char *objstem = NULL; int discard_output = strcmp(outflag, "/dev/null") == 0; + int output_dir = outflag[0] && !discard_output + && build_output_dir(outflag); + const char *root_identity = !literal && is_dir ? src : NULL; + if (output_dir && is_dir) { + free(incs); + return exec_package_command(argc, argv, src, + add_dot ? NULL : resolved, root_identity, add_dot, 1); + } + const char *create_output_dir = NULL; + int output_path_error = 0; if (outflag[0] && !discard_output) { /* -o sets both the binary path and the intermediate stem so * artifacts land beside the requested output (T3). */ - memcpy(out, outflag, strlen(outflag) + 1); + if (output_dir) { + if (build_output_path(outflag, resolved, out, + sizeof out) < 0) { + output_path_error = 1; + basename_no_ext(resolved, out, sizeof out); + } + create_output_dir = outflag; + } else { + memcpy(out, outflag, strlen(outflag) + 1); + } objstem = out; + } else if (is_dir && root_identity != NULL) { + build_import_leaf(root_identity, out, sizeof out); } else if (is_dir) { char tmp[PATH_MAX]; memcpy(tmp, resolved, strlen(resolved) + 1); @@ -7075,7 +7204,8 @@ do_build(int argc, char **argv) } else { basename_no_ext(resolved, out, sizeof out); } - const char *root_identity = !literal && is_dir ? src : NULL; + const char *default_output_dir = !outflag[0] && build_output_dir(out) + ? out : NULL; if (discard_output) { char tmpdir[PATH_MAX], tmp[PATH_MAX]; int dn = snprintf(tmpdir, sizeof tmpdir, "/tmp/ww_build_%d", getpid()); @@ -7095,7 +7225,7 @@ do_build(int argc, char **argv) * library compilation still need request-private product paths. */ int rc = build_one_sep(resolved, is_dir, root_identity, tmp, tmp, incs, &linkflags, 0, 0, 0, SEP_VARIANT_PRODUCTION, NULL, - emit_asm, 0, workdir); + emit_asm, 0, workdir, NULL, NULL, 0); int cleanfail = 0; if (unlink(tmp) != 0 && errno != ENOENT) { fputs("ww: cannot remove temporary output\n", stderr); @@ -7111,7 +7241,8 @@ do_build(int argc, char **argv) } int rc = build_one_sep(resolved, is_dir, root_identity, out, objstem, incs, &linkflags, outflag[0] != '\0', 0, 0, SEP_VARIANT_PRODUCTION, NULL, - emit_asm, 1, workdir); + emit_asm, 1, workdir, create_output_dir, default_output_dir, + output_path_error); free(incs); return rc; } @@ -7156,7 +7287,7 @@ do_run(int argc, char **argv) const char *root_identity = !literal && is_dir ? src : NULL; int buildrc = build_one_sep(resolved, is_dir, root_identity, tmp, tmp, incs, &linkflags, - 0, 1, 0, SEP_VARIANT_PRODUCTION, NULL, 0, 0, NULL); + 0, 1, 0, SEP_VARIANT_PRODUCTION, NULL, 0, 0, NULL, NULL, NULL, 0); free(incs); if (buildrc != 0) { if (unlink(tmp) != 0 && errno != ENOENT) @@ -7226,6 +7357,10 @@ do_test(int argc, char **argv) int package_publish = 0; int package_create_workdir = 0; const char *package_create_output_dir = NULL; + int package_output_path_error = 0; + const char *package_default_output_dir = NULL; + const char *package_output_collision_base = NULL; + const char *package_output_collision_dir = NULL; struct seplinkflags package_linkflags = {0}; int afterdash = 0; const char *request_identity = NULL; @@ -7296,6 +7431,34 @@ do_test(int argc, char **argv) return 2; } package_create_output_dir = argv[++i]; + } else if (strcmp(argv[i], + "--ww-command-output-path-error") == 0) { + if (package_output_path_error) { + fprintf(stderr, + "ww test: invalid --ww-command-output-path-error\n"); + return 2; + } + package_output_path_error = 1; + } else if (strcmp(argv[i], "--ww-default-output-dir") == 0) { + if (i + 1 >= argc || package_default_output_dir != NULL + || argv[i + 1][0] == '\0') { + fprintf(stderr, + "ww test: invalid --ww-default-output-dir\n"); + return 2; + } + package_default_output_dir = argv[++i]; + } else if (strcmp(argv[i], + "--ww-command-output-collision") == 0) { + if (i + 2 >= argc + || package_output_collision_base != NULL + || argv[i + 1][0] == '\0' + || argv[i + 2][0] == '\0') { + fprintf(stderr, + "ww test: invalid --ww-command-output-collision\n"); + return 2; + } + package_output_collision_base = argv[++i]; + package_output_collision_dir = argv[++i]; } else if (strcmp(argv[i], "--ww-package-test") == 0) { if (i + 9 >= argc) { fprintf(stderr, @@ -7363,6 +7526,7 @@ do_test(int argc, char **argv) products[nproducts].directory_product = 1; products[nproducts].no_tests = test_product && !has_internal && !has_external; + products[nproducts].build_action = 1; products[nproducts].root = -1; products[nproducts].variant_root = -1; products[nproducts].production_root = -1; @@ -7494,6 +7658,12 @@ do_test(int argc, char **argv) fprintf(stderr, "ww test: invalid private directory creation\n"); return 2; } + if ((package_output_path_error || package_default_output_dir != NULL + || package_output_collision_base != NULL) + && (!package_build || nproducts == 0)) { + fprintf(stderr, "ww test: invalid private output preflight\n"); + return 2; + } if (package_create_workdir && nproducts == 0) { fprintf(stderr, "ww test: invalid private directory creation\n"); return 2; @@ -7595,7 +7765,10 @@ do_test(int argc, char **argv) incs, workdir, products, nproducts, package_build ? 0 : 1, package_publish, package_build ? &package_linkflags : NULL, emit_asm, - package_create_workdir, package_create_output_dir); + package_create_workdir, package_create_output_dir, + package_default_output_dir, package_output_path_error, + package_output_collision_base, + package_output_collision_dir); free(products); free(incs); return r; @@ -7643,7 +7816,7 @@ do_test(int argc, char **argv) outstem[0] && !discard_output ? outstem : tmp, incs, NULL, 0, 0, 1, SEP_VARIANT_PRODUCTION, NULL, emit_asm, - outstem[0] && !discard_output ? 1 : 0, workdir); + outstem[0] && !discard_output ? 1 : 0, workdir, NULL, NULL, 0); if (br != 0) { if (owntmp && unlink(outp) != 0 && errno != ENOENT) fputs("ww: cannot remove temporary output\n", stderr); @@ -7711,7 +7884,7 @@ do_test(int argc, char **argv) int br = build_one_sep(target, 0, NULL, outp, outstem[0] && !discard_output ? outstem : tmp, incs, NULL, 0, 0, 1, SEP_VARIANT_PRODUCTION, NULL, emit_asm, - outstem[0] && !discard_output ? 1 : 0, workdir); + outstem[0] && !discard_output ? 1 : 0, workdir, NULL, NULL, 0); if (br != 0) { if (owntmp && unlink(outp) != 0 && errno != ENOENT) fputs("ww: cannot remove temporary output\n", stderr); @@ -7759,7 +7932,9 @@ do_test(int argc, char **argv) int r = build_package_tests(target, request_identity, incs, workdir, products, nproducts, package_build ? 0 : 1, package_publish, package_build ? &package_linkflags : NULL, - emit_asm, package_create_workdir, package_create_output_dir); + emit_asm, package_create_workdir, package_create_output_dir, + package_default_output_dir, package_output_path_error, + package_output_collision_base, package_output_collision_dir); free(products); free(incs); return r; diff --git a/docs/build-system.md b/docs/build-system.md index a7aa6521..780fb696 100644 --- a/docs/build-system.md +++ b/docs/build-system.md @@ -6533,6 +6533,233 @@ runtime cwd/environment/stdin. Build runtime behavior is inapplicable, and caller-output rollback, occupied caller stages, and output permissions are inapplicable to the exact discard branch because it creates no public inode. +### 11.30 Implemented single-root build output directories + +An explicit `ww build -o OUT` now treats `OUT` as a directory when ordinary +`stat` reports an existing directory or the spelling ends in `/`. The rule is +independent of package count. A selected command directory publishes +`OUT/` (falling back to the selected local directory +leaf when no contextual identity exists); a raw command-line source publishes +`OUT/`. A missing trailing-slash hierarchy is +created from `0777`, filtered by caller umask, through the existing checked +directory ledger. All roots load first, but independently selected non-main +roots are omitted from the directory branch's action list. A selection +containing no command—including a raw non-main root—rejects as +`ww: no main packages to build` without running a producer or changing the +output directory. After a lone command's default basename is synthesized, an +existing directory at that +basename instead rejects as +`ww: build output "" already exists and is a directory`; a non-main +package has no default public output and is unaffected. + +#### Pinned Go evidence and fact classification + +The sole authority is official Go 1.26.5 at commit +`c19862e5f8415b4f24b189d065ed739517c548ba`: + +- `runBuild` completes package loading and package-error checking before output + handling at + [`cmd/go/internal/work/build.go`, lines 459–471](https://github.com/golang/go/blob/c19862e5f8415b4f24b189d065ed739517c548ba/src/cmd/go/internal/work/build.go#L459-L471). + After synthesizing a lone `main` package's default output at lines 473–478, + its output branch classifies an existing directory through + `os.Stat`, or a spelling ending in `/` or the host path separator, at + [lines 508–518](https://github.com/golang/go/blob/c19862e5f8415b4f24b189d065ed739517c548ba/src/cmd/go/internal/work/build.go#L508-L518). + It then creates install actions only for packages named `main`, targets each + at the output directory joined with `DefaultExecName`, rejects an empty + command action list as `no main packages to build`, and executes that graph + at + [lines 519–535](https://github.com/golang/go/blob/c19862e5f8415b4f24b189d065ed739517c548ba/src/cmd/go/internal/work/build.go#L519-L535). + The non-directory single-output branch is separate at + [lines 537–548](https://github.com/golang/go/blob/c19862e5f8415b4f24b189d065ed739517c548ba/src/cmd/go/internal/work/build.go#L537-L548). +- `DefaultExecName` uses the final import-path element for a directory package + and the source basename for command-line files + ([`cmd/go/internal/load/pkg.go`, lines 1727–1769](https://github.com/golang/go/blob/c19862e5f8415b4f24b189d065ed739517c548ba/src/cmd/go/internal/load/pkg.go#L1727-L1769)). +- `BuildInstallFunc` creates the target parent before installation, and + `Shell.Mkdir` implements that operation as `os.MkdirAll(dir, 0777)` + ([`cmd/go/internal/work/exec.go`, lines 1975–2000](https://github.com/golang/go/blob/c19862e5f8415b4f24b189d065ed739517c548ba/src/cmd/go/internal/work/exec.go#L1975-L2000), + [`cmd/go/internal/work/shell.go`, lines 283–301](https://github.com/golang/go/blob/c19862e5f8415b4f24b189d065ed739517c548ba/src/cmd/go/internal/work/shell.go#L283-L301)). +- Official + [`build_output.txt`, lines 10–29 and 41–45](https://github.com/golang/go/blob/c19862e5f8415b4f24b189d065ed739517c548ba/src/cmd/go/testdata/script/build_output.txt#L10-L45) + asserts a raw source's default basename, a missing trailing-slash directory, + and an existing directory destination. +- Official + [`build_multi_main.txt`, lines 1–16](https://github.com/golang/go/blob/c19862e5f8415b4f24b189d065ed739517c548ba/src/cmd/go/testdata/script/build_multi_main.txt#L1-L16) + asserts command fan-out beneath one directory, no-main rejection, the + implicit-default-existing-directory error, and raw command-line-source + placement beneath an existing output directory. + +Load-before-output ordering, `stat`/separator classification, command-only +installation, import-leaf naming, destination joining, no-main rejection, and +the `0777` parent-creation request are directly implemented by the pinned +source. Raw-source basename and existing/missing directory behavior are +directly asserted by official testdata. The resulting parent mode after umask +is derived from the pinned `MkdirAll` call. Application to +exactly one directory package is derived from `runBuild`: the directory branch +tests output form, not package cardinality, while the cardinality check exists +only in the later non-directory branch. These facts apply to WW's local +manifest-free command and raw-source products without importing Go's module, +cache, registry, distribution, or network behavior. The installed host Go +version is not authority. + +#### Direct pre-fix measurements + +Fresh public Cstage (`out/bin/ww`) and WWstage (`out/bin/ww_ww`) probes measured +the same divergence before production edits: + +- one command directory plus an existing output directory spelled without `/` + returned 0, renamed the caller's directory to a PID-bearing transaction + backup, installed a 4,268-byte ELF executable at the directory pathname, and + diagnosed failure to remove the nonempty backup; +- the same existing directory spelled with `/` returned 1 with + `ww: cannot preserve transaction destination .../`; +- a missing nested spelling ending in `/` returned 1 while trying to acquire + `/.sepwork` before the hierarchy existed; +- a raw `main.ww` plus an existing trailing-slash destination failed through + the same transaction-destination path instead of producing `OUT/main`; and +- a non-main directory plus an existing directory spelled without `/` returned + 0, replaced the directory pathname by an archive, wrote its `.wwi` beside + that pathname, and stranded the former directory as a transaction backup; + and +- for both a command directory and a raw command-line source, an existing + directory at the synthesized default basename was renamed to a transaction + backup and replaced by the executable; both stages returned 0 and diagnosed + inability to remove the deliberately nonempty backup. + +Completion review measured four additional stage-identical pre-completion +behaviors before their production edits. A mixed command/non-main request +compiled the independent non-main root in both stages; a raw non-main root +returned 0 and published an archive plus `.wwi`; a contextual `alias` symlink +to physical directory `physical` published `OUT/physical`; and an exactly +arranged umask `000` produced newly created output parents with mode `0700`. +Pinned Go instead loads then omits the independent non-main action, rejects the +raw no-command selection, uses the requested import leaf, and requests parent +mode `0777`. The preserved traces and stat results are in the session evidence +ledger. + +Final review then measured four stage-identical load-precedence leaks before +the completion edit. A non-main root with a missing import reported only +`no main packages`; a command with a missing import and an overlong derived +directory destination reported only the path error; two colliding command +basenames, one with a missing import, reported only the duplicate-destination +error; and a recursive command with a missing import plus an implicit default +directory collision reported only the collision. Pinned `runBuild` lines +470–471 load and check all selected packages before any output handling at +lines 473–548. Missing-package diagnostics therefore precede no-main, +derived-path, duplicate-destination, and implicit-default checks. Extending +that boundary to WW's transactional duplicate guard is derived from the pinned +ordering because the guard is WW-local output preflight. The exact probes and +outputs are preserved in the evidence ledger. + +Those are directly measured WW facts. Both drivers interpreted every non-null +single-root `-o` as one file and object stem. The compiler, assembler, linker, +archive writer, and shared package coordinator were not the cause. + +#### Ownership, actions, publication, and identity + +`internal/wwpackage.packagecommand` remains the shared package-request output +owner. It classifies output directories, discovers and groups the complete +selection, and passes every selected root plus output-preflight metadata into +the shared separate-build executor. Only after that executor has loaded all +packages and imports does it retain command roots, derive their complete action +closure, and evaluate no-main, path-length, duplicate-destination, and +implicit-default checks. A separate presentation field carries the requested +import leaf (or local-path fallback) through collision preflight and +publication; canonical physical directory metadata remains loader metadata. +The coordinator commits the retained products through one request transaction. +The dispatch part of the gap belonged to the early single-root compatibility +choice in `cmd/ww.do_build` and `selfhost/cmd/ww.dobuild`; completion review +also closed the coordinator's independent-non-main action, +physical-leaf-presentation, and load-precedence leaks. + +After ordinary argument parsing and root resolution, both drivers now apply the +same exact Unix classification. A directory root with a directory output enters +the existing package coordinator. An explicit logical root carries its +unchanged logical root identity while the corresponding argument is replaced +by the already-resolved loader route; a default invocation inserts exactly one +`.`; a literal directory remains literal. Thus the shared loader, grouping, +graph, action, output-preflight, and publication rules operate exactly as they +do for a larger request. + +The raw-file compatibility route remains driver-owned. It derives the joined +command path, uses that path as the existing direct action's product and stem, +and passes the classified directory to the shared transaction's checked +creation ledger. A bounds failure is carried as preflight metadata so source +and import errors, and raw no-main rejection, retain pinned load-first +precedence before the path diagnostic. It does not manufacture a package +request or change raw-source graph identity. `stat` follows a symlinked output +directory; lexical publication remains beneath the requested symlink spelling. +Exact `/dev/null` is classified first by the completed discard rule and never +enters this directory branch. + +For an implicit default, the drivers pass the existing-directory collision as +preflight metadata to the common separate-build executor. The executor waits +until package/import loading, contextual checks, and graph-cycle validation +have established the root action kind. It rejects a command before scratch, +workdir, tool, stage, or destination acquisition, but lets a non-main package +perform its unchanged no-public-output build. This keeps raw and directory +compatibility routes on the same diagnostic-precedence rule without deriving +kind from a path, filename, declared-name guess, or driver-side source scan. + +Loading and source/import rejection precede every output-derived rejection, +including no-main, derived-path length, duplicate destination, and implicit +default collision; all precede output creation. A selection with no command +rejects after full package/import loading and graph validation but before +compiler, assembler, linker, or directory mutation. In a mixed request, +independently selected non-main roots have no action; a non-main package +reachable as a command dependency still performs its ordinary semantic action. +Repeated exact roots retain canonical graph/action deduplication. Successful +commands compile, assemble, archive, and link normally; `-S -w` remains +action-only and publishes no command. Directory form still selects only +commands and therefore retains no-main rejection, but destination length, +duplicate publication names, implicit destination collision, and output-parent +creation belong to the install action that `-S` never reaches. They are +inapplicable to that assembly-only request. An explicit external workdir +prevents an adjacent `.sepwork`; without `-w`, that established WW +scratch tree remains an ordinary retained build artifact beneath the output +directory. + +Existing destination contents and symlink targets survive successful +publication. Compiler failure, linker failure, or linker signal preserves the +prior command and every committed persistent artifact, removes `.new` and +transaction stages, and rolls back only directory prefixes created for the +failed request. Every caller-output prefix is requested as `0777` and filtered +once by the caller umask; persistent/private work directories keep their +separate modes. The raw route has the same missing-directory rollback through +the shared creation ledger. Independent Cstage and WWstage processes use +disjoint output, work, stage, and process ownership and may complete +concurrently. `ww build` itself has no runtime action; direct execution of the +published command verifies its ordinary program exit result, while runtime +failure/timeout policy remains owned by `ww run` and `ww test`. + +This is output disposition and dispatch only. Dotted package/import identity, +declared package name, physical source directory metadata, file-local import +bindings, graph edges, action and storage keys, symbols, `.wwi`, compiler, +assembler, archive and linker semantic inputs, artifact bytes, invalidation, +and public-file output-mode formula are unchanged. Correcting caller-output +parent creation metadata does not enter any semantic identity. There is no +persisted-byte contract change: build workdir format remains `18`, test +workdir format remains `19`, and semantic storage remains `3`. + +The WW-native owner `single_root_build_output_directory` covers literal, +logical, default-dot, symlinked, raw-file, explicit existing/missing, and +implicit-default existing-directory forms in both stages; basename selection +and directory-content preservation; logical alias versus physical-leaf +separation; command rejection, raw no-main rejection, non-main no-output +behavior, long raw-output and load-error precedence; mixed command-only and +repeated roots; skipped-root import rejection; and no-main, directory-derived +path, duplicate-destination, and recursive implicit-collision precedence with +zero tool activity; exact `0777` missing-parent creation under umask `000`; +the already-aligned retained-test control; `-S -w` command selection with no +install-only preflight or output-directory creation; cold, warm, +and invalidated persistence; exact compiler/assembler/linker action traces; +successful program exit results; compiler and linker failure; linker signal; +prior-state and newly-created-directory rollback; `.new` and transaction +cleanup; concurrent stage isolation; executable and semantic-artifact bytes; +and exact diagnostic parity. Existing owners continue to cover public-file +output umasks, occupied stages, generalized multi-product transactions, test +runtime failure and timeout, null discard, and broader package/import graph +matrices. + ## 12. Candidate architectures and hard-gate decision Five candidates were developed as coherent systems, not as feature bins. diff --git a/docs/spec.md b/docs/spec.md index 0c8bdb4f..008b5a7d 100644 --- a/docs/spec.md +++ b/docs/spec.md @@ -310,6 +310,35 @@ ImportPath = ident { "." ident } . `fn main`; path and directory spelling do not classify commands. An ordinary import of a package declared `main` is rejected, except for the toolchain's colocated external-test wiring. +- For `ww build`, an explicit `-o` names an output directory when ordinary + `stat` reports an existing directory (following symlinks) or its spelling + ends in `/`. This classification is independent of whether one or many + package roots were requested. Each selected command is published beneath + that directory using the final component of its requested contextual import + name, with the selected local directory leaf as the fallback when no such + identity exists. Independently selected non-main roots load but do not enter + the action list; non-main command dependencies retain ordinary actions, and + a selection containing no command rejects before tools or output creation. + A raw command-line `.ww` source uses its source basename without `.ww`, and + a raw non-main source is the same no-command rejection. Missing + trailing-slash hierarchies are created transactionally from `0777`, filtered + by the caller's umask. + Loading and package/import diagnostics precede no-main, derived-path, + duplicate-destination, implicit-default, and other output preflight; all of + those checks precede creation. Producer failure or interruption preserves + existing directory contents and removes only request-created prefixes and + stages. A non-directory output retains the single-product file/archive rule. + If a lone + command's synthesized default basename already names a directory, loading + and graph validation complete and the build rejects before tools without + changing that directory; a non-main package synthesizes no default public + output. Output paths and directory metadata never become package, import, + graph, action, + symbol, artifact, `.wwi`, or persistence identity. + Assembly-only `-S` retains the directory form's command-action selection and + no-main rejection, but it reaches no install action: destination length, + duplicate publication names, implicit destination collision, and output + parent creation are therefore inapplicable. - A newly published `ww build` command is created with permission `0777` filtered by the invoking process's umask. A newly published non-command archive, and the adjacent WW interface required to consume it, use `0666` diff --git a/docs/test-system-v2.md b/docs/test-system-v2.md index ca2f2012..9061e616 100644 --- a/docs/test-system-v2.md +++ b/docs/test-system-v2.md @@ -234,6 +234,28 @@ package with no selected test source validates production but publishes nothing and does not create an otherwise unneeded output directory. Successful `-c` output is silent apart from no-test reporting. +The package suite also pins the adjacent `ww build` output-directory branch. +After normal loading, an existing directory (including a symlink to one) or an +explicit spelling ending in `/` receives each command under its requested +import-leaf name (or local directory-leaf fallback) even for exactly one +selected root; a raw `.ww` command uses its source basename. A missing +trailing-slash hierarchy is created from `0777`, filtered by umask, by the +request transaction. A directory or raw no-main-only selection and an import +failure run no tools and do not create output. Independently selected non-main +siblings in a mixed request load but have no action; command dependencies still +build. Package/import rejection precedes no-main, derived destination length, +duplicate command basename, and implicit default-directory preflight. If the +synthesized default command basename already exists as a directory, +load/graph validation precedes a zero-tool rejection and preserves that +directory; a non-main package has no corresponding default output. +Persistent work reuse, invalidation, failures, interruption, and +concurrent drivers retain the same action and rollback rules; output form does +not enter package/import/action identity. This build branch starts no runtime +process, while direct execution of its published binary remains an artifact +check rather than part of `ww build`. Assembly-only `-S` still selects command +actions and rejects a no-command directory request, but it performs no +install-only destination validation or output-directory creation. + The retained executable is byte-identical to the temporary runnable and has executable mode `0777` filtered by the caller's umask, but it is never the path executed by the coordinator. Publication participates in the driver's one @@ -312,6 +334,24 @@ precede those coordinator-owned source-validation parses. Separate compilation is the only driver build path; no compatibility mode switch remains. +`test/package/package_test.ww` owns the focused dual-stage +`single_root_build_output_directory` observer. It covers existing and missing +directory spellings; trailing and non-trailing forms; literal, logical, +default-dot, symlink, raw-file, and implicit-default routes; build/test +adjacency; mixed and repeated roots; command-only publication, no-main and +implicit-collision rejection, non-main default behavior; import +diagnostic precedence over no-main, derived-path, duplicate-destination, and +recursive implicit-collision checks; `-S -w` command selection with long, +duplicate, implicit, and missing-directory publication preflight omitted; +cold, warm, and invalidated persistence; +compiler, assembler, and linker action traces; compiler/linker failure and +linker signal; prior-state and directory-creation rollback; concurrent +isolation; executable modes and runtime exit values; `.new`/transaction +cleanup; and Cstage/WWstage diagnostic, binary, assembly, and persistent +semantic-artifact byte parity. General output permissions, occupied stages, +multi-product transactions, null discard, and test runtime timeout remain with +their existing observers. + `test/package/package_test.ww` also owns the dual-stage `declared_name_identity_and_file_import_scope` and `explicit_import_alias_binding_modes` observers. They generate temporary diff --git a/internal/wwpackage/package.ww b/internal/wwpackage/package.ww index 137c7ebf..bbdd5d33 100644 --- a/internal/wwpackage/package.ww +++ b/internal/wwpackage/package.ww @@ -24,6 +24,7 @@ type pkgfolder = struct { type pkggroup = struct { dir: str, pkg: str, + basename: str, testname: str, publish: str, prodpkg: str, @@ -58,6 +59,11 @@ type pkgplan = struct { buildonly: bool, publish: bool, emitasm: bool, + outputpatherror: bool, + defaultoutputdir: str, + outputcollisionbase: str, + outputcollisiondir: str, + suppressbuildreports: bool, }; def PKG_COUNT_MAX: i32 = 2147483647; @@ -1368,8 +1374,9 @@ fn pkgsetplanpaths(p: *pkgplan, groups: []pkggroup, root: str, index: i32, return false; }; if (buildonly) { - if (outputdir && strings.compare(g.pkg, "main") == 0) { - if (!pkgjoinpath(outname, pkgbase(g.dir), &g.bin)) { return false; }; + if (outputdir && strings.compare(g.pkg, "main") == 0 + && !p.outputpatherror && !p.emitasm) { + if (!pkgjoinpath(outname, g.basename, &g.bin)) { return false; }; } else if (outname.len != 0 && !outputdir) { g.bin = outname; } else if (!pkgstring(&g.bin, g.root, "/package.build")) { return false; }; } else { @@ -1416,7 +1423,7 @@ fn pkgreportcommand(fd: i32, kind: str, g: *pkggroup, fn pkgstartbuild(p: *pkgplan, groups: []pkggroup, builder: str, includes: []str, libdirs: []str, libs: []str, h: *exec.process) bool = { let nproducts: i32 = p.end - p.start; - let capacity: i32 = 16; + let capacity: i32 = 24; if (nproducts < 0 || nproducts > (PKG_COUNT_MAX - capacity) / 10) { pkgputln(os.STDERR_FILENO, "wwtest package: package graph is too large"); return false; @@ -1457,6 +1464,18 @@ fn pkgstartbuild(p: *pkgplan, groups: []pkggroup, builder: str, includes: []str, append(ba, "--ww-create-output-dir"); append(ba, p.outputdir); }; + if (p.outputpatherror) { + append(ba, "--ww-command-output-path-error"); + }; + if (p.defaultoutputdir.len != 0) { + append(ba, "--ww-default-output-dir"); + append(ba, p.defaultoutputdir); + }; + if (p.outputcollisionbase.len != 0) { + append(ba, "--ww-command-output-collision"); + append(ba, p.outputcollisionbase); + append(ba, p.outputcollisiondir); + }; if (p.identity.len != 0) { append(ba, "--ww-root-identity"); append(ba, p.identity); @@ -1627,8 +1646,10 @@ fn pkgemitplan(p: *pkgplan, groups: []pkggroup, let i: i32 = p.start; for (i < p.end) { if (!pkgproductbuilt(&groups[i], buildonly)) { - pkgreportcommand(os.STDERR_FILENO, "build", &groups[i], - &p.buildres); + if (!p.suppressbuildreports) { + pkgreportcommand(os.STDERR_FILENO, "build", &groups[i], + &p.buildres); + }; failed += 1; } else if (buildonly) { void; @@ -2165,6 +2186,7 @@ export fn packagecommand(args: []str) int = { g.dir = f.path; g.pkg = family; if (g.pkg.len == 0) { g.pkg = srcs[f.start].pkg; }; + g.basename = ""; g.testname = ""; g.publish = ""; g.prodpkg = f.prodpkg; @@ -2193,57 +2215,48 @@ export fn packagecommand(args: []str) int = { return 1; }; pkgsortgroups(groups); + i = 0; + for (i < groups.len) { + groups[i].basename = pkgbase(groups[i].dir); + if (roots.len == 1 && !anyrecurse && requestidentity.len != 0) { + groups[i].basename = pkgimportbase(requestidentity); + }; + i += 1; + }; let defaultout: bool = false; if (buildonly && !buildnull && outname.len == 0 && folders.len == 1 && groups.len == 1 && strings.compare(groups[0].pkg, "main") == 0) { - outname = pkgbase(groups[0].dir); + outname = groups[0].basename; defaultout = true; }; + let defaultoutputdir: str = ""; if (defaultout && pkgstatdir(outname)) { - pkgput(os.STDERR_FILENO, "ww: build output \""); - pkgput(os.STDERR_FILENO, outname); - pkgputln(os.STDERR_FILENO, "\" already exists and is a directory"); - return 1; + defaultoutputdir = outname; }; let outputdir: bool = buildonly && explicitout && !buildnull && pkgoutputdir(outname); + let outputpatherror: bool = false; + let outputcollisionbase: str = ""; + let outputcollisiondir: str = ""; if (outputdir) { - let mainpackages: i32 = 0; - i = 0; - for (i < groups.len) { - if (strings.compare(groups[i].pkg, "main") == 0) { - mainpackages += 1; - }; - i += 1; - }; - if (mainpackages == 0) { - pkgputln(os.STDERR_FILENO, "ww: no main packages to build"); - return 1; - }; i = 0; for (i < groups.len) { if (strings.compare(groups[i].pkg, "main") == 0) { let slash: i64 = 1i64; if (strings.hassuffix(outname, "/")) { slash = 0i64; }; let outputlen: i64 = outname.len: i64 + slash - + pkgbase(groups[i].dir).len: i64; + + groups[i].basename.len: i64; if (outputlen + 1i64 > os.PATH_MAX: i64) { - pkgputln(os.STDERR_FILENO, - "ww: command output path is too long"); - return 1; + outputpatherror = true; }; let j: i32 = 0; for (j < i) { if (strings.compare(groups[j].pkg, "main") == 0 - && strings.compare(pkgbase(groups[j].dir), - pkgbase(groups[i].dir)) == 0) { - pkgput(os.STDERR_FILENO, - "ww: multiple commands produce output basename "); - pkgputquoted(os.STDERR_FILENO, pkgbase(groups[i].dir)); - pkgput(os.STDERR_FILENO, " in directory "); - pkgputquoted(os.STDERR_FILENO, outname); - pkgputln(os.STDERR_FILENO, ""); - return 1; + && strings.compare(groups[j].basename, + groups[i].basename) == 0 + && outputcollisionbase.len == 0) { + outputcollisionbase = groups[i].basename; + outputcollisiondir = outname; }; j += 1; }; @@ -2273,11 +2286,9 @@ export fn packagecommand(args: []str) int = { }; i = 0; for (i < groups.len) { - let leaf: str = pkgbase(groups[i].dir); - if (roots.len == 1 && !anyrecurse && requestidentity.len != 0) { - leaf = pkgimportbase(requestidentity); + if (!pkgstring(&groups[i].testname, groups[i].basename, ".test")) { + return 1; }; - if (!pkgstring(&groups[i].testname, leaf, ".test")) { return 1; }; groups[i].publish = ""; if (testretain && !groups[i].notests && !testnull) { if (!explicitout) { @@ -2368,6 +2379,12 @@ export fn packagecommand(args: []str) int = { && groups.len == 1 && strings.compare(groups[0].pkg, "main") != 0; plan.emitasm = emitasm; + plan.outputpatherror = outputpatherror; + plan.defaultoutputdir = defaultoutputdir; + plan.outputcollisionbase = outputcollisionbase; + plan.outputcollisiondir = outputcollisiondir; + plan.suppressbuildreports = buildonly + && (outputdir || defaultoutputdir.len != 0); append(plans, plan); let createdir: str = ""; if (buildonly && outputdir) { diff --git a/selfhost/cmd/ww/main.ww b/selfhost/cmd/ww/main.ww index c4cc5671..0a6a4115 100644 --- a/selfhost/cmd/ww/main.ww +++ b/selfhost/cmd/ww/main.ww @@ -1465,6 +1465,7 @@ type seppkg = struct { generatedtargets: []i32, ngeneratedtargets: i32, failed: bool, + action: bool, // reached by this request's semantic action list testsupport: bool, loaded: bool, exportchanged: bool, @@ -1513,6 +1514,7 @@ type sepproduct = struct { variant: i32, directoryproduct: bool, notests: bool, + buildaction: bool, // loaded product retained in the action list context: i32, root: i32, variantroot: i32, @@ -2973,6 +2975,7 @@ fn sepfindoraddvariant(g: *sepgraph, path: *u8, entry: *u8, g.pkg[g.n].generatedtargets = emptytargets; g.pkg[g.n].ngeneratedtargets = 0; g.pkg[g.n].failed = false; + g.pkg[g.n].action = false; g.pkg[g.n].testsupport = false; g.pkg[g.n].loaded = false; g.pkg[g.n].exportchanged = false; @@ -3537,7 +3540,9 @@ fn sepfname(g: *sepgraph, pi: i32, scratch: *u8, suffix: str) *u8 = { fn sepvalidateartifactpaths(g: *sepgraph, scratch: *u8) i32 = { let i: i32 = 0; for (i < g.n) { - if (g.pkg[i].failed || !g.pkg[i].loaded) { i += 1; continue; }; + if (g.pkg[i].failed || !g.pkg[i].loaded || !g.pkg[i].action) { + i += 1; continue; + }; if (g.pkg[i].storage == nil && sepassignstorage(&g.pkg[i], scratch) < 0) { return -1; }; i += 1; @@ -3547,10 +3552,12 @@ fn sepvalidateartifactpaths(g: *sepgraph, scratch: *u8) i32 = { changed = false; i = 0; for (i < g.n && !changed) { - if (g.pkg[i].failed || !g.pkg[i].loaded) { i += 1; continue; }; + if (g.pkg[i].failed || !g.pkg[i].loaded || !g.pkg[i].action) { + i += 1; continue; + }; let j: i32 = i + 1; for (j < g.n) { - if (g.pkg[j].failed || !g.pkg[j].loaded + if (g.pkg[j].failed || !g.pkg[j].loaded || !g.pkg[j].action || !cstreq(g.pkg[i].storage, g.pkg[j].storage)) { j += 1; continue; }; @@ -4350,6 +4357,7 @@ fn sepaddgeneratedmain(g: *sepgraph, product: *sepproduct, ordinal: i32, ti += 1; }; p.failed = false; + p.action = false; p.testsupport = false; p.loaded = true; p.exportchanged = false; @@ -4473,6 +4481,7 @@ fn sepclonefortest(g: *sepgraph, original: i32, owner: *u8, p.generatedtargets = emptytargets; p.ngeneratedtargets = 0; p.failed = src.failed; + p.action = false; p.testsupport = src.testsupport; p.loaded = src.loaded; p.exportchanged = false; @@ -4501,6 +4510,15 @@ fn sepclonefortest(g: *sepgraph, original: i32, owner: *u8, seppkgfreeowned(&p); return -1; }; + // A test copy is product-scoped even when its replaced source node is not + // in the final action closure. Preserve the complete-action locator without + // relying on a storage collision with that inactive source node. + p.storage = sepstoragedigest(&p); + p.storagehashed = true; + if (p.storage == nil) { + seppkgfreeowned(&p); + return -1; + }; if (src.nsources > 0) { let sources: []*u8; @@ -5856,7 +5874,7 @@ fn sepvalidateunitowner(g: *sepgraph, pi: i32, scratch: *u8) i32 = { fn sepvalidateworkdirowners(g: *sepgraph, scratch: *u8) i32 = { let i: i32 = 0; for (i < g.n) { - if (!g.pkg[i].failed && g.pkg[i].loaded + if (!g.pkg[i].failed && g.pkg[i].loaded && g.pkg[i].action && sepvalidateunitowner(g, i, scratch) < 0) { return -1; }; i += 1; }; @@ -6035,7 +6053,7 @@ fn sepvalidaterequeststaging(g: *sepgraph, scratch: *u8, warm: bool, ".a.new", ".init.unit.new", ".init.s.new", ".init.o.new"]; let pi: i32 = 0; for (pi < g.n) { - if (!g.pkg[pi].failed && g.pkg[pi].loaded) { + if (!g.pkg[pi].failed && g.pkg[pi].loaded && g.pkg[pi].action) { let si: i32 = 0; for (si < suffix.len) { let path: *u8 = sepfname(g, pi, scratch, suffix[si]); @@ -6437,6 +6455,7 @@ fn sepdiscardrequeststaging(g: *sepgraph, scratch: *u8, warm: bool, let rc: i32 = 0; let pi: i32 = 0; for (pi < g.n) { + if (!g.pkg[pi].action) { pi += 1; continue; }; let si: i32 = 0; for (si < suffix.len) { let path: *u8 = sepfname(g, pi, scratch, suffix[si]); @@ -6941,10 +6960,12 @@ fn sepfinishrequest(selfdir: *u8, l6: *u8, c6: *u8, a6: *u8, }; fn buildonesepimpl(selfdir: *u8, src: *u8, entryisdir: i32, - out: *u8, objstem: *u8, incs: *u8, lf: *lflags, - publishpackage: i32, requirecommand: i32, istest: i32, - products: *sepproduct, nproducts: i32, emitasm: i32, - workdir: *u8, createworkdir: bool, createoutputdir: *u8, + out: *u8, objstem: *u8, incs: *u8, lf: *lflags, + publishpackage: i32, requirecommand: i32, istest: i32, + products: *sepproduct, nproducts: i32, emitasm: i32, + workdir: *u8, createworkdir: bool, createoutputdir: *u8, + defaultoutputdir: *u8, outputpatherror: bool, + outputcollisionbase: *u8, outputcollisiondir: *u8, scratchout: **u8, graphout: **sepgraph) i32 = { sepfatalallocation = false; @@ -7511,9 +7532,6 @@ fn buildonesepimpl(selfdir: *u8, src: *u8, entryisdir: i32, let rootpackage: bool = istest == 0 && nproducts == 1 && !g.pkg[products[0].root].failed && !seprootiscommand(&g.pkg[products[0].root]); - if (sepvalidateartifactpaths(g, scratch) < 0) { return 1; }; - if (warm && workdirexists - && sepvalidateworkdirowners(g, scratch) < 0) { return 1; }; let ci: i32 = 0; let order: []i32; let stack: []i32; @@ -7572,6 +7590,22 @@ fn buildonesepimpl(selfdir: *u8, src: *u8, entryisdir: i32, return 1; }; }; + if (istest == 0 && createoutputdir != nil) { + let actions: i32 = 0; + producti = 0; + for (producti < nproducts) { + let root: i32 = products[producti].root; + if (g.pkg[root].failed) { return 1; }; + products[producti].buildaction = + seprootiscommand(&g.pkg[root]); + if (products[producti].buildaction) { actions += 1; }; + producti += 1; + }; + if (actions == 0) { + cerr("ww: no main packages to build\n"); + return 1; + }; + }; if (!g.pkg[products[0].root].failed && rootpackage && publishpackage != 0 && emitasm == 0 && validatepackageoutputpath(out) < 0) { return 1; }; @@ -7580,12 +7614,25 @@ fn buildonesepimpl(selfdir: *u8, src: *u8, entryisdir: i32, producti = 0; for (producti < nproducts) { let root: i32 = products[producti].root; - if (!g.pkg[root].failed) { + if (products[producti].buildaction && !g.pkg[root].failed) { if (septopovisit(g, root, order, &norder, stack, 0) < 0) { return 1; }; }; producti += 1; }; + let actionpi: i32 = 0; + for (actionpi < g.n) { + g.pkg[actionpi].action = false; + actionpi += 1; + }; + let actionoi: i32 = 0; + for (actionoi < norder) { + g.pkg[order[actionoi]].action = true; + actionoi += 1; + }; + if (sepvalidateartifactpaths(g, scratch) < 0) { return 1; }; + if (warm && workdirexists + && sepvalidateworkdirowners(g, scratch) < 0) { return 1; }; // Propagate already-known package-load failures before scratch or status // acquisition. Good sibling roots may remain viable for deterministic // staging/diagnosis, but any failure rejects publication; an entirely failed @@ -7605,12 +7652,31 @@ fn buildonesepimpl(selfdir: *u8, src: *u8, entryisdir: i32, let viableproduct: bool = false; producti = 0; for (producti < nproducts) { - if (!g.pkg[products[producti].root].failed) { + if (products[producti].buildaction + && !g.pkg[products[producti].root].failed) { viableproduct = true; }; producti += 1; }; if (!viableproduct) { return 1; }; + if (istest == 0 && emitasm == 0 && outputpatherror) { + cerr("ww: command output path is too long\n"); + return 1; + }; + if (istest == 0 && emitasm == 0 && outputcollisionbase != nil) { + cerr("ww: multiple commands produce output basename "); + sepputquoted(outputcollisionbase); + cerr(" in directory "); + sepputquoted(outputcollisiondir); + cerr("\n"); + return 1; + }; + if (istest == 0 && emitasm == 0 && defaultoutputdir != nil && nproducts == 1 + && seprootiscommand(&g.pkg[products[0].root])) { + cerrpath("ww: build output \"", defaultoutputdir, + "\" already exists and is a directory\n"); + return 1; + }; if (sepvalidaterequeststaging(g, scratch, warm, products, nproducts, rootpackage, publishpackage, emitasm, istest) < 0) { sepfreeproductstaging(products, nproducts); @@ -7622,10 +7688,8 @@ fn buildonesepimpl(selfdir: *u8, src: *u8, entryisdir: i32, let createdwork: sepcreateddirs; createdoutput.n = 0; createdwork.n = 0; - let outputmode: i32 = 448; - if (istest != 0) { outputmode = 511; }; - if (createoutputdir != nil - && sepmkdirsrecord(createoutputdir, outputmode, &createdoutput) != 0) { + if (emitasm == 0 && createoutputdir != nil + && sepmkdirsrecord(createoutputdir, 511, &createdoutput) != 0) { if (istest != 0) { cerrpath("ww: cannot create test output directory ", createoutputdir, "\n"); @@ -8149,7 +8213,8 @@ fn buildonesep(selfdir: *u8, src: *u8, entryisdir: i32, objstem: *u8, incs: *u8, lf: *lflags, publishpackage: i32, requirecommand: i32, istest: i32, rootvariant: i32, testpackage: *u8, emitasm: i32, - keepscratch: i32, workdir: *u8) i32 = { + keepscratch: i32, workdir: *u8, createoutputdir: *u8, + defaultoutputdir: *u8, outputpatherror: bool) i32 = { let scratch: *u8 = nil; let g: *sepgraph = nil; let product: sepproduct; @@ -8169,12 +8234,15 @@ fn buildonesep(selfdir: *u8, src: *u8, entryisdir: i32, product.variant = rootvariant; product.directoryproduct = false; product.notests = false; + product.buildaction = true; product.root = -1; product.variantroot = -1; product.support = -1; let r: i32 = buildonesepimpl(selfdir, src, entryisdir, out, objstem, incs, lf, publishpackage, requirecommand, istest, &product, 1, - emitasm, workdir, false, nil, &scratch, &g); + emitasm, workdir, false, createoutputdir, defaultoutputdir, + outputpatherror, nil, nil, + &scratch, &g); sepgraphfree(g); if (keepscratch == 0 && scratch != nil) { if (cstrendswithlit(scratch, ".sepwork")) { @@ -8205,7 +8273,9 @@ fn buildonesep(selfdir: *u8, src: *u8, entryisdir: i32, fn buildpackagetests(selfdir: *u8, src: *u8, rootidentity: *u8, incs: *u8, workdir: *u8, products: *sepproduct, nproducts: i32, istest: i32, publishpackage: i32, lf: *lflags, emitasm: i32, - createworkdir: bool, createoutputdir: *u8) i32 = { + createworkdir: bool, createoutputdir: *u8, defaultoutputdir: *u8, + outputpatherror: bool, outputcollisionbase: *u8, + outputcollisiondir: *u8) i32 = { let scratch: *u8 = nil; let g: *sepgraph = nil; let i: i32 = 0; @@ -8217,7 +8287,8 @@ fn buildpackagetests(selfdir: *u8, src: *u8, rootidentity: *u8, products[0].out, products[0].out, incs, lf, publishpackage, 0, istest, products, nproducts, emitasm, workdir, createworkdir, - createoutputdir, &scratch, &g); + createoutputdir, defaultoutputdir, outputpatherror, + outputcollisionbase, outputcollisiondir, &scratch, &g); sepgraphfree(g); return r; }; @@ -8313,7 +8384,7 @@ fn resolvemodule(selfdir: *u8, name: *u8, incs: *u8, isdir: *i32) *u8 = { }; fn writeusage(fd: i32) void = { - let s: str = "usage: ww [-V] [args...]\n -V print version and exit\n build [-S] [-w DIR] [-I DIR] [-o FILE] [path ...] build local package graphs\n run [path] ... build then exec, passing extra args to the program\n test [-S -o STEM] [-w DIR] [options] [path ...] build/run tests; -S emits package asm\n version print version and exit\n\n path forms:\n foo.ww literal file\n foo search cwd, -I dirs, then the source library for foo.ww or foo/\n lib/foo directory: build its package sources\n -o publishes a non-main archive FILE + FILE.wwi\n lib/... every eligible package under lib, recursively\n . build the cwd's .ww\n"; + let s: str = "usage: ww [-V] [args...]\n -V print version and exit\n build [-S] [-w DIR] [-I DIR] [-o FILE|DIR] [path ...] build local package graphs\n run [path] ... build then exec, passing extra args to the program\n test [-S -o STEM] [-w DIR] [options] [path ...] build/run tests; -S emits package asm\n version print version and exit\n\n path forms:\n foo.ww literal file\n foo search cwd, -I dirs, then the source library for foo.ww or foo/\n lib/foo directory: build its package sources\n -o FILE publishes a non-main archive FILE + FILE.wwi\n -o DIR publishes each selected command beneath DIR\n lib/... every eligible package under lib, recursively\n . build the cwd's .ww\n"; os.write(fd, s.ptr, s.len: u64); }; @@ -8352,6 +8423,54 @@ fn defaultoutpath(src: *u8) *u8 = { return out.ptr; }; +// Go's build -o directory branch follows an existing destination through +// stat, and a trailing platform separator declares a directory which the +// request may need to create. WW's platform separator is '/'. +fn buildoutputdir(path: *u8) bool = { + if (cstrendswithlit(path, "/")) { return true; }; + let fi: os.filestat; + match (os.stat(&fi, pathstr(path))) { + case void => { + let typ: u32 = (fi.mode: u32) & 61440u32; + return typ == os.mode.DIR: u32; + }; + case let e: os.oserror => return false; + }; +}; + +fn buildoutputpath(dir: *u8, src: *u8) *u8 = { + let base: *u8 = defaultoutpath(src); + let need: u64 = 0u64; + if (!sepaddbytes(&need, cstrlen(dir)) + || (!cstrendswithlit(dir, "/") && !sepaddbytes(&need, 1u64)) + || !sepaddbytes(&need, cstrlen(base)) + || !sepaddbytes(&need, 1u64) + || need > os.PATH_MAX: u64) { + return nil; + }; + if (cstrendswithlit(dir, "/")) { + return sepappendlit(dir, pathstr(base)); + }; + return sepjoinpath(dir, base); +}; + +fn defaultimportoutpath(identity: *u8) *u8 = { + let n: u64 = cstrlen(identity); + let start: u64 = 0u64; + let i: u64 = 0u64; + for (i < n) { + if (identity[i] == 46u8) { start = i + 1u64; }; + i += 1u64; + }; + let out: []u8 = alloc([], (os.PATH_MAX: u64))!; + out.len = os.PATH_MAX; + let off: u64 = 0u64; + i = start; + for (i < n) { out[off] = identity[i]; off += 1u64; i += 1u64; }; + cstrseal(out.ptr, off); + return out.ptr; +}; + fn dobuild(selfdir: *u8, argv: **u8, argc: i32, start: i32) i32 = { let src: *u8 = nil; let srcindex: i32 = -1; @@ -8504,11 +8623,34 @@ fn dobuild(selfdir: *u8, argv: **u8, argc: i32, start: i32) i32 = { let objstem: *u8 = nil; let discardoutput: bool = outflag != nil && cstreqlit(outflag, "/dev/null"); + let outputdir: bool = outflag != nil && outflag[0u64] != 0u8 + && !discardoutput && buildoutputdir(outflag); + let rootidentity: *u8 = nil; + if (!requestedliteral && isdir != 0) { rootidentity = src; }; + if (outputdir && isdir != 0) { + let coordinatortarget: *u8 = resolved; + if (srcindex < 0) { coordinatortarget = nil; }; + return execpackagetests(selfdir, argv, argc, start, srcindex, + coordinatortarget, rootidentity, srcindex < 0, true); + }; + let createoutputdir: *u8 = nil; + let outputpatherror: bool = false; if (outflag != nil && outflag[0u64] != 0u8 && !discardoutput) { // -o sets both the binary path and the intermediate stem so // artifacts land beside the requested output (T3). - out = outflag; - objstem = outflag; + if (outputdir) { + out = buildoutputpath(outflag, resolved); + if (out == nil) { + outputpatherror = true; + out = defaultoutpath(resolved); + }; + createoutputdir = outflag; + } else { + out = outflag; + }; + objstem = out; + } else { if (isdir != 0 && rootidentity != nil) { + out = defaultimportoutpath(rootidentity); } else { if (isdir != 0) { let rlen: u64 = cstrlen(resolved); for (rlen > 1u64) { @@ -8525,14 +8667,16 @@ fn dobuild(selfdir: *u8, argv: **u8, argc: i32, start: i32) i32 = { cstrseal(out, off); } else { out = defaultoutpath(resolved); - }; }; + }; }; }; + let defaultoutputdir: *u8 = nil; + if ((outflag == nil || outflag[0u64] == 0u8) && buildoutputdir(out)) { + defaultoutputdir = out; + }; let lf: lflags; lf.libdirs = &libdirs[0]; lf.nlibdirs = nlibdirs; lf.libs = &libs[0]; lf.nlibs = nlibs; - let rootidentity: *u8 = nil; - if (!requestedliteral && isdir != 0) { rootidentity = src; }; if (discardoutput) { let tmp: []u8 = alloc([], (os.PATH_MAX: u64))!; tmp.len = os.PATH_MAX; @@ -8547,7 +8691,7 @@ fn dobuild(selfdir: *u8, argv: **u8, argc: i32, start: i32) i32 = { let rc: i32 = buildonesep(selfdir, resolved, isdir, rootidentity, outp, outp, incs.ptr, &lf, 0i32, 0i32, 0i32, SEP_VARIANT_PRODUCTION, nil, - emitasm, 0i32, workdir); + emitasm, 0i32, workdir, nil, nil, false); let cleanbad: bool = false; let cleanrc: i32 = os.remove(pathstr(outp)); if (cleanrc != 0 && cleanrc != -2i32) { @@ -8566,7 +8710,8 @@ fn dobuild(selfdir: *u8, argv: **u8, argc: i32, start: i32) i32 = { return buildonesep(selfdir, resolved, isdir, rootidentity, out, objstem, incs.ptr, &lf, publishpackage, 0i32, 0i32, SEP_VARIANT_PRODUCTION, nil, - emitasm, 1i32, workdir); + emitasm, 1i32, workdir, createoutputdir, defaultoutputdir, + outputpatherror); }; // Format the owned driver workspace /tmp/ into buf. Pid is @@ -8748,7 +8893,7 @@ fn dorun(selfdir: *u8, argv: **u8, argc: i32, start: i32) i32 = { if (buildonesep(selfdir, resolved, isdir, rootidentity, outp, outp, incs.ptr, &lf, 0i32, 1i32, 0i32, SEP_VARIANT_PRODUCTION, nil, - 0i32, 0i32, nil) != 0) { + 0i32, 0i32, nil, nil, nil, false) != 0) { let cleanrc: i32 = os.remove(pathstr(outp)); if (cleanrc != 0 && cleanrc != -2i32) { cerr("ww: cannot remove temporary output\n"); @@ -8841,7 +8986,7 @@ fn runsingletest(selfdir: *u8, src: *u8, incs: *u8, compileonly: i32, if (retainout) { keep = 1; }; let bres: i32 = buildonesep(selfdir, src, 0, nil, outp, objstem, incs, &lf, 0i32, 0i32, 1i32, SEP_VARIANT_PRODUCTION, nil, - emitasm, keep, workdir); + emitasm, keep, workdir, nil, nil, false); if (bres != 0) { if (owntmp) { let cleanrc: i32 = os.remove(pathstr(outp)); @@ -8942,6 +9087,10 @@ fn dotest(selfdir: *u8, argv: **u8, argc: i32, start: i32) i32 = { let packagepublish: bool = false; let packagecreateworkdir: bool = false; let packagecreateoutputdir: *u8 = nil; + let packageoutputpatherror: bool = false; + let packagedefaultoutputdir: *u8 = nil; + let packageoutputcollisionbase: *u8 = nil; + let packageoutputcollisiondir: *u8 = nil; let maxpackagelflags: i32 = 32; let packagelibdirs: [32]*u8; let packagelibs: [32]*u8; @@ -9008,6 +9157,37 @@ fn dotest(selfdir: *u8, argv: **u8, argc: i32, start: i32) i32 = { i += 2; continue; }; + if (cstreqlit(p, "--ww-command-output-path-error")) { + if (packageoutputpatherror) { + cerr("ww test: invalid --ww-command-output-path-error\n"); + return 2; + }; + packageoutputpatherror = true; + i += 1; + continue; + }; + if (cstreqlit(p, "--ww-default-output-dir")) { + if (i + 1 >= argc || packagedefaultoutputdir != nil + || argv[i + 1][0u64] == 0u8) { + cerr("ww test: invalid --ww-default-output-dir\n"); + return 2; + }; + packagedefaultoutputdir = argv[i + 1]; + i += 2; + continue; + }; + if (cstreqlit(p, "--ww-command-output-collision")) { + if (i + 2 >= argc || packageoutputcollisionbase != nil + || argv[i + 1][0u64] == 0u8 + || argv[i + 2][0u64] == 0u8) { + cerr("ww test: invalid --ww-command-output-collision\n"); + return 2; + }; + packageoutputcollisionbase = argv[i + 1]; + packageoutputcollisiondir = argv[i + 2]; + i += 3; + continue; + }; if (cstreqlit(p, "--ww-package-test")) { if (i + 9 >= argc) { cerr("ww test: --ww-package-test needs kind, package, production, internal, external, directory, output, publication, and status\n"); @@ -9067,6 +9247,7 @@ fn dotest(selfdir: *u8, argv: **u8, argc: i32, start: i32) i32 = { if (buildproduct) { product.variant = SEP_VARIANT_PRODUCTION; }; product.directoryproduct = true; product.notests = testproduct && !hasinternal && !hasexternal; + product.buildaction = true; product.root = -1; product.variantroot = -1; product.productionroot = -1; @@ -9243,6 +9424,12 @@ fn dotest(selfdir: *u8, argv: **u8, argc: i32, start: i32) i32 = { cerr("ww test: invalid private directory creation\n"); return 2; }; + if ((packageoutputpatherror || packagedefaultoutputdir != nil + || packageoutputcollisionbase != nil) + && (!packagebuild || products.len == 0)) { + cerr("ww test: invalid private output preflight\n"); + return 2; + }; if (packagecreateworkdir && products.len == 0) { cerr("ww test: invalid private directory creation\n"); return 2; @@ -9388,7 +9575,9 @@ fn dotest(selfdir: *u8, argv: **u8, argc: i32, start: i32) i32 = { incs.ptr, workdir, products.ptr, products.len, packagemode, publishmode, &packagelinks, emitasm, packagecreateworkdir, - packagecreateoutputdir); + packagecreateoutputdir, packagedefaultoutputdir, + packageoutputpatherror, packageoutputcollisionbase, + packageoutputcollisiondir); }; let replacement: *u8 = nil; if (resolved != target) { replacement = resolved; }; diff --git a/test/package/package_test.ww b/test/package/package_test.ww index be533d8f..d4a60ba9 100644 --- a/test/package/package_test.ww +++ b/test/package/package_test.ww @@ -512,6 +512,36 @@ fn directoryhasnew(path: str) bool = { return false; }; +fn directoryhasfragment(path: str, needle: str) bool = { + let fd: i32 = os.open(path, os.flag.RDONLY, 0i32); + assert(fd >= 0); + let buf: []u8 = alloc([], 16384u64)!; + buf.len = 16384; + let n: i64 = os.getdents64(fd, buf.ptr, buf.len: u64); + for (n > 0i64) { + let off: i32 = 0; + for (off < n: i32) { + let reclen: i32 = (buf[off + 16]: i32) + + ((buf[off + 17]: i32) * 256); + assert(reclen >= 20 && off + reclen <= n: i32); + let len: i32 = 0; + for (buf[off + 19 + len] != 0u8) { len += 1; }; + let name: str; + name.ptr = buf.ptr + (off + 19): u64; + name.len = len; + if (has(name, needle)) { + assert(os.close(fd) == 0); + return true; + }; + off += reclen; + }; + n = os.getdents64(fd, buf.ptr, buf.len: u64); + }; + assert(n == 0i64); + assert(os.close(fd) == 0); + return false; +}; + fn directoryisempty(path: str) bool = { let fd: i32 = os.open(path, os.flag.RDONLY, 0i32); assert(fd >= 0); @@ -7966,12 +7996,9 @@ fn cwdwritedata(dir: str, label: str) void = { (120i64 * (time.second: i64)): time.duration, &out); expectexit(&out, 1); assert(out.stdout.len == 0); - let creationfullwant: str = creationwant; - if (ci != 0) { - creationfullwant = strings.concat(creationwant, "FAIL ", - foldcreation, " [main] (build exit 1)\n"); - }; - assert(same(out.stderr, creationfullwant)); + // Build-output failures are emitted by the shared driver after its + // load/check boundary; the coordinator adds no test-style FAIL line. + assert(same(out.stderr, creationwant)); assert(readfile(compilertrace).len == 0); assert(readfile(assemblertrace).len == 0); assert(readfile(linkertrace).len == 0); @@ -12654,6 +12681,963 @@ fn runtimepath(relative: str) str = { clean(root); }; +// A single directory root used to bypass the package coordinator and treat +// every non-null -o spelling as one file. Exercise the Go output-directory +// branch at that dispatch boundary while the established graph transaction +// remains responsible for actions, publication, and rollback. +@test fn single_root_build_output_directory() void = { + let root: str = fresh(); + let source: str = strings.concat(root, "/source"); + let base: str = strings.concat(source, "/base"); + let alpha: str = strings.concat(source, "/alpha"); + let library: str = strings.concat(source, "/library"); + let bad: str = strings.concat(source, "/bad"); + let badlibrary: str = strings.concat(source, "/badlibrary"); + let duplicateone: str = strings.concat(source, "/duplicate-one/tool"); + let duplicatetwo: str = strings.concat(source, "/duplicate-two/tool"); + let duplicatethree: str = strings.concat(source, "/duplicate-three/tool"); + mkdirall(base); mkdirall(alpha); mkdirall(library); mkdirall(bad); + mkdirall(badlibrary); mkdirall(duplicateone); mkdirall(duplicatetwo); + mkdirall(duplicatethree); + writefile(strings.concat(base, "/base.ww"), strings.concat( + "package base;\n", + "export fn value() i32 = { return 4; };\n")); + let alphafile: str = strings.concat(alpha, "/main.ww"); + let alphaoriginal: str = strings.concat( + "package main;\nimport base;\n", + "fn main() i32 = { return base.value(); };\n"); + let alphachanged: str = strings.concat( + "package main;\nimport base;\n", + "fn main() i32 = { return base.value() + 1; };\n"); + let alphafailing: str = strings.concat( + "package main;\nimport base;\n", + "fn main() i32 = { return base.value() + 2; };\n"); + writefile(alphafile, alphaoriginal); + writefile(strings.concat(alpha, "/main_test.ww"), + "package main;\n@test fn valid() void = { assert(true); };\n"); + writefile(strings.concat(library, "/library.ww"), strings.concat( + "package renamed;\n", + "export fn value() i32 = { return 7; };\n")); + writefile(strings.concat(bad, "/main.ww"), strings.concat( + "package main;\nimport _ missing.pkg;\n", + "fn main() i32 = { return 0; };\n")); + writefile(strings.concat(badlibrary, "/library.ww"), strings.concat( + "package renamed;\nimport _ missing.pkg;\n", + "export fn value() i32 = { return 0; };\n")); + writefile(strings.concat(duplicateone, "/main.ww"), + "package main;\nfn main() i32 = { return 12; };\n"); + writefile(strings.concat(duplicatetwo, "/main.ww"), strings.concat( + "package main;\nimport _ missing.pkg;\n", + "fn main() i32 = { return 13; };\n")); + writefile(strings.concat(duplicatethree, "/main.ww"), + "package main;\nfn main() i32 = { return 14; };\n"); + let logical: str = strings.concat(source, "/logical"); + assert(os.symlink(alpha, logical) == 0); + let raw: str = strings.concat(source, "/raw.ww"); + writefile(raw, "package main;\nfn main() i32 = { return 9; };\n"); + let rawlibrary: str = strings.concat(source, "/rawlibrary.ww"); + writefile(rawlibrary, + "package renamed;\nexport fn value() i32 = { return 10; };\n"); + let rawbad: str = strings.concat(source, "/rawbad.ww"); + writefile(rawbad, strings.concat( + "package main;\nimport _ missing.pkg;\n", + "fn main() i32 = { return 11; };\n")); + let longoutputbytes: []u8 = alloc([], os.PATH_MAX: u64)!; + let longi: i32 = 0; + for (longi < root.len) { + append(longoutputbytes, root[longi]); longi += 1; + }; + append(longoutputbytes, '/': u8); + for (longoutputbytes.len < os.PATH_MAX - 2) { + append(longoutputbytes, 'o': u8); + }; + append(longoutputbytes, '/': u8); + assert(longoutputbytes.len == os.PATH_MAX - 1); + let longoutputdir: str = strings.frombytes(longoutputbytes); + + let compilerwrapper: str = strings.concat(root, "/output-dir-w6c.sh"); + let assemblerwrapper: str = strings.concat(root, "/output-dir-w6a.sh"); + let linkerwrapper: str = strings.concat(root, "/output-dir-w6l.sh"); + writeexecutable(compilerwrapper, strings.concat( + "#!/bin/sh\nprintf 'compile %s\\n' \"$*\" >> \"$WW_ODIR_CTRACE\"\n", + "if test \"$WW_ODIR_COMPILE_MODE\" = fail; then\n", + " printf 'injected output-directory compiler failure\\n' >&2\n", + " exit 96\nfi\nexec \"$WW_ODIR_REAL_C\" \"$@\"\n")); + writeexecutable(assemblerwrapper, strings.concat( + "#!/bin/sh\nprintf 'assemble %s\\n' \"$*\" >> \"$WW_ODIR_ATRACE\"\n", + "exec \"$WW_ODIR_REAL_A\" \"$@\"\n")); + writeexecutable(linkerwrapper, strings.concat( + "#!/bin/sh\nprintf 'link %s\\n' \"$*\" >> \"$WW_ODIR_LTRACE\"\n", + "case \"$WW_ODIR_LINK_MODE\" in\n", + " fail) printf 'injected output-directory linker failure\\n' >&2;", + " exit 97;;\n", + " signal) kill -TERM \"$$\";;\nesac\n", + "exec \"$WW_ODIR_REAL_L\" \"$@\"\n")); + + let stages: []str = ["ww", "ww_ww"]; + let compilers: []str = ["w6c", "w6c_ww"]; + let assemblers: []str = ["w6a", "w6a_ww"]; + let linkers: []str = ["w6l", "w6l_ww"]; + let tags: []str = ["c", "ww"]; + let umasklauncher: str = driver("package-umaskexec"); + let baseenv: []str = os.getenvs(); + let routebinref: str = ""; + let rawbinref: str = ""; + let testbinref: str = ""; + let changedbinref: str = ""; + let asmref: str = ""; + let nomainref: str = ""; + let importref: str = ""; + let implicitref: str = ""; + let implicitrawref: str = ""; + let implicitimportref: str = ""; + let compilerfailref: str = ""; + let linkerfailref: str = ""; + let linkersignalref: str = ""; + let rawrollbackref: str = ""; + let rawnonmainref: str = ""; + let rawlongref: str = ""; + let rawlongimportref: str = ""; + let dirlongref: str = ""; + let dirlongimportref: str = ""; + let mixedimportref: str = ""; + let nomainimportref: str = ""; + let duplicateimportref: str = ""; + let duplicateref: str = ""; + let recursiveimplicitref: str = ""; + let recursiveimplicitimportref: str = ""; + let coldctraceref: str = ""; + let coldatraceref: str = ""; + let coldltraceref: str = ""; + let warmltraceref: str = ""; + let changedctraceref: str = ""; + let changedatraceref: str = ""; + let changedltraceref: str = ""; + let suffixes: []str = [".unit.ww", ".wwi", ".s", ".o", ".a", + ".init.unit.ww", ".init.s", ".init.o"]; + let artifactrefs: []str = ["", "", "", "", "", "", "", ""]; + let out: commandout; + let si: i32 = 0; + for (si < stages.len) { + rewritefile(alphafile, alphaoriginal); + let ctrace: str = strings.concat(root, "/", tags[si], "-odir-c.trace"); + let atrace: str = strings.concat(root, "/", tags[si], "-odir-a.trace"); + let ltrace: str = strings.concat(root, "/", tags[si], "-odir-l.trace"); + writefile(ctrace, ""); writefile(atrace, ""); writefile(ltrace, ""); + let env: []str = alloc([], (baseenv.len + 11): u64)!; + let ei: i32 = 0; + for (ei < baseenv.len) { + if (!strings.hasprefix(baseenv[ei], "WW_W6C=") + && !strings.hasprefix(baseenv[ei], "WW_W6A=") + && !strings.hasprefix(baseenv[ei], "WW_W6L=") + && !strings.hasprefix(baseenv[ei], "WW_ODIR_")) { + append(env, baseenv[ei]); + }; + ei += 1; + }; + append(env, strings.concat("WW_W6C=", compilerwrapper)); + append(env, strings.concat("WW_W6A=", assemblerwrapper)); + append(env, strings.concat("WW_W6L=", linkerwrapper)); + append(env, strings.concat("WW_ODIR_CTRACE=", ctrace)); + append(env, strings.concat("WW_ODIR_ATRACE=", atrace)); + append(env, strings.concat("WW_ODIR_LTRACE=", ltrace)); + append(env, strings.concat("WW_ODIR_REAL_C=", driver(compilers[si]))); + append(env, strings.concat("WW_ODIR_REAL_A=", driver(assemblers[si]))); + append(env, strings.concat("WW_ODIR_REAL_L=", driver(linkers[si]))); + let compilemodeindex: i32 = env.len; + append(env, "WW_ODIR_COMPILE_MODE="); + let linkmodeindex: i32 = env.len; + append(env, "WW_ODIR_LINK_MODE="); + + // An existing directory without a trailing slash remains a directory. + // The literal root's path leaf, not its declared name or source file, + // names the command; unrelated directory contents survive publication. + let existing: str = strings.concat(root, "/existing-", tags[si]); + assert(os.mkdir(existing, 448i32) == 0); + writefile(strings.concat(existing, "/sentinel"), "existing-sentinel\n"); + let existingav: []str = [driver(stages[si]), "build", "-I", source, + "-o", existing, alpha]; + runcommandenvdir(root, strings.concat("odir-existing-", tags[si]), + existingav, env, root, + (120i64 * (time.second: i64)): time.duration, &out); + expectexit(&out, 0); + assert(out.stdout.len == 0 && out.stderr.len == 0); + assert(same(readfile(strings.concat(existing, "/sentinel")), + "existing-sentinel\n")); + let existingbin: str = strings.concat(existing, "/alpha"); + assert(os.exists(existingbin)); + let routebytes: str = readfile(existingbin); + if (si == 0) { routebinref = strings.dup(routebytes); } + else { assert(same(routebinref, routebytes)); }; + let runav: []str = [existingbin]; + runcommand(root, strings.concat("odir-existing-run-", tags[si]), + runav, time.second, &out); + expectexit(&out, 4); + assert(!directoryhasnew(existing)); + + // A missing trailing-slash hierarchy is created, and logical package + // resolution retains the same command identity and bytes. + let missingroot: str = strings.concat(root, "/missing-", tags[si]); + let missingout: str = strings.concat(missingroot, "/nested/"); + let missingav: []str = [driver(stages[si]), "build", "-I", source, + "-o", missingout, "alpha"]; + runcommandenvdir(root, strings.concat("odir-missing-", tags[si]), + missingav, env, root, + (120i64 * (time.second: i64)): time.duration, &out); + expectexit(&out, 0); + assert(out.stdout.len == 0 && out.stderr.len == 0); + assert(same(routebytes, readfile(strings.concat(missingout, "alpha")))); + + // A contextual request's final import component is presentation + // metadata. Canonicalizing a symlink target must not leak the physical + // directory leaf into explicit or implicit command names. + let logicalout: str = strings.concat(root, "/logical-output-", tags[si]); + assert(os.mkdir(logicalout, 448i32) == 0); + let logicalav: []str = [driver(stages[si]), "build", "-I", source, + "-o", logicalout, "logical"]; + runcommandenvdir(root, strings.concat("odir-logical-", tags[si]), + logicalav, env, root, + (120i64 * (time.second: i64)): time.duration, &out); + expectexit(&out, 0); + assert(same(routebytes, + readfile(strings.concat(logicalout, "/logical")))); + assert(!os.exists(strings.concat(logicalout, "/alpha"))); + let logicalcaller: str = strings.concat(root, "/logical-caller-", + tags[si]); + let logicalwork: str = strings.concat(root, "/logical-work-", + tags[si]); + mkdirall(logicalcaller); mkdirall(logicalwork); + let logicaldefaultav: []str = [driver(stages[si]), "build", "-w", + logicalwork, "-I", source, "logical"]; + runcommandenvdir(root, strings.concat("odir-logical-default-", tags[si]), + logicaldefaultav, env, logicalcaller, + (120i64 * (time.second: i64)): time.duration, &out); + expectexit(&out, 0); + assert(same(routebytes, + readfile(strings.concat(logicalcaller, "/logical")))); + assert(!os.exists(strings.concat(logicalcaller, "/alpha"))); + + // BuildInstallFunc creates a missing caller output parent from 0777; + // the launcher fixes umask at 000 so every minted prefix is observable. + let modeparent: str = strings.concat(root, "/mode-parent-", tags[si]); + let modeout: str = strings.concat(modeparent, "/nested/"); + let modeav: []str = [umasklauncher, "000", driver(stages[si]), + "build", "-I", source, "-o", modeout, "alpha"]; + runcommandenvdir(root, strings.concat("odir-mode-", tags[si]), modeav, + env, root, (120i64 * (time.second: i64)): time.duration, &out); + expectexit(&out, 0); + assert(permissionmode(modeparent) == 511u32); + assert(permissionmode(strings.concat(modeparent, "/nested")) == 511u32); + assert(permissionmode(strings.concat(modeout, "alpha")) == 511u32); + assert(same(routebytes, readfile(strings.concat(modeout, "alpha")))); + + // stat follows a symlinked output directory. With no explicit root the + // default dot is inserted into the delegated request exactly once. + let symlinktarget: str = strings.concat(root, "/symlink-target-", + tags[si]); + let symlinkout: str = strings.concat(root, "/symlink-out-", tags[si]); + assert(os.mkdir(symlinktarget, 448i32) == 0); + writefile(strings.concat(symlinktarget, "/sentinel"), + "symlink-sentinel\n"); + assert(os.symlink(symlinktarget, symlinkout) == 0); + let defaultav: []str = [driver(stages[si]), "build", "-I", source, + "-o", symlinkout]; + runcommandenvdir(root, strings.concat("odir-default-", tags[si]), + defaultav, env, alpha, + (120i64 * (time.second: i64)): time.duration, &out); + expectexit(&out, 0); + assert(out.stdout.len == 0 && out.stderr.len == 0); + assert(same(readfile(strings.concat(symlinktarget, "/sentinel")), + "symlink-sentinel\n")); + assert(same(routebytes, + readfile(strings.concat(symlinkout, "/alpha")))); + + // Raw command-line files remain driver-owned. Existing and missing + // output directories both derive the source basename without .ww. + let rawexisting: str = strings.concat(root, "/raw-existing-", tags[si]); + assert(os.mkdir(rawexisting, 448i32) == 0); + writefile(strings.concat(rawexisting, "/sentinel"), "raw-sentinel\n"); + let rawexistingav: []str = [driver(stages[si]), "build", "-o", + rawexisting, raw]; + runcommandenvdir(root, strings.concat("odir-raw-existing-", tags[si]), + rawexistingav, env, root, + (120i64 * (time.second: i64)): time.duration, &out); + expectexit(&out, 0); + let rawbin: str = strings.concat(rawexisting, "/raw"); + let rawbytes: str = readfile(rawbin); + if (si == 0) { rawbinref = strings.dup(rawbytes); } + else { assert(same(rawbinref, rawbytes)); }; + let rawrun: []str = [rawbin]; + runcommand(root, strings.concat("odir-raw-run-", tags[si]), rawrun, + time.second, &out); + expectexit(&out, 9); + assert(same(readfile(strings.concat(rawexisting, "/sentinel")), + "raw-sentinel\n")); + let rawmissingroot: str = strings.concat(root, "/raw-missing-", + tags[si]); + let rawmissingout: str = strings.concat(rawmissingroot, "/nested/"); + let rawmissingav: []str = [driver(stages[si]), "build", "-o", + rawmissingout, raw]; + runcommandenvdir(root, strings.concat("odir-raw-missing-", tags[si]), + rawmissingav, env, root, + (120i64 * (time.second: i64)): time.duration, &out); + expectexit(&out, 0); + assert(same(rawbytes, readfile(strings.concat(rawmissingout, "raw")))); + + // The directory form requests command actions, not an archive name. + // A raw non-main root therefore rejects after loading and before tools or + // caller-directory creation, just like a directory-package root. + rewritefile(ctrace, ""); rewritefile(atrace, ""); rewritefile(ltrace, ""); + let rawlibroot: str = strings.concat(root, "/raw-library-", tags[si]); + let rawlibout: str = strings.concat(rawlibroot, "/nested/"); + let rawlibav: []str = [driver(stages[si]), "build", "-o", rawlibout, + rawlibrary]; + runcommandenvdir(root, strings.concat("odir-raw-library-", tags[si]), + rawlibav, env, root, time.second, &out); + expectexit(&out, 1); + assert(same(out.stderr, "ww: no main packages to build\n")); + assert(readfile(ctrace).len == 0 && readfile(atrace).len == 0 + && readfile(ltrace).len == 0); + assert(!os.exists(rawlibroot)); + if (si == 0) { rawnonmainref = strings.dup(out.stderr); } + else { assert(same(rawnonmainref, out.stderr)); }; + + // Raw destination joining is output preflight, not loading. A bad import + // therefore wins over the derived PATH_MAX failure; a valid command then + // reports that path error before tools or directory creation. + let rawlongbadav: []str = [driver(stages[si]), "build", "-o", + longoutputdir, rawbad]; + runcommandenvdir(root, strings.concat("odir-raw-long-import-", tags[si]), + rawlongbadav, env, root, time.second, &out); + expectexit(&out, 1); + assert(has(out.stderr, "cannot find package missing.pkg\n")); + assert(!has(out.stderr, "command output path is too long")); + assert(readfile(ctrace).len == 0 && readfile(atrace).len == 0 + && readfile(ltrace).len == 0); + if (si == 0) { rawlongimportref = strings.dup(out.stderr); } + else { assert(same(rawlongimportref, out.stderr)); }; + let rawlongav: []str = [driver(stages[si]), "build", "-o", + longoutputdir, raw]; + runcommandenvdir(root, strings.concat("odir-raw-long-", tags[si]), + rawlongav, env, root, time.second, &out); + expectexit(&out, 1); + assert(same(out.stderr, "ww: command output path is too long\n")); + assert(readfile(ctrace).len == 0 && readfile(atrace).len == 0 + && readfile(ltrace).len == 0); + if (si == 0) { rawlongref = strings.dup(out.stderr); } + else { assert(same(rawlongref, out.stderr)); }; + + // The coordinator also defers its derived command destination until the + // shared driver has loaded every import. The valid twin pins the retained + // PATH_MAX rejection after the same no-tool boundary. + rewritefile(ctrace, ""); rewritefile(atrace, ""); rewritefile(ltrace, ""); + let dirlongbadav: []str = [driver(stages[si]), "build", "-I", source, + "-o", longoutputdir, bad]; + runcommandenvdir(root, strings.concat("odir-directory-long-import-", + tags[si]), dirlongbadav, env, root, time.second, &out); + expectexit(&out, 1); + assert(has(out.stderr, "cannot find package missing.pkg\n")); + assert(!has(out.stderr, "command output path is too long")); + assert(readfile(ctrace).len == 0 && readfile(atrace).len == 0 + && readfile(ltrace).len == 0); + if (si == 0) { dirlongimportref = strings.dup(out.stderr); } + else { assert(same(dirlongimportref, out.stderr)); }; + let dirlongav: []str = [driver(stages[si]), "build", "-I", source, + "-o", longoutputdir, alpha]; + runcommandenvdir(root, strings.concat("odir-directory-long-", tags[si]), + dirlongav, env, root, time.second, &out); + expectexit(&out, 1); + assert(same(out.stderr, "ww: command output path is too long\n")); + assert(readfile(ctrace).len == 0 && readfile(atrace).len == 0 + && readfile(ltrace).len == 0); + if (si == 0) { dirlongref = strings.dup(out.stderr); } + else { assert(same(dirlongref, out.stderr)); }; + + // Test retains its already-established directory semantics; dispatching + // build through the coordinator must not disturb the adjacent branch. + let testout: str = strings.concat(root, "/test-output-", tags[si]); + assert(os.mkdir(testout, 448i32) == 0); + let testav: []str = [driver(stages[si]), "test", "-c", "-I", source, + "-o", testout, alpha]; + runcommandenvdir(root, strings.concat("odir-test-control-", tags[si]), + testav, env, root, + (120i64 * (time.second: i64)): time.duration, &out); + expectexit(&out, 0); + let testbin: str = strings.concat(testout, "/alpha.test"); + let testbytes: str = readfile(testbin); + if (si == 0) { testbinref = strings.dup(testbytes); } + else { assert(same(testbinref, testbytes)); }; + + // Repeated command roots deduplicate. A mixed independent non-main root + // is loaded but omitted from the command-only action list; dependencies + // of the retained command still build normally. + rewritefile(ctrace, ""); rewritefile(atrace, ""); rewritefile(ltrace, ""); + let mixedout: str = strings.concat(root, "/mixed-", tags[si]); + let mixedwork: str = strings.concat(root, "/mixed-work-", tags[si]); + assert(os.mkdir(mixedout, 448i32) == 0); + let mixedav: []str = [driver(stages[si]), "build", "-w", mixedwork, + "-I", source, + "-o", mixedout, alpha, alpha, library]; + runcommandenvdir(root, strings.concat("odir-mixed-", tags[si]), + mixedav, env, root, + (120i64 * (time.second: i64)): time.duration, &out); + expectexit(&out, 0); + assert(os.exists(strings.concat(mixedout, "/alpha"))); + assert(!os.exists(strings.concat(mixedout, "/library"))); + assert(occurrences(readfile(ctrace), "\n") == 2); + assert(occurrences(readfile(atrace), "\n") == 3); + assert(occurrences(readfile(ltrace), "\n") == 1); + assert(!has(readfile(ctrace), "library.unit.ww")); + assert(!os.exists(strings.concat(mixedwork, "/library.a"))); + + // Filtering occurs only after loading every selected root. A structural + // import failure in an otherwise skipped non-main sibling therefore + // outranks actions and output mutation. + rewritefile(ctrace, ""); rewritefile(atrace, ""); rewritefile(ltrace, ""); + let mixedbadout: str = strings.concat(root, "/mixed-bad-", tags[si]); + assert(os.mkdir(mixedbadout, 448i32) == 0); + writefile(strings.concat(mixedbadout, "/sentinel"), + "mixed-bad-sentinel\n"); + let mixedbadav: []str = [driver(stages[si]), "build", "-I", source, + "-o", mixedbadout, alpha, badlibrary]; + runcommandenvdir(root, strings.concat("odir-mixed-bad-", tags[si]), + mixedbadav, env, root, time.second, &out); + expectexit(&out, 1); + assert(has(out.stderr, "cannot find package missing.pkg\n")); + assert(readfile(ctrace).len == 0 && readfile(atrace).len == 0 + && readfile(ltrace).len == 0); + assert(same(readfile(strings.concat(mixedbadout, "/sentinel")), + "mixed-bad-sentinel\n")); + if (si == 0) { mixedimportref = strings.dup(out.stderr); } + else { assert(same(mixedimportref, out.stderr)); }; + + // Duplicate caller-visible basenames are output preflight too. A load + // failure in either selected command wins; two valid commands then reject + // the ambiguous publication without invoking any producer. + let duplicateout: str = strings.concat(root, "/duplicate-output"); + if (si == 0) { + assert(os.mkdir(duplicateout, 448i32) == 0); + writefile(strings.concat(duplicateout, "/sentinel"), + "duplicate-sentinel\n"); + }; + rewritefile(ctrace, ""); rewritefile(atrace, ""); rewritefile(ltrace, ""); + let duplicatebadav: []str = [driver(stages[si]), "build", "-I", source, + "-o", duplicateout, duplicateone, duplicatetwo]; + runcommandenvdir(root, strings.concat("odir-duplicate-import-", tags[si]), + duplicatebadav, env, root, time.second, &out); + expectexit(&out, 1); + assert(has(out.stderr, "cannot find package missing.pkg\n")); + assert(!has(out.stderr, "multiple commands produce output basename")); + assert(readfile(ctrace).len == 0 && readfile(atrace).len == 0 + && readfile(ltrace).len == 0); + assert(same(readfile(strings.concat(duplicateout, "/sentinel")), + "duplicate-sentinel\n")); + if (si == 0) { duplicateimportref = strings.dup(out.stderr); } + else { assert(same(duplicateimportref, out.stderr)); }; + let duplicateav: []str = [driver(stages[si]), "build", "-I", source, + "-o", duplicateout, duplicateone, duplicatethree]; + runcommandenvdir(root, strings.concat("odir-duplicate-", tags[si]), + duplicateav, env, root, time.second, &out); + expectexit(&out, 1); + assert(same(out.stderr, strings.concat( + "ww: multiple commands produce output basename \"tool\" in directory \"", + duplicateout, "\"\n"))); + assert(readfile(ctrace).len == 0 && readfile(atrace).len == 0 + && readfile(ltrace).len == 0); + assert(same(readfile(strings.concat(duplicateout, "/sentinel")), + "duplicate-sentinel\n")); + assert(!directoryhasnew(duplicateout)); + if (si == 0) { duplicateref = strings.dup(out.stderr); } + else { assert(same(duplicateref, out.stderr)); }; + + // No-main and import failures are loading/planning rejections. They run + // no tools, preserve existing contents, and do not mint missing output. + rewritefile(ctrace, ""); rewritefile(atrace, ""); rewritefile(ltrace, ""); + let badnonmainout: str = strings.concat(root, "/bad-nonmain-", tags[si]); + assert(os.mkdir(badnonmainout, 448i32) == 0); + writefile(strings.concat(badnonmainout, "/sentinel"), + "bad-nonmain-sentinel\n"); + let badnonmainav: []str = [driver(stages[si]), "build", "-I", source, + "-o", badnonmainout, badlibrary]; + runcommandenvdir(root, strings.concat("odir-nonmain-import-", tags[si]), + badnonmainav, env, root, time.second, &out); + expectexit(&out, 1); + assert(has(out.stderr, "cannot find package missing.pkg\n")); + assert(!has(out.stderr, "no main packages to build")); + assert(readfile(ctrace).len == 0 && readfile(atrace).len == 0 + && readfile(ltrace).len == 0); + assert(same(readfile(strings.concat(badnonmainout, "/sentinel")), + "bad-nonmain-sentinel\n")); + if (si == 0) { nomainimportref = strings.dup(out.stderr); } + else { assert(same(nomainimportref, out.stderr)); }; + + let nonmainout: str = strings.concat(root, "/nonmain-", tags[si]); + assert(os.mkdir(nonmainout, 448i32) == 0); + writefile(strings.concat(nonmainout, "/sentinel"), "nonmain-sentinel\n"); + let nonmainav: []str = [driver(stages[si]), "build", "-I", source, + "-o", nonmainout, library]; + runcommandenvdir(root, strings.concat("odir-nonmain-", tags[si]), + nonmainav, env, root, time.second, &out); + expectexit(&out, 1); + assert(same(out.stderr, "ww: no main packages to build\n")); + assert(readfile(ctrace).len == 0 && readfile(atrace).len == 0 + && readfile(ltrace).len == 0); + assert(same(readfile(strings.concat(nonmainout, "/sentinel")), + "nonmain-sentinel\n")); + if (si == 0) { nomainref = strings.dup(out.stderr); } + else { assert(same(nomainref, out.stderr)); }; + + let badroot: str = strings.concat(root, "/bad-output-", tags[si]); + let badout: str = strings.concat(badroot, "/nested/"); + let badav: []str = [driver(stages[si]), "build", "-I", source, + "-o", badout, bad]; + runcommandenvdir(root, strings.concat("odir-import-", tags[si]), badav, + env, root, time.second, &out); + expectexit(&out, 1); + assert(has(out.stderr, "cannot find package missing.pkg\n")); + assert(readfile(ctrace).len == 0 && readfile(atrace).len == 0 + && readfile(ltrace).len == 0); + assert(!os.exists(badroot)); + if (si == 0) { importref = strings.dup(out.stderr); } + else { assert(same(importref, out.stderr)); }; + + // The same pinned branch runs after a lone command's default output + // name is synthesized. An existing directory is then a load-complete + // rejection, while a non-main package has no default public output. + let implicitcaller: str = strings.concat(root, "/implicit-caller-", + tags[si]); + let implicitout: str = strings.concat(implicitcaller, "/alpha"); + mkdirall(implicitout); + writefile(strings.concat(implicitout, "/sentinel"), + "implicit-sentinel\n"); + rewritefile(ctrace, ""); rewritefile(atrace, ""); rewritefile(ltrace, ""); + let implicitav: []str = [driver(stages[si]), "build", "-I", source, + alpha]; + runcommandenvdir(root, strings.concat("odir-implicit-", tags[si]), + implicitav, env, implicitcaller, time.second, &out); + expectexit(&out, 1); + assert(same(out.stderr, + "ww: build output \"alpha\" already exists and is a directory\n")); + assert(readfile(ctrace).len == 0 && readfile(atrace).len == 0 + && readfile(ltrace).len == 0); + assert(same(readfile(strings.concat(implicitout, "/sentinel")), + "implicit-sentinel\n")); + if (si == 0) { implicitref = strings.dup(out.stderr); } + else { assert(same(implicitref, out.stderr)); }; + + let implicitrawcaller: str = strings.concat(root, + "/implicit-raw-caller-", tags[si]); + let implicitrawout: str = strings.concat(implicitrawcaller, "/raw"); + mkdirall(implicitrawout); + writefile(strings.concat(implicitrawout, "/sentinel"), + "implicit-raw-sentinel\n"); + let implicitrawav: []str = [driver(stages[si]), "build", raw]; + runcommandenvdir(root, strings.concat("odir-implicit-raw-", tags[si]), + implicitrawav, env, implicitrawcaller, time.second, &out); + expectexit(&out, 1); + assert(same(out.stderr, + "ww: build output \"raw\" already exists and is a directory\n")); + assert(readfile(ctrace).len == 0 && readfile(atrace).len == 0 + && readfile(ltrace).len == 0); + assert(same(readfile(strings.concat(implicitrawout, "/sentinel")), + "implicit-raw-sentinel\n")); + if (si == 0) { implicitrawref = strings.dup(out.stderr); } + else { assert(same(implicitrawref, out.stderr)); }; + + let implicitbadcaller: str = strings.concat(root, + "/implicit-bad-caller-", tags[si]); + let implicitbadout: str = strings.concat(implicitbadcaller, "/bad"); + mkdirall(implicitbadout); + let implicitbadav: []str = [driver(stages[si]), "build", "-I", source, + bad]; + runcommandenvdir(root, strings.concat("odir-implicit-bad-", tags[si]), + implicitbadav, env, implicitbadcaller, time.second, &out); + expectexit(&out, 1); + assert(has(out.stderr, "cannot find package missing.pkg\n")); + assert(!has(out.stderr, "already exists and is a directory")); + assert(readfile(ctrace).len == 0 && readfile(atrace).len == 0 + && readfile(ltrace).len == 0); + if (si == 0) { implicitimportref = strings.dup(out.stderr); } + else { assert(same(implicitimportref, out.stderr)); }; + + // Recursive selection reaches the same load-before-output boundary. Its + // implicit basename collision is deferred until all selected imports pass. + let recursivebadcaller: str = strings.concat(root, + "/recursive-bad-caller-", tags[si]); + let recursivebadout: str = strings.concat(recursivebadcaller, "/bad"); + mkdirall(recursivebadout); + writefile(strings.concat(recursivebadout, "/sentinel"), + "recursive-bad-sentinel\n"); + rewritefile(ctrace, ""); rewritefile(atrace, ""); rewritefile(ltrace, ""); + let recursivebadav: []str = [driver(stages[si]), "build", "-I", source, + strings.concat(bad, "/...")]; + runcommandenvdir(root, strings.concat("odir-recursive-import-", tags[si]), + recursivebadav, env, recursivebadcaller, time.second, &out); + expectexit(&out, 1); + assert(has(out.stderr, "cannot find package missing.pkg\n")); + assert(!has(out.stderr, "already exists and is a directory")); + assert(readfile(ctrace).len == 0 && readfile(atrace).len == 0 + && readfile(ltrace).len == 0); + assert(same(readfile(strings.concat(recursivebadout, "/sentinel")), + "recursive-bad-sentinel\n")); + if (si == 0) { recursiveimplicitimportref = strings.dup(out.stderr); } + else { assert(same(recursiveimplicitimportref, out.stderr)); }; + + let recursivecaller: str = strings.concat(root, "/recursive-caller-", + tags[si]); + let recursiveout: str = strings.concat(recursivecaller, "/alpha"); + mkdirall(recursiveout); + writefile(strings.concat(recursiveout, "/sentinel"), + "recursive-sentinel\n"); + let recursiveav: []str = [driver(stages[si]), "build", "-I", source, + strings.concat(alpha, "/...")]; + runcommandenvdir(root, strings.concat("odir-recursive-", tags[si]), + recursiveav, env, recursivecaller, time.second, &out); + expectexit(&out, 1); + assert(same(out.stderr, + "ww: build output \"alpha\" already exists and is a directory\n")); + assert(readfile(ctrace).len == 0 && readfile(atrace).len == 0 + && readfile(ltrace).len == 0); + assert(same(readfile(strings.concat(recursiveout, "/sentinel")), + "recursive-sentinel\n")); + assert(!directoryhasnew(recursiveout)); + if (si == 0) { recursiveimplicitref = strings.dup(out.stderr); } + else { assert(same(recursiveimplicitref, out.stderr)); }; + + let implicitlibcaller: str = strings.concat(root, + "/implicit-library-caller-", tags[si]); + let implicitlibout: str = strings.concat(implicitlibcaller, "/library"); + let implicitlibwork: str = strings.concat(root, + "/implicit-library-work-", tags[si]); + mkdirall(implicitlibout); mkdirall(implicitlibwork); + writefile(strings.concat(implicitlibout, "/sentinel"), + "implicit-library-sentinel\n"); + rewritefile(ctrace, ""); rewritefile(atrace, ""); rewritefile(ltrace, ""); + let implicitlibav: []str = [driver(stages[si]), "build", "-w", + implicitlibwork, "-I", source, library]; + runcommandenvdir(root, strings.concat("odir-implicit-library-", tags[si]), + implicitlibav, env, implicitlibcaller, + (120i64 * (time.second: i64)): time.duration, &out); + expectexit(&out, 0); + assert(occurrences(readfile(ctrace), "\n") == 1); + assert(occurrences(readfile(atrace), "\n") == 1); + assert(readfile(ltrace).len == 0); + assert(same(readfile(strings.concat(implicitlibout, "/sentinel")), + "implicit-library-sentinel\n")); + assert(!os.exists(strings.concat(implicitlibout, "/library.a"))); + + // -S remains action-only. With the coordinator-owned -w tree it emits + // semantic assembly but no command publication beneath -o. + rewritefile(ctrace, ""); rewritefile(atrace, ""); rewritefile(ltrace, ""); + let asmout: str = strings.concat(root, "/asm-output-", tags[si]); + let asmwork: str = strings.concat(root, "/asm-work-", tags[si]); + assert(os.mkdir(asmout, 448i32) == 0); + writefile(strings.concat(asmout, "/sentinel"), "asm-sentinel\n"); + let asmav: []str = [driver(stages[si]), "build", "-S", "-w", asmwork, + "-I", source, "-o", asmout, "alpha"]; + runcommandenvdir(root, strings.concat("odir-asm-", tags[si]), asmav, + env, root, + (120i64 * (time.second: i64)): time.duration, &out); + expectexit(&out, 0); + assert(readfile(ctrace).len != 0 && readfile(atrace).len == 0 + && readfile(ltrace).len == 0); + assert(!os.exists(strings.concat(asmout, "/alpha"))); + assert(same(readfile(strings.concat(asmout, "/sentinel")), + "asm-sentinel\n")); + let asmbytes: str = readfile(strings.concat(asmwork, "/alpha.s")); + if (si == 0) { asmref = strings.dup(asmbytes); } + else { assert(same(asmref, asmbytes)); }; + + // WW's -S boundary stops before Go's install action. Directory form still + // selects command actions, but publication-only path, collision, default, + // and parent-creation checks are inapplicable. + let asmmissingroot: str = strings.concat(root, "/asm-missing-", tags[si]); + let asmmissingout: str = strings.concat(asmmissingroot, "/nested/"); + let asmmissingav: []str = [driver(stages[si]), "build", "-S", "-w", + asmwork, "-I", source, "-o", asmmissingout, alpha]; + rewritefile(ctrace, ""); rewritefile(atrace, ""); rewritefile(ltrace, ""); + runcommandenvdir(root, strings.concat("odir-asm-missing-", tags[si]), + asmmissingav, env, root, time.second, &out); + expectexit(&out, 0); + assert(out.stdout.len == 0 && out.stderr.len == 0); + assert(!os.exists(asmmissingroot)); + assert(readfile(ctrace).len == 0 && readfile(atrace).len == 0 + && readfile(ltrace).len == 0); + + let asmlongav: []str = [driver(stages[si]), "build", "-S", "-w", + asmwork, "-I", source, "-o", longoutputdir, alpha]; + runcommandenvdir(root, strings.concat("odir-asm-long-", tags[si]), + asmlongav, env, root, time.second, &out); + expectexit(&out, 0); + assert(out.stdout.len == 0 && out.stderr.len == 0); + assert(readfile(ctrace).len == 0 && readfile(atrace).len == 0 + && readfile(ltrace).len == 0); + + let asmduplicatework: str = strings.concat(root, "/asm-duplicate-work-", + tags[si]); + let asmduplicateav: []str = [driver(stages[si]), "build", "-S", "-w", + asmduplicatework, "-I", source, "-o", duplicateout, + duplicateone, duplicatethree]; + runcommandenvdir(root, strings.concat("odir-asm-duplicate-", tags[si]), + asmduplicateav, env, root, + (120i64 * (time.second: i64)): time.duration, &out); + expectexit(&out, 0); + assert(out.stdout.len == 0 && out.stderr.len == 0); + assert(occurrences(readfile(ctrace), "\n") == 2); + assert(readfile(atrace).len == 0 && readfile(ltrace).len == 0); + assert(same(readfile(strings.concat(duplicateout, "/sentinel")), + "duplicate-sentinel\n")); + + rewritefile(ctrace, ""); rewritefile(atrace, ""); rewritefile(ltrace, ""); + let asmdefaultav: []str = [driver(stages[si]), "build", "-S", "-w", + asmwork, "-I", source, alpha]; + runcommandenvdir(root, strings.concat("odir-asm-default-", tags[si]), + asmdefaultav, env, implicitcaller, time.second, &out); + expectexit(&out, 0); + assert(out.stdout.len == 0 && out.stderr.len == 0); + assert(readfile(ctrace).len == 0 && readfile(atrace).len == 0 + && readfile(ltrace).len == 0); + assert(same(readfile(strings.concat(implicitout, "/sentinel")), + "implicit-sentinel\n")); + + let asmnonmainroot: str = strings.concat(root, "/asm-nonmain-", tags[si]); + let asmnonmainout: str = strings.concat(asmnonmainroot, "/nested/"); + let asmnonmainav: []str = [driver(stages[si]), "build", "-S", "-w", + asmwork, "-I", source, "-o", asmnonmainout, library]; + runcommandenvdir(root, strings.concat("odir-asm-nonmain-", tags[si]), + asmnonmainav, env, root, time.second, &out); + expectexit(&out, 1); + assert(same(out.stderr, "ww: no main packages to build\n")); + assert(!os.exists(asmnonmainroot)); + assert(readfile(ctrace).len == 0 && readfile(atrace).len == 0 + && readfile(ltrace).len == 0); + + // Persistent action storage is independent of output disposition. Cold + // and invalidated requests compile normally; warm commands still relink. + rewritefile(alphafile, alphaoriginal); + let work: str = strings.concat(root, "/persist-work-", tags[si]); + let persistout: str = strings.concat(root, "/persist-output-", tags[si]); + assert(os.mkdir(persistout, 448i32) == 0); + writefile(strings.concat(persistout, "/sentinel"), "persist-sentinel\n"); + let persistav: []str = [driver(stages[si]), "build", "-w", work, + "-I", source, "-o", persistout, "alpha"]; + rewritefile(ctrace, ""); rewritefile(atrace, ""); rewritefile(ltrace, ""); + runcommandenvdir(root, strings.concat("odir-cold-", tags[si]), persistav, + env, root, + (120i64 * (time.second: i64)): time.duration, &out); + expectexit(&out, 0); + assert(occurrences(readfile(ctrace), "\n") == 2); + assert(occurrences(readfile(atrace), "\n") == 3); + assert(occurrences(readfile(ltrace), "\n") == 1); + let persistbin: str = strings.concat(persistout, "/alpha"); + let coldbytes: str = readfile(persistbin); + let normalizedc: str = normalizedtrace(readfile(ctrace), + strings.concat(work, "/"), strings.concat(persistout, "/")); + let normalizeda: str = normalizedtrace(readfile(atrace), + strings.concat(work, "/"), strings.concat(persistout, "/")); + let normalizedl: str = normalizedtrace(readfile(ltrace), + strings.concat(work, "/"), strings.concat(persistout, "/")); + if (si == 0) { + coldctraceref = strings.dup(normalizedc); + coldatraceref = strings.dup(normalizeda); + coldltraceref = strings.dup(normalizedl); + } else { + assert(same(coldctraceref, normalizedc)); + assert(same(coldatraceref, normalizeda)); + assert(same(coldltraceref, normalizedl)); + }; + rewritefile(ctrace, ""); rewritefile(atrace, ""); rewritefile(ltrace, ""); + runcommandenvdir(root, strings.concat("odir-warm-", tags[si]), persistav, + env, root, + (120i64 * (time.second: i64)): time.duration, &out); + expectexit(&out, 0); + assert(readfile(ctrace).len == 0 && readfile(atrace).len == 0); + assert(occurrences(readfile(ltrace), "\n") == 1); + assert(same(coldbytes, readfile(persistbin))); + let warmtrace: str = normalizedtrace(readfile(ltrace), + strings.concat(work, "/"), strings.concat(persistout, "/")); + if (si == 0) { warmltraceref = strings.dup(warmtrace); } + else { assert(same(warmltraceref, warmtrace)); }; + + rewritefile(alphafile, alphachanged); + rewritefile(ctrace, ""); rewritefile(atrace, ""); rewritefile(ltrace, ""); + runcommandenvdir(root, strings.concat("odir-invalidated-", tags[si]), + persistav, env, root, + (120i64 * (time.second: i64)): time.duration, &out); + expectexit(&out, 0); + assert(occurrences(readfile(ctrace), "\n") == 1); + assert(occurrences(readfile(atrace), "\n") == 1); + assert(occurrences(readfile(ltrace), "\n") == 1); + let changedbytes: str = readfile(persistbin); + assert(!same(coldbytes, changedbytes)); + if (si == 0) { changedbinref = strings.dup(changedbytes); } + else { assert(same(changedbinref, changedbytes)); }; + let persistrunav: []str = [persistbin]; + runcommand(root, strings.concat("odir-invalidated-run-", tags[si]), + persistrunav, time.second, &out); + expectexit(&out, 5); + normalizedc = normalizedtrace(readfile(ctrace), + strings.concat(work, "/"), strings.concat(persistout, "/")); + normalizeda = normalizedtrace(readfile(atrace), + strings.concat(work, "/"), strings.concat(persistout, "/")); + normalizedl = normalizedtrace(readfile(ltrace), + strings.concat(work, "/"), strings.concat(persistout, "/")); + if (si == 0) { + changedctraceref = strings.dup(normalizedc); + changedatraceref = strings.dup(normalizeda); + changedltraceref = strings.dup(normalizedl); + } else { + assert(same(changedctraceref, normalizedc)); + assert(same(changedatraceref, normalizeda)); + assert(same(changedltraceref, normalizedl)); + }; + let ai: i32 = 0; + for (ai < suffixes.len) { + let artifact: str = readfile(strings.concat(work, "/alpha", + suffixes[ai])); + if (si == 0) { artifactrefs[ai] = strings.dup(artifact); } + else { assert(same(artifactrefs[ai], artifact)); }; + ai += 1; + }; + assert((permissionmode(persistbin) & 73u32) != 0u32); + assert(!os.exists(strings.concat(persistout, "/alpha.sepwork"))); + + // Compiler failure, linker failure, and linker interruption cannot + // replace the prior command or commit an invalid persistent generation. + let kept: []str = alloc([], suffixes.len: u64)!; + ai = 0; + for (ai < suffixes.len) { + append(kept, strings.dup(readfile(strings.concat(work, "/alpha", + suffixes[ai])))); + ai += 1; + }; + rewritefile(alphafile, alphafailing); + env[compilemodeindex] = "WW_ODIR_COMPILE_MODE=fail"; + rewritefile(ctrace, ""); rewritefile(atrace, ""); rewritefile(ltrace, ""); + runcommandenvdir(root, strings.concat("odir-compiler-fail-", tags[si]), + persistav, env, root, + (120i64 * (time.second: i64)): time.duration, &out); + expectexit(&out, 1); + assert(has(out.stderr, + "injected output-directory compiler failure\n")); + assert(has(out.stderr, "ww: w6c failed for alpha\n")); + assert(readfile(atrace).len == 0 && readfile(ltrace).len == 0); + assert(same(changedbytes, readfile(persistbin))); + if (si == 0) { compilerfailref = strings.dup(out.stderr); } + else { assert(same(compilerfailref, out.stderr)); }; + env[compilemodeindex] = "WW_ODIR_COMPILE_MODE="; + env[linkmodeindex] = "WW_ODIR_LINK_MODE=fail"; + rewritefile(ctrace, ""); rewritefile(atrace, ""); rewritefile(ltrace, ""); + runcommandenvdir(root, strings.concat("odir-linker-fail-", tags[si]), + persistav, env, root, + (120i64 * (time.second: i64)): time.duration, &out); + expectexit(&out, 1); + assert(has(out.stderr, "injected output-directory linker failure\n")); + assert(has(out.stderr, "ww: w6l failed\n")); + assert(same(changedbytes, readfile(persistbin))); + if (si == 0) { linkerfailref = strings.dup(out.stderr); } + else { assert(same(linkerfailref, out.stderr)); }; + env[linkmodeindex] = "WW_ODIR_LINK_MODE=signal"; + rewritefile(ctrace, ""); rewritefile(atrace, ""); rewritefile(ltrace, ""); + runcommandenvdir(root, strings.concat("odir-linker-signal-", tags[si]), + persistav, env, root, + (120i64 * (time.second: i64)): time.duration, &out); + expectexit(&out, 1); + assert(has(out.stderr, "ww: w6l failed\n")); + assert(same(changedbytes, readfile(persistbin))); + if (si == 0) { linkersignalref = strings.dup(out.stderr); } + else { assert(same(linkersignalref, out.stderr)); }; + ai = 0; + for (ai < suffixes.len) { + assert(same(kept[ai], readfile(strings.concat(work, "/alpha", + suffixes[ai])))); + ai += 1; + }; + assert(!directoryhasnew(work) && !directoryhasnew(persistout)); + assert(!directoryhasfragment(work, ".wwtxn.")); + assert(!directoryhasfragment(persistout, ".wwtxn.")); + assert(same(readfile(strings.concat(persistout, "/sentinel")), + "persist-sentinel\n")); + + // The raw direct route uses the same creation ledger. A late failure + // removes every directory minted for this request and leaves no stage. + env[linkmodeindex] = "WW_ODIR_LINK_MODE=fail"; + let rawfailroot: str = strings.concat(root, "/raw-fail-", tags[si]); + let rawfailout: str = strings.concat(rawfailroot, "/nested/"); + let rawfailav: []str = [driver(stages[si]), "build", "-o", + rawfailout, raw]; + runcommandenvdir(root, strings.concat("odir-raw-fail-", tags[si]), + rawfailav, env, root, + (120i64 * (time.second: i64)): time.duration, &out); + expectexit(&out, 1); + assert(has(out.stderr, "injected output-directory linker failure\n")); + assert(!os.exists(rawfailroot)); + if (si == 0) { rawrollbackref = strings.dup(out.stderr); } + else { assert(same(rawrollbackref, out.stderr)); }; + env[linkmodeindex] = "WW_ODIR_LINK_MODE="; + assert(!directoryhasfragment(root, ".wwtxn.")); + si += 1; + }; + + // Distinct Cstage and WWstage requests may build simultaneously without + // sharing output, work, stage, transaction, or process ownership. + rewritefile(alphafile, alphaoriginal); + let concurrentcout: str = strings.concat(root, "/concurrent-c-output"); + let concurrentwout: str = strings.concat(root, "/concurrent-ww-output"); + let concurrentcwork: str = strings.concat(root, "/concurrent-c-work"); + let concurrentwwork: str = strings.concat(root, "/concurrent-ww-work"); + mkdirall(concurrentcout); mkdirall(concurrentwout); + writefile(strings.concat(concurrentcout, "/sentinel"), "c-sentinel\n"); + writefile(strings.concat(concurrentwout, "/sentinel"), "ww-sentinel\n"); + let cav: []str = [driver("ww"), "build", "-w", concurrentcwork, "-I", + source, "-o", concurrentcout, "alpha"]; + let wav: []str = [driver("ww_ww"), "build", "-w", concurrentwwork, + "-I", source, "-o", concurrentwout, "alpha"]; + let cc: exec.command; + cc.path = cav[0]; cc.argv = cav; cc.env = os.getenvs(); cc.dir = root; + cc.stdoutpath = strings.concat(root, "/concurrent-c.stdout"); + cc.stderrpath = strings.concat(root, "/concurrent-c.stderr"); + cc.deadline = time.add(time.now(time.clock.monotonic), + (120i64 * (time.second: i64)): time.duration); + cc.grace = (100i64 * (time.millisecond: i64)): time.duration; + let wc: exec.command; + wc.path = wav[0]; wc.argv = wav; wc.env = os.getenvs(); wc.dir = root; + wc.stdoutpath = strings.concat(root, "/concurrent-ww.stdout"); + wc.stderrpath = strings.concat(root, "/concurrent-ww.stderr"); + wc.deadline = cc.deadline; + wc.grace = cc.grace; + let cp: exec.process; + let wp: exec.process; + exec.start(&cp, &cc); exec.start(&wp, &wc); + let cdone: bool = false; + let wdone: bool = false; + for (!cdone || !wdone) { + if (!cdone) { cdone = exec.poll(&cp); }; + if (!wdone) { wdone = exec.poll(&wp); }; + if (!cdone || !wdone) { + time.sleep(time.millisecond, time.clock.monotonic); + }; + }; + assert(cp.result.errno == 0 && cp.result.cleanuperrno == 0); + assert(wp.result.errno == 0 && wp.result.cleanuperrno == 0); + assert(cp.result.termination == exec.termination.EXIT && cp.result.code == 0); + assert(wp.result.termination == exec.termination.EXIT && wp.result.code == 0); + assert(readfile(cc.stdoutpath).len == 0 && readfile(cc.stderrpath).len == 0); + assert(readfile(wc.stdoutpath).len == 0 && readfile(wc.stderrpath).len == 0); + assert(same(readfile(strings.concat(concurrentcout, "/alpha")), + readfile(strings.concat(concurrentwout, "/alpha")))); + assert(same(readfile(strings.concat(concurrentcout, "/sentinel")), + "c-sentinel\n")); + assert(same(readfile(strings.concat(concurrentwout, "/sentinel")), + "ww-sentinel\n")); + assert(!directoryhasnew(concurrentcout) + && !directoryhasnew(concurrentwout)); + assert(!directoryhasnew(concurrentcwork) + && !directoryhasnew(concurrentwwork)); + assert(!directoryhasfragment(root, ".wwtxn.")); + clean(root); +}; + @test fn build_output_permissions_follow_umask() void = { let root: str = fresh(); let source: str = strings.concat(root, "/source");