diff --git a/cmd/ww/main.c b/cmd/ww/main.c index 9f74588d..0cf32f61 100644 --- a/cmd/ww/main.c +++ b/cmd/ww/main.c @@ -1309,6 +1309,7 @@ struct sepproduct { int directory_product; int no_tests; int build_action; /* loaded product retained in the action list */ + int public_out; int context; int root; int variant_root; /* retained single-unit root outside directory products */ @@ -4983,6 +4984,7 @@ struct septxnentry { char *backup; int had_old; int installed; + int public_output; }; struct septxn { @@ -4991,7 +4993,8 @@ struct septxn { }; static int -sep_txn_add(struct septxn *tx, const char *stage, const char *dst) +sep_txn_add_mode(struct septxn *tx, const char *stage, const char *dst, + int public_output) { if (strcmp(stage, dst) == 0) { fprintf(stderr, "ww: transaction path collision: %s\n", dst); @@ -5013,6 +5016,7 @@ sep_txn_add(struct septxn *tx, const char *stage, const char *dst) e->stage = strdup(stage); e->dst = strdup(dst); e->backup = sep_sprintf("%s.wwtxn.%ld.old", dst, (long)getpid()); + e->public_output = public_output; if (e->stage == NULL || e->dst == NULL || e->backup == NULL) { sep_fail_nomem(); free(e->backup); free(e->dst); free(e->stage); @@ -5029,6 +5033,18 @@ sep_txn_add(struct septxn *tx, const char *stage, const char *dst) return 0; } +static int +sep_txn_add(struct septxn *tx, const char *stage, const char *dst) +{ + return sep_txn_add_mode(tx, stage, dst, 0); +} + +static int +sep_txn_add_public(struct septxn *tx, const char *stage, const char *dst) +{ + return sep_txn_add_mode(tx, stage, dst, 1); +} + static void sep_txn_discard(struct septxn *tx) { @@ -5049,8 +5065,87 @@ sep_txn_free(struct septxn *tx) memset(tx, 0, sizeof *tx); } +struct sep_output_magic { + const unsigned char *bytes; + size_t n; +}; + +#define SEP_OUTPUT_MAGIC(s) { (const unsigned char *)(s), sizeof(s) - 1 } + +/* Go 1.26.5 work.objectMagic. WW interfaces are an additional public output + * kind and always begin with the compiler-owned module directive. */ +static const struct sep_output_magic sep_output_magic[] = { + SEP_OUTPUT_MAGIC("!\n"), + SEP_OUTPUT_MAGIC("\n"), + SEP_OUTPUT_MAGIC("\x7f" "ELF"), + SEP_OUTPUT_MAGIC("\xfe\xed\xfa\xce"), + SEP_OUTPUT_MAGIC("\xfe\xed\xfa\xcf"), + SEP_OUTPUT_MAGIC("\xce\xfa\xed\xfe"), + SEP_OUTPUT_MAGIC("\xcf\xfa\xed\xfe"), + SEP_OUTPUT_MAGIC("\x4d\x5a\x90\x00\x03\x00"), + SEP_OUTPUT_MAGIC("\x4d\x5a\x78\x00\x01\x00"), + SEP_OUTPUT_MAGIC("\x00\x00\x01\xeb"), + SEP_OUTPUT_MAGIC("\x00\x00\x8a\x97"), + SEP_OUTPUT_MAGIC("\x00\x00\x06\x47"), + SEP_OUTPUT_MAGIC("\x00\x61\x73\x6d"), + SEP_OUTPUT_MAGIC("\x01\xdf"), + SEP_OUTPUT_MAGIC("\x01\xf7"), + SEP_OUTPUT_MAGIC("//ww:module "), +}; + +#undef SEP_OUTPUT_MAGIC + +static int +sep_is_object_output(const char *path) +{ + unsigned char buf[64] = {0}; + int fd = open(path, O_RDONLY); + if (fd < 0) return 0; + size_t got = 0; + int bad = 0; + while (got < sizeof buf) { + ssize_t n = read(fd, buf + got, sizeof buf - got); + if (n > 0) got += (size_t)n; + else if (n == 0) break; + else if (errno != EINTR) { bad = 1; break; } + } + (void)close(fd); + if (bad) return 0; + for (size_t i = 0; i < nelem(sep_output_magic); i++) + if (got >= sep_output_magic[i].n + && memcmp(buf, sep_output_magic[i].bytes, + sep_output_magic[i].n) == 0) + return 1; + return 0; +} + +/* Pinned Go's checkDstOverwrite follows symlinks with stat and protects only + * directories and nonempty regular non-object files. Other file kinds remain + * eligible for the install operation itself to replace. */ +static int +sep_check_dst_overwrite(const char *dst) +{ + struct stat st; + if (stat(dst, &st) != 0) return 0; + if (S_ISDIR(st.st_mode)) { + fputs("ww: build output ", stderr); + sep_put_quoted(dst); + fputs(" already exists and is a directory\n", stderr); + return -1; + } + if (S_ISREG(st.st_mode) && st.st_size != 0 + && !sep_is_object_output(dst)) { + fputs("ww: build output ", stderr); + sep_put_quoted(dst); + fputs(" already exists and is not an object file\n", stderr); + return -1; + } + return 0; +} + /* One request-wide rollback group: producers and linkers finish first; only - * then are old destinations parked and all staged files installed. */ + * then are public destinations checked, old destinations parked, and all + * staged files installed. */ static int sep_txn_commit(struct septxn *tx) { @@ -5060,6 +5155,10 @@ sep_txn_commit(struct septxn *tx) tx->v[i].stage); goto rollback; } + for (int i = 0; i < tx->n; i++) + if (tx->v[i].public_output + && sep_check_dst_overwrite(tx->v[i].dst) != 0) + goto rollback; for (int i = 0; i < tx->n; i++) if (path_exists_nofollow(tx->v[i].backup) != 0) { fprintf(stderr, "ww: transaction backup already exists: %s\n", @@ -5476,6 +5575,52 @@ sep_mkdirs(const char *path, mode_t mode, struct sep_created_dirs *created) return 0; } +/* A retained running test is built and executed from request-private storage. + * Its Go-like install action runs only after that test succeeds, but reuses + * the same guarded publisher as build and compile-only test products. */ +static int +sep_install_test_output(const char *stage, const char *dst) +{ + char *install_stage = sep_sprintf("%s.install", stage); + if (install_stage == NULL) return 1; + if (strlen(install_stage) + 1 > (size_t)PATH_MAX + || path_exists_nofollow(install_stage) != 0 + || copy_executable_stage(stage, install_stage) != 0) { + fputs("ww: cannot stage retained test output\n", stderr); + free(install_stage); + return 1; + } + char *parent = sep_parent_path(dst); + if (parent == NULL) { + (void)unlink(install_stage); + free(install_stage); + return 1; + } + struct sep_created_dirs created = {0}; + if (sep_mkdirs(parent, 0777, &created) != 0) { + fprintf(stderr, "ww: cannot create test output directory %s\n", + parent); + free(parent); + (void)unlink(install_stage); + free(install_stage); + return 1; + } + free(parent); + struct septxn tx = {0}; + if (sep_txn_add_public(&tx, install_stage, dst) != 0 + || sep_txn_commit(&tx) != 0) { + sep_txn_discard(&tx); + sep_txn_free(&tx); + sep_rollback_dirs(&created); + (void)unlink(install_stage); + free(install_stage); + return 1; + } + sep_txn_free(&tx); + free(install_stage); + return 0; +} + /* build_sep_plan — discover dependencies for every requested product in one * package universe, compile the dependency-first union once, then link each * root from its own complete reachable archive closure. The dependency-first @@ -6713,11 +6858,14 @@ prepare_transaction: } for (int i = 0; i < nproducts; i++) { if (products[i].stage_out != NULL - && sep_txn_add(&tx, products[i].stage_out, - products[i].out) < 0) + && (products[i].public_out + ? sep_txn_add_public(&tx, products[i].stage_out, + products[i].out) + : sep_txn_add(&tx, products[i].stage_out, + products[i].out)) < 0) goto request_fail; if (products[i].stage_publish != NULL - && sep_txn_add(&tx, products[i].stage_publish, + && sep_txn_add_public(&tx, products[i].stage_publish, products[i].publish) < 0) goto request_fail; if (products[i].stage_iface != NULL) { @@ -6725,7 +6873,7 @@ prepare_transaction: int on = snprintf(outiface, sizeof outiface, "%s.wwi", products[i].out); if (on < 0 || (size_t)on >= sizeof outiface - || sep_txn_add(&tx, products[i].stage_iface, + || sep_txn_add_public(&tx, products[i].stage_iface, outiface) < 0) goto request_fail; } @@ -6771,7 +6919,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, const char *create_output_dir, + int public_output, int keepscratch, const char *workdir, + const char *create_output_dir, const char *default_output_dir, int output_path_error) { char scratch[PATH_MAX] = {0}; @@ -6786,6 +6935,7 @@ build_one_sep(const char *src, int entry_is_dir, const char *root_identity, .artifact = NULL, .variant = root_variant, .build_action = 1, + .public_out = public_output, .root = -1, .variant_root = -1, .support = -1, @@ -7225,7 +7375,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, NULL, NULL, 0); + emit_asm, 0, 0, workdir, NULL, NULL, 0); int cleanfail = 0; if (unlink(tmp) != 0 && errno != ENOENT) { fputs("ww: cannot remove temporary output\n", stderr); @@ -7241,7 +7391,7 @@ 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, create_output_dir, default_output_dir, + emit_asm, 1, 1, workdir, create_output_dir, default_output_dir, output_path_error); free(incs); return rc; @@ -7287,7 +7437,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, NULL, NULL, 0); + 0, 1, 0, SEP_VARIANT_PRODUCTION, NULL, 0, 0, 0, NULL, NULL, NULL, 0); free(incs); if (buildrc != 0) { if (unlink(tmp) != 0 && errno != ENOENT) @@ -7334,6 +7484,9 @@ do_run(int argc, char **argv) static int do_test(int argc, char **argv) { + if (argc == 3 + && strcmp(argv[0], "--ww-install-test-output") == 0) + return sep_install_test_output(argv[1], argv[2]); const char *src = NULL; struct sepproduct *products = NULL; int nproducts = 0, productcap = 0; @@ -7475,7 +7628,10 @@ do_test(int argc, char **argv) const char *publish = argv[++i]; const char *status = argv[++i]; size_t pn = strlen(name); - int build_product = strcmp(kind, "build") == 0; + int build_product = strcmp(kind, "build") == 0 + || strcmp(kind, "build-public") == 0; + int public_build_product = + strcmp(kind, "build-public") == 0; int test_product = strcmp(kind, "test") == 0; int has_production = strcmp(production, "-") != 0; int has_internal = strcmp(internal, "-") != 0; @@ -7527,6 +7683,7 @@ do_test(int argc, char **argv) products[nproducts].no_tests = test_product && !has_internal && !has_external; products[nproducts].build_action = 1; + products[nproducts].public_out = public_build_product; products[nproducts].root = -1; products[nproducts].variant_root = -1; products[nproducts].production_root = -1; @@ -7788,9 +7945,12 @@ do_test(int argc, char **argv) } char tmpdir[PATH_MAX] = {0}, tmp[PATH_MAX]; const char *outp; - int owntmp = (!outstem[0] || discard_output) && !workdir[0]; - if (outstem[0] && !discard_output) outp = outstem; - else if (workdir[0]) { + int retain_output = outstem[0] && !discard_output; + int deferred_install = retain_output && !compileonly && !emit_asm; + int owntmp = deferred_install + || ((!outstem[0] || discard_output) && !workdir[0]); + if (retain_output && !deferred_install) outp = outstem; + else if (workdir[0] && !deferred_install) { /* The workdir owns the persistent test binary the same * way it owns the package artifacts. */ int tn = snprintf(tmp, sizeof tmp, "%s/main", workdir); @@ -7816,7 +7976,8 @@ 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, NULL, NULL, 0); + retain_output && !deferred_install, retain_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); @@ -7837,6 +7998,9 @@ do_test(int argc, char **argv) return cleanfail ? 1 : 0; } int rc = run_test_bin(outp, pattern); + if (rc == 0 && deferred_install + && sep_install_test_output(outp, outstem) != 0) + rc = 1; if (owntmp && unlink(outp) != 0 && errno != ENOENT) { fputs("ww: cannot remove temporary output\n", stderr); if (rc == 0) rc = 1; @@ -7860,9 +8024,12 @@ do_test(int argc, char **argv) } char tmpdir[PATH_MAX] = {0}, tmp[PATH_MAX]; const char *outp; - int owntmp = (!outstem[0] || discard_output) && !workdir[0]; - if (outstem[0] && !discard_output) outp = outstem; - else if (workdir[0]) { + int retain_output = outstem[0] && !discard_output; + int deferred_install = retain_output && !compileonly && !emit_asm; + int owntmp = deferred_install + || ((!outstem[0] || discard_output) && !workdir[0]); + if (retain_output && !deferred_install) outp = outstem; + else if (workdir[0] && !deferred_install) { int tn = snprintf(tmp, sizeof tmp, "%s/main", workdir); if (tn < 0 || (size_t)tn >= sizeof tmp) { fputs("ww: workdir path is too long\n", stderr); @@ -7884,7 +8051,8 @@ 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, NULL, NULL, 0); + retain_output && !deferred_install, retain_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); @@ -7905,6 +8073,9 @@ do_test(int argc, char **argv) return cleanfail ? 1 : 0; } int rc = run_test_bin(outp, pattern); + if (rc == 0 && deferred_install + && sep_install_test_output(outp, outstem) != 0) + rc = 1; if (owntmp && unlink(outp) != 0 && errno != ENOENT) { fputs("ww: cannot remove temporary output\n", stderr); if (rc == 0) rc = 1; diff --git a/docs/build-system.md b/docs/build-system.md index 780fb696..7968d074 100644 --- a/docs/build-system.md +++ b/docs/build-system.md @@ -6214,11 +6214,10 @@ The authority is official Go 1.26.5 at commit The separation between saved and executed paths is a conclusion derived from the pinned action dependencies: the run consumes the temporary link action, not the install action. Duplicate names are likewise a materialization -collision, not package identity. Go's sources do not assert that a set of -retained binaries is installed as one rollback transaction; WW keeps its -existing stronger request-wide transaction while matching the observable -accepted, rejected, and preserved outputs. No installed host Go behavior was -used as authority. +collision, not package identity. Section 11.31 completes the later install +dependency: compile-only products retain the request transaction, while a +running retained product installs independently only after its successful run. +No installed host Go behavior was used as authority. Before this slice, direct native measurements of both Cstage and WWstage showed that single-package `-c -o FILE` retained and did not run, but `-o FILE` @@ -6268,23 +6267,24 @@ performs ordinary production validation, reports `[no tests]`, and creates no binary or otherwise-unused output directory. Successful compile-only products are silent, matching Go's no-op print action. -After linking, the driver copies the private runnable bytes to a distinct -`.new` inode opened with executable mode `0777` subject to umask. Temporary -runnable, retained copy, statuses, changed persistent actions, tool records, -and stamp then enter the existing one-request transaction. All producers and -linkers complete before installation. Any load, compile, assemble, archive, -link, stage, or install failure preserves old retained binaries and persistent -bytes, discards all stages, removes cold scratch, and rolls back only output -prefixes created by that request. Occupied or dangling `.new` paths reject -before tools and are never overwritten. +For `-c`, the driver copies the private runnable bytes to a distinct `.new` +inode opened with executable mode `0777` subject to umask. Temporary runnable, +retained copy, statuses, changed persistent actions, tool records, and stamp +then enter the existing one-request transaction. All producers and linkers +complete before installation. Any load, compile, assemble, archive, link, +stage, or install failure preserves old retained binaries and persistent bytes, +discards all stages, removes cold scratch, and rolls back only output prefixes +created by that request. Occupied or dangling `.new` paths reject before tools +and are never overwritten. -Execution begins only after publication commits. Assertion failure, signal, -timeout, or child-setup failure therefore leaves an explicitly retained binary -while retaining the established stdout/stderr result routing and sibling -isolation. Parallel products stage independent runnable/copy pairs; the shared -transaction prevents partial sibling publication and canonical result emission -order remains unchanged. Direct invocation of a retained binary continues to -inherit caller cwd, environment, and separate standard descriptors. +For running `-o`, the build transaction commits only the private runnable, +status, and semantic actions. The coordinator executes that runnable and, on a +successful result, invokes the selected driver stage's public install action. +Assertion failure, signal, timeout, interruption, or child-setup failure skips +that action and preserves any prior binary. Successful parallel products +install independently after their runs; canonical result emission order stays +unchanged. Direct invocation of a retained binary continues to inherit caller +cwd, environment, and separate standard descriptors. `-c` and `-o` can accompany `-w`: the workdir owns only semantic actions while the invocation/output path owns only the retained copy. Unchanged actions are @@ -6302,9 +6302,9 @@ declared-name versus import-leaf naming; executable mode and direct execution; temporary argv versus retained path; no-test omission; duplicate and non-directory rejection; occupied stages; serial and parallel sibling publication; injected late-link rollback over old files and newly created -parents; runtime-failure retention; persistent cold/warm/invalidation behavior; -diagnostic equality; retained binary byte identity; and absence of `.new` -residue. Existing package tests continue to own all action/test variants, +parents; runtime-failure preservation; persistent cold/warm/invalidation +behavior; diagnostic equality; retained binary byte identity; and absence of +`.new` residue. Existing package tests continue to own all action/test variants, graph identity, output ordering, cwd/environment/stdin, timeout, and broader transaction behavior. @@ -6760,6 +6760,150 @@ output umasks, occupied stages, generalized multi-product transactions, test runtime failure and timeout, null discard, and broader package/import graph matrices. +### 11.31 Implemented Go-like public-output overwrite safety + +Every caller-visible build and retained-test install now protects an existing +destination at the same late boundary as Go 1.26.5. After applicable producers +finish, ordinary `stat` rejects a directory and rejects a nonempty regular file +whose leading bytes do not identify a toolchain output. Absent paths, empty +regular reservations, recognized outputs, and non-directory non-regular paths +remain replaceable. Exact `/dev/null` and assembly-only `-S` have no install +action and never enter this rule. + +#### Pinned Go evidence and classification + +The sole authority is official Go 1.26.5 at commit +`c19862e5f8415b4f24b189d065ed739517c548ba`: + +- `Shell.moveOrCopyFile` and `Shell.CopyFile` call `checkDstOverwrite` before + replacing the destination + ([`cmd/go/internal/work/shell.go`, lines 119–235](https://github.com/golang/go/blob/c19862e5f8415b4f24b189d065ed739517c548ba/src/cmd/go/internal/work/shell.go#L119-L235)). + `checkDstOverwrite` uses `os.Stat`, rejects a directory, and—unless forced— + rejects a nonempty regular file for which `isObject` is false + ([lines 248–261](https://github.com/golang/go/blob/c19862e5f8415b4f24b189d065ed739517c548ba/src/cmd/go/internal/work/shell.go#L248-L261)). +- `BuildInstallFunc` creates the destination parent and reaches + `moveOrCopyFile(..., false)` only after its build producer + ([`cmd/go/internal/work/exec.go`, lines 1904–2000](https://github.com/golang/go/blob/c19862e5f8415b4f24b189d065ed739517c548ba/src/cmd/go/internal/work/exec.go#L1904-L2000)). + `objectMagic` and `isObject` read the first 64 bytes and recognize archive, + ELF, Mach-O, PE, Plan 9, WASM, and XCOFF prefixes without consulting a file + extension + ([lines 2118–2150](https://github.com/golang/go/blob/c19862e5f8415b4f24b189d065ed739517c548ba/src/cmd/go/internal/work/exec.go#L2118-L2150)). +- `runBuild` loads and checks every selected package before constructing the + output/install action + ([`cmd/go/internal/work/build.go`, lines 459–558](https://github.com/golang/go/blob/c19862e5f8415b4f24b189d065ed739517c548ba/src/cmd/go/internal/work/build.go#L459-L558)). +- `builderTest` makes `-c` depend directly on the install action. For a running + retained test, the run consumes the private build action and the install + action additionally depends on that run + ([`cmd/go/internal/test/test.go`, lines 1257–1364](https://github.com/golang/go/blob/c19862e5f8415b4f24b189d065ed739517c548ba/src/cmd/go/internal/test/test.go#L1257-L1364)). + `Builder.Do` invokes an actor only when dependency failure has not propagated + (unless the action explicitly ignores failure), so a failed test run skips + `BuildInstallFunc` + ([`cmd/go/internal/work/exec.go`, lines 72–207](https://github.com/golang/go/blob/c19862e5f8415b4f24b189d065ed739517c548ba/src/cmd/go/internal/work/exec.go#L72-L207)). +- Official `build_output_overwrite.txt` requires refusal to replace a + nonempty source file and preservation of its contents + ([lines 1–20](https://github.com/golang/go/blob/c19862e5f8415b4f24b189d065ed739517c548ba/src/cmd/go/testdata/script/build_output_overwrite.txt#L1-L20)). + Official `test_compile_tempfile.txt` requires an existing empty reservation + to be accepted and replaced + ([lines 1–11](https://github.com/golang/go/blob/c19862e5f8415b4f24b189d065ed739517c548ba/src/cmd/go/testdata/script/test_compile_tempfile.txt#L1-L11)). + Official `build_output.txt` separately pins executable command and archive + products + ([lines 47–57 and 64–76](https://github.com/golang/go/blob/c19862e5f8415b4f24b189d065ed739517c548ba/src/cmd/go/testdata/script/build_output.txt#L47-L76)). + +The destination predicate, complete magic table, load-before-install ordering, +producer-before-check ordering, and run-before-install dependency are directly +implemented by the pinned source. Non-overwrite and empty-file acceptance are +directly asserted by official testdata. Applying ELF and archive recognition +to WW's byte-identical output forms is derived from that implementation. Go +has no WW interface sidecar; recognizing only the compiler-owned +`//ww:module ` prefix is the derived local application that permits ordinary +repeat publication without letting arbitrary sidecar text be overwritten. No +installed host Go behavior was used as authority. + +#### Fresh four-axis audit and pre-fix measurements + +The bounded audit selected this one gap on the build axis and the shared +retained-test install axis. The package control selected the same canonical +command root twice and measured one deduplicated command action plus one +dependency action, with byte-identical Cstage/WWstage units, interfaces, and +archives. The import control placed a used alias in one source file and an +unused alias for the same dependency in a sibling; both stages emitted the +same file-local unused-import diagnostic and committed no work. Those package +and import candidates were aligned and were not changed. + +Fresh public Cstage and WWstage probes directly measured the same pre-fix +behavior. Explicit command, raw command, output-directory child, library +archive/interface, compile-only test, and running retained-test destinations +containing arbitrary nonempty text were replaced successfully. A nonempty +directory at a build or test child destination was renamed to a PID-bearing +transaction backup, replaced by the executable, and left stranded because +backup cleanup could not unlink the directory. Empty reservations were already +accepted. Missing-import rejection already preceded destination handling. All +measured successful executables, archives, interfaces, diagnostics, runtime +results, and semantic artifacts were stage-identical. Those are directly +measured WW facts, not source inferences. + +#### Ownership, action order, rollback, and identity + +The Cstage `sep_txn_commit` and WWstage `septxncommit` publishers own the byte +predicate. Each transaction entry now explicitly distinguishes a public +install from internal status, tool-identity, stamp, and persistent-action +state. Only command/archive output, retained-copy, and published `.wwi` entries +are checked. Producers still finish before transaction commit; a rejected +destination discards all staged outputs and preserves every prior public and +persistent byte. A library archive and interface remain one rollback group, so +arbitrary text in either destination changes neither. + +`internal/wwpackage.packagecommand` continues to own directory-test naming and +scheduling. A running retained descriptor withholds its public destination +from the build child. After a successful private run, the coordinator invokes +a private action in the selected driver, which stages an executable copy and +re-enters the same guarded publisher. A failed, signalled, timed-out, +interrupted, or unstartable run never invokes that action. Successful products +in a multi-package running request install independently; compile-only products +retain the established request transaction. The driver-owned raw single-file +route applies the same private build, run, and guarded-install sequence. + +Package-build descriptors use `build-public` only for caller-visible command or +archive products. Private package-build placeholders, test runnables, `ww run`, +workdir-owned test binaries, null-discard products, and assembly-only products +remain internal entries. Destination path, file kind, magic, declared name, +requested alias, import leaf, physical directory, and publication order do not +enter package/import identity, graph edges, action keys, symbols, artifacts, +`.wwi` contents, or persistence keys. + +The guard follows symlinks for classification, matching `os.Stat`; the existing +transaction still replaces the destination directory entry itself. It permits +FIFO and other non-directory non-regular destinations because the pinned guard +does. Diagnostics are exactly +`ww: build output "PATH" already exists and is a directory` and +`ww: build output "PATH" already exists and is not an object file` in both +stages. No guard is preflighted during loading: package/import errors still win, +and compiler, assembler, archive, or linker failure prevents the install action +from being reached. + +Build runtime is inapplicable because `ww build` starts no program. Test +runtime is applicable and owns the post-run dependency above. Producer failure, +linker interruption, output-parent rollback, concurrency, occupied stages, +prior-state preservation, and residue cleanup remain governed by the existing +request/private-action transactions; the new check adds no process-global +state. Public artifact bytes and modes are unchanged on accepted installs. +There is no persisted-byte contract change: build workdir format remains `18`, +test workdir format remains `19`, and semantic storage format remains `3`. + +The WW-native `public_output_overwrite_safety` observer covers both stages: +direct, default, raw, package-output-directory, library, compile-only test, and +running-test routes; late linker activity and load precedence; absent/empty, +ELF, archive, interface, arbitrary regular, directory, symlink, and FIFO +destinations; cold, warm, and invalidated persistent rollback; run-before-check +and failed-run no-install behavior; exact null and assembly-only exclusions; +runtime results; modes; diagnostic identity; public and semantic artifact-byte +identity; and `.new`, install-stage, and transaction-backup cleanup. +`test_binary_publication_transaction` pins the changed failed-run behavior and +the existing linker failure, output-parent rollback, multi-product, persistent, +and retained-binary contracts. Existing request-transaction, timeout, +interruption, and concurrent-driver owners continue to cover those unchanged +dimensions. + ## 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 008b5a7d..df62ea14 100644 --- a/docs/spec.md +++ b/docs/spec.md @@ -335,6 +335,22 @@ ImportPath = ident { "." ident } . output. Output paths and directory metadata never become package, import, graph, action, symbol, artifact, `.wwi`, or persistence identity. + + Every caller-visible build installation checks its destination after all + applicable compile, assemble, archive, and link producers finish. Ordinary + `stat` follows symlinks. An existing directory rejects as + `ww: build output "PATH" already exists and is a directory`; an existing + nonempty regular file rejects as `... is not an object file` unless its + leading bytes identify a Go 1.26.5 object/output form. The recognized table + is archive, ELF, Mach-O, PE, Plan 9, WASM, and XCOFF magic; WW additionally + recognizes its compiler-owned `//ww:module ` interface prefix. An absent + path, an empty regular reservation, or a non-directory non-regular path may + be replaced. A published non-main archive and its `.wwi` sidecar are checked + as one WW request transaction, so arbitrary caller text in either + destination preserves both old outputs and the committed persistent + generation. This safety check is output disposition only: it does not enter + package/import loading, graph or action identity, artifact bytes, or + invalidation. 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 @@ -714,12 +730,17 @@ paths, and retained binary names remain presentation or loader metadata and do not become canonical package or action identity. The retained file is an executable, byte-identical copy of the private -runnable. It joins package artifacts and statuses in the request-wide atomic -publication transaction. Build, link, stage, or install failure preserves old -destinations and removes temporary stages and invocation-created output -prefixes. Test execution starts only after that transaction commits, so a -runtime failure leaves an explicitly retained binary. A no-test product -publishes no binary and does not create a directory solely for one. +runnable. A compile-only retained binary joins package artifacts and statuses +in the request-wide atomic publication transaction. Build, link, stage, or +install failure preserves old destinations and removes temporary stages and +invocation-created output prefixes. For a running `-o` request, the private +binary executes first. Only a successful run enters the guarded install +action; a failed, signalled, timed-out, interrupted, or unstartable run +publishes no new copy and preserves any prior destination. The post-run guard +uses the same directory/nonempty-regular/object-magic rule as `ww build`. +Successful products in a multi-package running request install independently; +their visible result order remains package order. A no-test product publishes +no binary and does not create a directory solely for one. `-w` may persist the unchanged semantic actions for either `-c` or running retention without changing publication identity or introducing a test-result cache. diff --git a/docs/test-system-v2.md b/docs/test-system-v2.md index 9061e616..de75926f 100644 --- a/docs/test-system-v2.md +++ b/docs/test-system-v2.md @@ -258,11 +258,25 @@ 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 -request-wide transaction: producer, linker, staging, or installation failure -preserves every prior destination and removes stages and newly created output -prefixes. A runtime failure occurs after commit and therefore leaves the saved -binary. The language runtime owns individual `@test` functions. +executed by the coordinator. Compile-only publication participates in the +driver's one request-wide transaction: producer, linker, staging, or +installation failure preserves every prior destination and removes stages and +newly created output prefixes. A running retained request instead withholds +the public path from that build transaction. The coordinator runs the private +binary, then invokes the selected stage driver's guarded install action only +after a successful process result. Failure, signal, timeout, interruption, or +child-start failure therefore preserves any prior retained binary and creates +no new one. Successful products install independently after their runs. The +language runtime owns individual `@test` functions. + +Every build or retained-test public install follows Go 1.26.5's late +destination safety rule. After applicable producers (and, for running tests, +after the successful run), ordinary `stat` rejects a directory and rejects a +nonempty regular non-object file. Empty reservations, recognized prior +outputs, and non-directory non-regular paths remain replaceable. Recognition +uses Go's archive/ELF/Mach-O/PE/Plan 9/WASM/XCOFF magic plus WW's narrow +`//ww:module ` interface prefix. Package/import rejection keeps its earlier +diagnostic precedence; exact `/dev/null` and `-S` never enter the guard. Every actually executed directory product gives its single generated binary the product's canonical absolute physical source directory as child cwd. A @@ -468,8 +482,9 @@ with one fresh `mkdir` and refuses an existing path; it never clears a collision. A caller keeps only the exact artifacts it observes and removes that exact tree on every later success or failure. Directory-package test plans instead keep their cold semantic-action scratch and runnable binary -inside the coordinator's temporary root; only the optional retained executable -escapes through the transaction above. `ww run` and no-output single-file +inside the coordinator's temporary root; a compile-only retained executable +escapes through the build transaction, while a running retained executable +uses the post-run install action above. `ww run` and no-output single-file `ww test` use driver-owned scratch instead; both driver stages place that scratch and their temporary executable beneath one freshly acquired directory, remove both after every build result, and make cleanup failure fail the diff --git a/internal/wwpackage/package.ww b/internal/wwpackage/package.ww index bbdd5d33..66c722be 100644 --- a/internal/wwpackage/package.ww +++ b/internal/wwpackage/package.ww @@ -39,9 +39,13 @@ type pkggroup = struct { bin: str, buildok: str, runoutput: str, + installoutput: str, state: i32, runstartfailed: bool, runres: exec.result, + publicbin: bool, + installattempted: bool, + installres: exec.result, }; type pkgplan = struct { @@ -64,6 +68,7 @@ type pkgplan = struct { outputcollisionbase: str, outputcollisiondir: str, suppressbuildreports: bool, + deferpublish: bool, }; def PKG_COUNT_MAX: i32 = 2147483647; @@ -1373,16 +1378,23 @@ fn pkgsetplanpaths(p: *pkgplan, groups: []pkggroup, root: str, index: i32, pkgfailpath(g.root, "cannot create product temporary path"); return false; }; + g.publicbin = false; + g.installattempted = false; if (buildonly) { 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; } + g.publicbin = true; + } else if (outname.len != 0 && !outputdir) { + g.bin = outname; + g.publicbin = true; + } else if (!pkgstring(&g.bin, g.root, "/package.build")) { return false; }; } else { if (!pkgstring(&g.bin, g.root, "/package.test")) { return false; }; }; if (!pkgstring(&g.runoutput, g.root, "/test.output") + || !pkgstring(&g.installoutput, g.root, "/install.output") || !pkgstring(&g.buildok, g.root, "/build.ok")) { return false; }; i += 1; }; @@ -1484,7 +1496,8 @@ fn pkgstartbuild(p: *pkgplan, groups: []pkggroup, builder: str, includes: []str, for (i < p.end) { let g: *pkggroup = &groups[i]; append(ba, "--ww-package-test"); - if (p.buildonly) { append(ba, "build"); } + if (p.buildonly && g.publicbin) { append(ba, "build-public"); } + else if (p.buildonly) { append(ba, "build"); } else { append(ba, "test"); }; append(ba, g.pkg); if (g.prodpkg.len != 0) { append(ba, g.prodpkg); } @@ -1495,7 +1508,7 @@ fn pkgstartbuild(p: *pkgplan, groups: []pkggroup, builder: str, includes: []str, else { append(ba, "-"); }; append(ba, g.dir); append(ba, g.bin); - if (g.publish.len != 0) { append(ba, g.publish); } + if (g.publish.len != 0 && !p.deferpublish) { append(ba, g.publish); } else { append(ba, "-"); }; append(ba, g.buildok); i += 1; @@ -1591,6 +1604,31 @@ fn pkgstartrun(g: *pkggroup, filters: []str, timeoutarg: str, return true; }; +// Pinned builderTest orders a retained running test as build -> run -> +// BuildInstallFunc. Re-enter the selected stage driver for the final action so +// build and test share one overwrite predicate instead of copying object magic +// into the coordinator. +fn pkginstalloutput(g: *pkggroup, builder: str) void = { + let ia: []str = [builder, "test", "--ww-install-test-output", + g.bin, g.publish]; + let env: []str = os.getenvs(); + let icmd: exec.command; + icmd.path = builder; + icmd.argv = ia; + icmd.env = env; + icmd.dir = ""; + icmd.stdoutpath = g.installoutput; + icmd.stderrpath = g.installoutput; + icmd.deadline.sec = 0i64; + icmd.deadline.nsec = 0i64; + icmd.grace = 0i64: time.duration; + let h: exec.process; + g.installattempted = true; + exec.start(&h, &icmd); + for (!exec.poll(&h)) { time.sleep(pkgpoll, time.clock.monotonic); }; + g.installres = h.result; +}; + fn pkgproductbuilt(g: *pkggroup, buildonly: bool) bool = { if (buildonly || g.notests) { return pkgisreg(g.buildok); }; return pkgisreg(g.buildok) && pkgisreg(g.bin); @@ -1602,6 +1640,35 @@ fn pkgrunok(g: *pkggroup) bool = { && g.runres.code == 0; }; +fn pkginstallok(g: *pkggroup) bool = { + return g.installres.errno == 0 && g.installres.cleanuperrno == 0 + && g.installres.termination == exec.termination.EXIT + && g.installres.code == 0; +}; + +fn pkgemitinstall(g: *pkggroup) bool = { + if (!g.installattempted) { return true; }; + let output: str; + if (!pkgread(g.installoutput, &output)) { + pkgfailpath(g.root, "cannot read install capture"); + return false; + }; + if (output.len != 0) { + pkgput(os.STDERR_FILENO, output); + if (output[output.len - 1] != '\n') { + pkgput(os.STDERR_FILENO, "\n"); + }; + }; + if (!pkginstallok(g)) { + if (output.len == 0) { + pkgreportcommand(os.STDERR_FILENO, "install", g, + &g.installres); + }; + return false; + }; + return true; +}; + fn pkgemitgroup(g: *pkggroup, compileonly: bool) bool = { if (g.notests) { pkgput(os.STDOUT_FILENO, "? "); @@ -1653,8 +1720,9 @@ fn pkgemitplan(p: *pkgplan, groups: []pkggroup, failed += 1; } else if (buildonly) { void; - } else if (!pkgemitgroup(&groups[i], compileonly)) { - failed += 1; + } else { + if (!pkgemitgroup(&groups[i], compileonly)) { failed += 1; }; + if (!compileonly && !pkgemitinstall(&groups[i])) { failed += 1; }; }; i += 1; }; @@ -2385,11 +2453,12 @@ export fn packagecommand(args: []str) int = { plan.outputcollisiondir = outputcollisiondir; plan.suppressbuildreports = buildonly && (outputdir || defaultoutputdir.len != 0); + plan.deferpublish = !buildonly && !compileonly && testretain && !testnull; append(plans, plan); let createdir: str = ""; if (buildonly && outputdir) { createdir = outname; - } else if (!buildonly && testretain && !testnull) { + } else if (!buildonly && compileonly && testretain && !testnull) { i = 0; for (i < groups.len) { if (groups[i].publish.len != 0) { @@ -2550,6 +2619,22 @@ export fn packagecommand(args: []str) int = { if (active > 0) { time.sleep(pkgpoll, time.clock.monotonic); }; }; + // A failed, signalled, timed-out, or unstartable run propagates to its Go + // install action, so it preserves any prior retained binary. Successful + // products install independently after their private execution. + if (!compileonly) { + i = 0; + for (i < groups.len) { + let g: *pkggroup = &groups[i]; + if (g.publish.len != 0 && !g.notests + && pkgproductbuilt(g, buildonly) && !g.runstartfailed + && pkgrunok(g)) { + pkginstalloutput(g, builder); + }; + i += 1; + }; + }; + i = 0; for (i < plans.len) { failed += pkgemitplan(&plans[i], groups, compileonly, buildonly); diff --git a/selfhost/cmd/ww/main.ww b/selfhost/cmd/ww/main.ww index 0a6a4115..743f2956 100644 --- a/selfhost/cmd/ww/main.ww +++ b/selfhost/cmd/ww/main.ww @@ -1515,6 +1515,7 @@ type sepproduct = struct { directoryproduct: bool, notests: bool, buildaction: bool, // loaded product retained in the action list + publicout: bool, context: i32, root: i32, variantroot: i32, @@ -6134,6 +6135,7 @@ type septxnentry = struct { backup: *u8, hadold: bool, installed: bool, + publicoutput: bool, }; fn sepalloctxnentries(cap: i32) ([]septxnentry | nomem) = { @@ -6207,8 +6209,8 @@ fn septxnbackup(dst: *u8) *u8 = { return buf.ptr; }; -fn septxnadd(entries: *[]septxnentry, n: *i32, - stage: *u8, dst: *u8) bool = { +fn septxnaddmode(entries: *[]septxnentry, n: *i32, + stage: *u8, dst: *u8, publicoutput: bool) bool = { if (cstreq(stage, dst)) { cerrpath("ww: transaction path collision: ", dst, "\n"); return false; @@ -6253,10 +6255,21 @@ fn septxnadd(entries: *[]septxnentry, n: *i32, (*entries)[*n].backup = backup; (*entries)[*n].hadold = false; (*entries)[*n].installed = false; + (*entries)[*n].publicoutput = publicoutput; *n += 1; return true; }; +fn septxnadd(entries: *[]septxnentry, n: *i32, + stage: *u8, dst: *u8) bool = { + return septxnaddmode(entries, n, stage, dst, false); +}; + +fn septxnaddpublic(entries: *[]septxnentry, n: *i32, + stage: *u8, dst: *u8) bool = { + return septxnaddmode(entries, n, stage, dst, true); +}; + fn septxnaddpkgsuffix(entries: *[]septxnentry, n: *i32, g: *sepgraph, pi: i32, scratch: *u8, stagesuffix: str, dstsuffix: str) bool = { @@ -6271,6 +6284,91 @@ fn septxndiscard(entries: []septxnentry, n: i32) void = { for (i < n) { os.remove(pathstr(entries[i].stage)); i += 1; }; }; +fn sepoutputprefix(buf: *u8, n: i64, magic: str) bool = { + if (n < 0i64 || n < magic.len: i64) { return false; }; + let i: i32 = 0; + for (i < magic.len) { + if (buf[i] != magic.ptr[i]) { return false; }; + i += 1; + }; + return true; +}; + +fn sepoutput2(buf: *u8, n: i64, a: u8, b: u8) bool = { + return n >= 2i64 && buf[0u64] == a && buf[1u64] == b; +}; + +fn sepoutput4(buf: *u8, n: i64, a: u8, b: u8, c: u8, d: u8) bool = { + return n >= 4i64 && buf[0u64] == a && buf[1u64] == b + && buf[2u64] == c && buf[3u64] == d; +}; + +fn sepoutput6(buf: *u8, n: i64, a: u8, b: u8, c: u8, d: u8, + e: u8, f: u8) bool = { + return n >= 6i64 && buf[0u64] == a && buf[1u64] == b + && buf[2u64] == c && buf[3u64] == d + && buf[4u64] == e && buf[5u64] == f; +}; + +// Go 1.26.5 work.objectMagic, plus WW's compiler-owned interface prefix. +fn sepisobjectoutput(path: *u8) bool = { + let fd: i32 = os.open(pathstr(path), os.flag.RDONLY, 0i32); + if (fd < 0) { return false; }; + let buf: [64]u8; + let got: u64 = 0u64; + let bad: bool = false; + for (got < 64u64) { + let n: i64 = os.read(fd, &buf[0] + got, 64u64 - got); + if (n > 0) { got += n: u64; } + else if (n == 0) { break; } + else if (n != -4i64) { bad = true; break; }; + }; + os.close(fd); + if (bad) { return false; }; + let n: i64 = got: i64; + if (sepoutputprefix(&buf[0], n, "!\n") + || sepoutputprefix(&buf[0], n, "\n") + || sepoutput4(&buf[0], n, 127u8, 69u8, 76u8, 70u8) + || sepoutput4(&buf[0], n, 254u8, 237u8, 250u8, 206u8) + || sepoutput4(&buf[0], n, 254u8, 237u8, 250u8, 207u8) + || sepoutput4(&buf[0], n, 206u8, 250u8, 237u8, 254u8) + || sepoutput4(&buf[0], n, 207u8, 250u8, 237u8, 254u8) + || sepoutput6(&buf[0], n, 77u8, 90u8, 144u8, 0u8, 3u8, 0u8) + || sepoutput6(&buf[0], n, 77u8, 90u8, 120u8, 0u8, 1u8, 0u8) + || sepoutput4(&buf[0], n, 0u8, 0u8, 1u8, 235u8) + || sepoutput4(&buf[0], n, 0u8, 0u8, 138u8, 151u8) + || sepoutput4(&buf[0], n, 0u8, 0u8, 6u8, 71u8) + || sepoutput4(&buf[0], n, 0u8, 97u8, 115u8, 109u8) + || sepoutput2(&buf[0], n, 1u8, 223u8) + || sepoutput2(&buf[0], n, 1u8, 247u8) + || sepoutputprefix(&buf[0], n, "//ww:module ")) { + return true; + }; + return false; +}; + +fn sepcheckdstoverwrite(dst: *u8) bool = { + let fi: os.filestat; + match (os.stat(&fi, pathstr(dst))) { + case void => { + let typ: u32 = (fi.mode: u32) & 61440u32; + if (typ == os.mode.DIR: u32) { + cerr("ww: build output "); sepputquoted(dst); + cerr(" already exists and is a directory\n"); + return false; + }; + if (typ == os.mode.REG: u32 && fi.sz != 0u64 + && !sepisobjectoutput(dst)) { + cerr("ww: build output "); sepputquoted(dst); + cerr(" already exists and is not an object file\n"); + return false; + }; + }; + case let e: os.oserror => void; + }; + return true; +}; + fn septxncommit(entries: []septxnentry, n: i32) bool = { let i: i32 = 0; let valid: bool = true; @@ -6283,6 +6381,17 @@ fn septxncommit(entries: []septxnentry, n: i32) bool = { }; i += 1; }; + if (valid) { + i = 0; + for (i < n) { + if (entries[i].publicoutput + && !sepcheckdstoverwrite(entries[i].dst)) { + valid = false; + break; + }; + i += 1; + }; + }; if (valid) { i = 0; for (i < n) { @@ -6614,6 +6723,50 @@ fn septxnrelease(entries: *[]septxnentry, n: i32) void = { entries.cap = 0; }; +// Running retained tests enter this Go-like install action only after their +// request-private executable has returned successfully. +fn sepinstalltestoutput(stage: *u8, dst: *u8) i32 = { + let installstage: *u8 = sepappendlit(stage, ".install"); + if (installstage == nil) { return 1; }; + if (cstrlen(installstage) + 1u64 > os.PATH_MAX: u64 + || pathexistsnofollow(installstage) != 0 + || copyexecutablestage(stage, installstage) != 0) { + cerr("ww: cannot stage retained test output\n"); + os.free(installstage: *void, cstrlen(installstage) + 1u64); + return 1; + }; + let parent: *u8 = seplexicalparent(dst); + if (parent == nil) { + os.remove(pathstr(installstage)); + os.free(installstage: *void, cstrlen(installstage) + 1u64); + return 1; + }; + let created: sepcreateddirs; + created.n = 0; + if (sepmkdirsrecord(parent, 511, &created) != 0) { + cerrpath("ww: cannot create test output directory ", parent, "\n"); + os.free(parent: *void, cstrlen(parent) + 1u64); + os.remove(pathstr(installstage)); + os.free(installstage: *void, cstrlen(installstage) + 1u64); + return 1; + }; + os.free(parent: *void, cstrlen(parent) + 1u64); + let entries: []septxnentry; + let n: i32 = 0; + if (!septxnaddpublic(&entries, &n, installstage, dst) + || !septxncommit(entries, n)) { + septxndiscard(entries, n); + septxnrelease(&entries, n); + seprollbackdirs(&created); + os.remove(pathstr(installstage)); + os.free(installstage: *void, cstrlen(installstage) + 1u64); + return 1; + }; + septxnrelease(&entries, n); + os.free(installstage: *void, cstrlen(installstage) + 1u64); + return 0; +}; + fn sepfinishfail(entries: *[]septxnentry, n: i32) i32 = { septxndiscard(*entries, n); septxnrelease(entries, n); @@ -6924,19 +7077,24 @@ fn sepfinishrequest(selfdir: *u8, l6: *u8, c6: *u8, a6: *u8, producti = 0; for (producti < nproducts) { if (products[producti].stageout != nil - && !septxnadd(&entries, &ntxn, products[producti].stageout, - products[producti].out)) { - return sepfinishfail(&entries, ntxn); + && ((products[producti].publicout + && !septxnaddpublic(&entries, &ntxn, + products[producti].stageout, products[producti].out)) + || (!products[producti].publicout + && !septxnadd(&entries, &ntxn, + products[producti].stageout, products[producti].out)))) { + return sepfinishfail(&entries, ntxn); }; if (products[producti].stagepublish != nil - && !septxnadd(&entries, &ntxn, products[producti].stagepublish, + && !septxnaddpublic(&entries, &ntxn, + products[producti].stagepublish, products[producti].publish)) { return sepfinishfail(&entries, ntxn); }; if (products[producti].stageiface != nil) { let outiface: *u8 = sepappendlit(products[producti].out, ".wwi"); if (outiface == nil - || !septxnadd(&entries, &ntxn, + || !septxnaddpublic(&entries, &ntxn, products[producti].stageiface, outiface)) { return sepfinishfail(&entries, ntxn); }; @@ -8213,7 +8371,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, createoutputdir: *u8, + publicoutput: bool, keepscratch: i32, workdir: *u8, + createoutputdir: *u8, defaultoutputdir: *u8, outputpatherror: bool) i32 = { let scratch: *u8 = nil; let g: *sepgraph = nil; @@ -8235,6 +8394,7 @@ fn buildonesep(selfdir: *u8, src: *u8, entryisdir: i32, product.directoryproduct = false; product.notests = false; product.buildaction = true; + product.publicout = publicoutput; product.root = -1; product.variantroot = -1; product.support = -1; @@ -8691,7 +8851,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, nil, nil, false); + emitasm, false, 0i32, workdir, nil, nil, false); let cleanbad: bool = false; let cleanrc: i32 = os.remove(pathstr(outp)); if (cleanrc != 0 && cleanrc != -2i32) { @@ -8710,7 +8870,7 @@ 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, createoutputdir, defaultoutputdir, + emitasm, true, 1i32, workdir, createoutputdir, defaultoutputdir, outputpatherror); }; @@ -8893,7 +9053,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, nil, nil, false) != 0) { + 0i32, false, 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"); @@ -8958,10 +9118,12 @@ fn runsingletest(selfdir: *u8, src: *u8, incs: *u8, compileonly: i32, let owntmp: bool = false; let retainout: bool = outstem != nil && !cstreqlit(outstem, "/dev/null"); - if (retainout) { + let deferredinstall: bool = retainout && compileonly == 0 + && emitasm == 0; + if (retainout && !deferredinstall) { outp = outstem; objstem = outstem; - } else { if (workdir != nil) { + } else { if (workdir != nil && !deferredinstall) { // The workdir owns the persistent test binary the same way it // owns the package artifacts. outp = joinpathlit(workdir, "main"); @@ -8975,6 +9137,7 @@ fn runsingletest(selfdir: *u8, src: *u8, incs: *u8, compileonly: i32, }; outp = joinpathlit(tmp.ptr, "main"); objstem = outp; + if (retainout) { objstem = outstem; }; }; }; // E3-C1: separate compilation is the sole build path (task #87). let lf: lflags; @@ -8986,7 +9149,8 @@ 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, nil, nil, false); + emitasm, retainout && !deferredinstall, keep, workdir, + nil, nil, false); if (bres != 0) { if (owntmp) { let cleanrc: i32 = os.remove(pathstr(outp)); @@ -9037,6 +9201,8 @@ fn runsingletest(selfdir: *u8, src: *u8, incs: *u8, compileonly: i32, rc = -1; }; }; }; + if (rc == 0 && deferredinstall + && sepinstalltestoutput(outp, outstem) != 0) { rc = 1; }; if (owntmp) { let cleanrc: i32 = os.remove(pathstr(outp)); if (cleanrc != 0 && cleanrc != -2i32) { @@ -9052,6 +9218,10 @@ fn runsingletest(selfdir: *u8, src: *u8, incs: *u8, compileonly: i32, }; fn dotest(selfdir: *u8, argv: **u8, argc: i32, start: i32) i32 = { + if (argc - start == 3 + && cstreqlit(argv[start], "--ww-install-test-output")) { + return sepinstalltestoutput(argv[start + 1], argv[start + 2]); + }; // -I parsing mirrors dobuild/dorun so a single-file test can resolve // transitive imports (e.g. 905_nkname asttest → tok); coupled to the // -T flip (task #5/#10). @@ -9203,7 +9373,10 @@ fn dotest(selfdir: *u8, argv: **u8, argc: i32, start: i32) i32 = { let publish: *u8 = argv[i + 8]; let status: *u8 = argv[i + 9]; let pn: u64 = cstrlen(name); - let buildproduct: bool = cstreqlit(kind, "build"); + let publicbuildproduct: bool = + cstreqlit(kind, "build-public"); + let buildproduct: bool = cstreqlit(kind, "build") + || publicbuildproduct; let testproduct: bool = cstreqlit(kind, "test"); let hasproduction: bool = !cstreqlit(production, "-"); let hasinternal: bool = !cstreqlit(internal, "-"); @@ -9248,6 +9421,7 @@ fn dotest(selfdir: *u8, argv: **u8, argc: i32, start: i32) i32 = { product.directoryproduct = true; product.notests = testproduct && !hasinternal && !hasexternal; product.buildaction = true; + product.publicout = publicbuildproduct; product.root = -1; product.variantroot = -1; product.productionroot = -1; diff --git a/test/package/package_test.ww b/test/package/package_test.ww index d4a60ba9..6cabaca8 100644 --- a/test/package/package_test.ww +++ b/test/package/package_test.ww @@ -12443,7 +12443,6 @@ fn runtimepath(relative: str) str = { let rollbackdiag: str = ""; let missingdiag: str = ""; let occupieddiag: str = ""; - let failurebytes: str = ""; let persistbasebytes: str = ""; let persistchangedbytes: str = ""; let baseenv: []str = os.getenvs(); @@ -12595,29 +12594,23 @@ fn runtimepath(relative: str) str = { " [no tests]\n"))); assert(out.stderr.len == 0 && !os.exists(noneroot)); - // Publication commits before execution. A failing test therefore leaves - // a runnable retained binary while its product failure stays on stdout. + // Pinned builderTest makes the install action depend on the run action. + // A failing run therefore preserves a prior destination and never enters + // the retained-output overwrite guard. let failurebin: str = strings.concat(root, "/failure-", tags[si], ".test"); + writefile(failurebin, "failure-sentinel\n"); let failureav: []str = [driver(stages[si]), "test", "-I", suite, "-o", failurebin, failed]; runcommandenvdir(root, strings.concat("publish-runtime-", tags[si]), failureav, env, root, (60i64 * (time.second: i64)): time.duration, &out); expectexit(&out, 1); - assert(os.exists(failurebin) && !os.exists(strings.concat(failurebin, - ".new"))); + assert(same(readfile(failurebin), "failure-sentinel\n")); + assert(!os.exists(strings.concat(failurebin, ".new"))); assert(has(out.stdout, "failed_decl.runtime_failure ... FAIL (exit 1)\n")); assert(out.stderr.len == 0); - let fi: os.filestat; - match (os.stat(&fi, failurebin)) { - case void => void; - case let e: os.oserror => abort("retained failure binary missing"); - }; - assert(((fi.mode: u32) & 73u32) != 0u32); - if (si == 0) { failurebytes = readfile(failurebin); } - else { assert(same(failurebytes, readfile(failurebin))); }; // -w keeps semantic actions persistent while -c and -o remain execution // and presentation policy. Compile-only can cold-build and invalidate; @@ -12681,6 +12674,377 @@ fn runtimepath(relative: str) str = { clean(root); }; +// Go 1.26.5's BuildInstallFunc protects caller data at the install boundary: +// after producers, and after a successful run for retained running tests. +// Exercise that one predicate through direct, raw, package, and test routes. +@test fn public_output_overwrite_safety() void = { + let root: str = fresh(); + let source: str = strings.concat(root, "/source"); + let dep: str = strings.concat(source, "/lib/dep"); + let app: str = strings.concat(source, "/cmd/app"); + let bad: str = strings.concat(source, "/cmd/bad"); + let checked: str = strings.concat(source, "/pkg/check"); + let failed: str = strings.concat(source, "/pkg/failed"); + let invocation: str = strings.concat(root, "/invoke"); + mkdirall(dep); mkdirall(app); mkdirall(bad); mkdirall(checked); + mkdirall(failed); mkdirall(invocation); + writefile(strings.concat(dep, "/dep.ww"), strings.concat( + "package renamed;\n", + "export fn value() int = { return 9; };\n")); + let appsource: str = strings.concat(app, "/main.ww"); + let appbase: str = strings.concat( + "package main;\n", + "import stable lib.dep;\n", + "export fn main() int = { return stable.value(); };\n"); + let appchanged: str = strings.concat( + "package main;\n", + "import stable lib.dep;\n", + "export fn main() int = { return stable.value() + 1; };\n"); + writefile(appsource, appbase); + writefile(strings.concat(bad, "/main.ww"), strings.concat( + "package main;\n", + "import missing nowhere.present;\n", + "export fn main() int = { return missing.value(); };\n")); + writefile(strings.concat(checked, "/check.ww"), + "package checked;\nfn value() int = { return 4; };\n"); + writefile(strings.concat(checked, "/check_test.ww"), strings.concat( + "package checked;\n", + "@test fn guarded_run() void = { assert(value() == 4); };\n")); + writefile(strings.concat(failed, "/failed_test.ww"), strings.concat( + "package failed;\n", + "@test fn failed_run() void = { assert(false); };\n")); + let raw: str = strings.concat(root, "/raw.ww"); + writefile(raw, + "package main;\nexport fn main() int = { return 3; };\n"); + + let linkerwrapper: str = strings.concat(root, "/overwrite-w6l.sh"); + writeexecutable(linkerwrapper, strings.concat( + "#!/bin/sh\n", + "printf 'link\\n' >> \"$WW_OVERWRITE_TRACE\"\n", + "exec \"$WW_OVERWRITE_REAL_L\" \"$@\"\n")); + let drivers: []str = ["ww", "ww_ww"]; + let linkers: []str = ["w6l", "w6l_ww"]; + let tags: []str = ["c", "ww"]; + let commandout: str = strings.concat(root, "/command.bin"); + let rawout: str = strings.concat(root, "/raw.bin"); + let defaultout: str = strings.concat(invocation, "/app"); + let libout: str = strings.concat(root, "/library.a"); + let testbin: str = strings.concat(root, "/check.test"); + let symlinktarget: str = strings.concat(root, "/symlink-target"); + let symlinkout: str = strings.concat(root, "/symlink.bin"); + let fifoout: str = strings.concat(root, "/fifo.bin"); + writefile(symlinktarget, "symlink-sentinel\n"); + assert(os.symlink(symlinktarget, symlinkout) == 0); + let builddir: str = strings.concat(root, "/build-output"); + let buildchild: str = strings.concat(builddir, "/app"); + let testdir: str = strings.concat(root, "/test-output"); + let testchild: str = strings.concat(testdir, "/check.test"); + mkdirall(buildchild); mkdirall(testchild); + writefile(strings.concat(buildchild, "/marker"), "build-directory\n"); + writefile(strings.concat(testchild, "/marker"), "test-directory\n"); + + let commanddiag: str = ""; + let rawdiag: str = ""; + let defaultdiag: str = ""; + let symlinkdiag: str = ""; + let builddirdiag: str = ""; + let librarydiag: str = ""; + let libraryarchivediag: str = ""; + let testcompilediag: str = ""; + let testrundiag: str = ""; + let testdirdiag: str = ""; + let loaddiag: str = ""; + let commandbytes: str = ""; + let librarybytes: str = ""; + let interfacebytes: str = ""; + let testbytes: str = ""; + let unitbytes: str = ""; + let baseenv: []str = os.getenvs(); + let out: commandout; + let si: i32 = 0; + for (si < drivers.len) { + let trace: str = strings.concat(root, "/overwrite-", tags[si], + ".trace"); + writefile(trace, ""); + let env: []str = alloc([], (baseenv.len + 3): u64)!; + let ei: i32 = 0; + for (ei < baseenv.len) { + if (!strings.hasprefix(baseenv[ei], "WW_W6L=") + && !strings.hasprefix(baseenv[ei], "WW_OVERWRITE_TRACE=") + && !strings.hasprefix(baseenv[ei], "WW_OVERWRITE_REAL_L=")) { + append(env, baseenv[ei]); + }; + ei += 1; + }; + append(env, strings.concat("WW_W6L=", linkerwrapper)); + append(env, strings.concat("WW_OVERWRITE_TRACE=", trace)); + append(env, strings.concat("WW_OVERWRITE_REAL_L=", + driver(linkers[si]))); + let work: str = strings.concat(root, "/command-work-", tags[si]); + let badwork: str = strings.concat(root, "/bad-work-", tags[si]); + let rawwork: str = strings.concat(root, "/raw-work-", tags[si]); + let defaultwork: str = strings.concat(root, "/default-work-", tags[si]); + let symlinkwork: str = strings.concat(root, "/symlink-work-", tags[si]); + let fifowork: str = strings.concat(root, "/fifo-work-", tags[si]); + let dirwork: str = strings.concat(root, "/dir-work-", tags[si]); + let libwork: str = strings.concat(root, "/lib-work-", tags[si]); + let testwork: str = strings.concat(root, "/test-work-", tags[si]); + let testdirwork: str = strings.concat(root, "/test-dir-work-", tags[si]); + mkdirall(work); mkdirall(badwork); mkdirall(rawwork); + mkdirall(defaultwork); mkdirall(symlinkwork); mkdirall(fifowork); + mkdirall(dirwork); + mkdirall(libwork); mkdirall(testwork); mkdirall(testdirwork); + + // A nonempty caller file rejects only after the linker ran, and a cold + // persistent generation remains unpublished. + if (os.exists(commandout)) { assert(os.remove(commandout) == 0); }; + writefile(commandout, "caller-command\n"); + rewritefile(trace, ""); + let commandav: []str = [driver(drivers[si]), "build", "-w", work, + "-I", source, "-o", commandout, "cmd.app"]; + runcommandenv(root, strings.concat("overwrite-command-", tags[si]), + commandav, env, (60i64 * (time.second: i64)): time.duration, &out); + expectexit(&out, 1); + assert(has(out.stderr, strings.concat("ww: build output \"", commandout, + "\" already exists and is not an object file\n"))); + assert(same(readfile(commandout), "caller-command\n")); + assert(permissionmode(commandout) == 384u32); + assert(readfile(trace).len != 0 && directoryisempty(work)); + assert(!os.exists(strings.concat(commandout, ".new")) + && !directoryhasfragment(root, "command.bin.wwtxn.")); + if (si == 0) { commanddiag = strings.dup(out.stderr); } + else { assert(same(commanddiag, out.stderr)); }; + + // Package/import failure keeps load precedence and starts no linker. + rewritefile(trace, ""); + let badav: []str = [driver(drivers[si]), "build", "-w", badwork, + "-I", source, "-o", commandout, "cmd.bad"]; + runcommandenv(root, strings.concat("overwrite-load-", tags[si]), badav, + env, (60i64 * (time.second: i64)): time.duration, &out); + expectexit(&out, 1); + assert(has(out.stderr, "cannot find package nowhere.present")); + assert(!has(out.stderr, "already exists and is not an object file")); + assert(readfile(trace).len == 0 && directoryisempty(badwork)); + assert(same(readfile(commandout), "caller-command\n")); + if (si == 0) { loaddiag = strings.dup(out.stderr); } + else { assert(same(loaddiag, out.stderr)); }; + + // Empty reservations and recognized ELF outputs are replaceable. A + // rejected invalidation preserves the prior persistent generation. + rewritefile(commandout, ""); rewritefile(trace, ""); + runcommandenv(root, strings.concat("overwrite-empty-", tags[si]), + commandav, env, (60i64 * (time.second: i64)): time.duration, &out); + expectexit(&out, 0); + let built: str = readfile(commandout); + assert(strings.hasprefix(built, "\x7fELF")); + let unit: str = readfile(strings.concat(work, "/cmd.app.unit.ww")); + if (si == 0) { + commandbytes = strings.dup(built); + unitbytes = strings.dup(unit); + } else { + assert(same(commandbytes, built) && same(unitbytes, unit)); + }; + rewritefile(appsource, appchanged); + rewritefile(commandout, "caller-after-change\n"); + rewritefile(trace, ""); + runcommandenv(root, strings.concat("overwrite-invalidated-", tags[si]), + commandav, env, (60i64 * (time.second: i64)): time.duration, &out); + expectexit(&out, 1); + assert(readfile(trace).len != 0); + assert(same(readfile(commandout), "caller-after-change\n")); + assert(same(readfile(strings.concat(work, "/cmd.app.unit.ww")), unit)); + rewritefile(appsource, appbase); + rewritefile(commandout, built); + runcommandenv(root, strings.concat("overwrite-elf-", tags[si]), + commandav, env, (60i64 * (time.second: i64)): time.duration, &out); + expectexit(&out, 0); + let runav: []str = [commandout]; + runcommand(root, strings.concat("overwrite-command-run-", tags[si]), + runav, (10i64 * (time.second: i64)): time.duration, &out); + expectexit(&out, 9); + + // Default, raw, symlink-following, and output-directory child routes + // use the same late guard and never strand rollback backups. + if (os.exists(defaultout)) { rewritefile(defaultout, "default-sentinel\n"); } + else { writefile(defaultout, "default-sentinel\n"); }; + let defaultav: []str = [driver(drivers[si]), "build", "-w", + defaultwork, "-I", source, "cmd.app"]; + runcommandenvdir(root, strings.concat("overwrite-default-", tags[si]), + defaultav, env, invocation, + (60i64 * (time.second: i64)): time.duration, &out); + expectexit(&out, 1); + assert(same(readfile(defaultout), "default-sentinel\n")); + if (si == 0) { defaultdiag = strings.dup(out.stderr); } + else { assert(same(defaultdiag, out.stderr)); }; + if (os.exists(rawout)) { rewritefile(rawout, "raw-sentinel\n"); } + else { writefile(rawout, "raw-sentinel\n"); }; + let rawav: []str = [driver(drivers[si]), "build", "-w", rawwork, + "-o", rawout, raw]; + runcommandenv(root, strings.concat("overwrite-raw-", tags[si]), rawav, + env, (60i64 * (time.second: i64)): time.duration, &out); + expectexit(&out, 1); + assert(same(readfile(rawout), "raw-sentinel\n")); + if (si == 0) { rawdiag = strings.dup(out.stderr); } + else { assert(same(rawdiag, out.stderr)); }; + let symlinkav: []str = [driver(drivers[si]), "build", "-w", + symlinkwork, "-I", source, "-o", symlinkout, "cmd.app"]; + runcommandenv(root, strings.concat("overwrite-symlink-", tags[si]), + symlinkav, env, (60i64 * (time.second: i64)): time.duration, &out); + expectexit(&out, 1); + assert(same(readfile(symlinkout), "symlink-sentinel\n")); + if (si == 0) { symlinkdiag = strings.dup(out.stderr); } + else { assert(same(symlinkdiag, out.stderr)); }; + if (os.exists(fifoout)) { assert(os.remove(fifoout) == 0); }; + let fifoav: []str = ["/usr/bin/mkfifo", fifoout]; + runcommand(root, strings.concat("overwrite-mkfifo-", tags[si]), fifoav, + (10i64 * (time.second: i64)): time.duration, &out); + expectexit(&out, 0); + let fifobuildav: []str = [driver(drivers[si]), "build", "-w", + fifowork, "-I", source, "-o", fifoout, "cmd.app"]; + runcommandenv(root, strings.concat("overwrite-fifo-", tags[si]), + fifobuildav, env, (60i64 * (time.second: i64)): time.duration, &out); + expectexit(&out, 0); + assert(strings.hasprefix(readfile(fifoout), "\x7fELF")); + let dirav: []str = [driver(drivers[si]), "build", "-w", dirwork, + "-I", source, "-o", strings.concat(builddir, "/"), "cmd.app"]; + runcommandenv(root, strings.concat("overwrite-build-dir-", tags[si]), + dirav, env, (60i64 * (time.second: i64)): time.duration, &out); + expectexit(&out, 1); + assert(has(out.stderr, "already exists and is a directory")); + assert(same(readfile(strings.concat(buildchild, "/marker")), + "build-directory\n")); + assert(!directoryhasfragment(builddir, ".wwtxn.")); + if (si == 0) { builddirdiag = strings.dup(out.stderr); } + else { assert(same(builddirdiag, out.stderr)); }; + + // Archive and WW-interface outputs each participate in the guard. An + // ordinary repeat accepts both recognized prior outputs; arbitrary + // interface or archive text rejects without changing either member. + if (os.exists(libout)) { assert(os.remove(libout) == 0); }; + if (os.exists(strings.concat(libout, ".wwi"))) { + assert(os.remove(strings.concat(libout, ".wwi")) == 0); + }; + let libav: []str = [driver(drivers[si]), "build", "-w", libwork, + "-I", source, "-o", libout, "lib.dep"]; + runcommandenv(root, strings.concat("overwrite-library-new-", tags[si]), + libav, env, (60i64 * (time.second: i64)): time.duration, &out); + expectexit(&out, 0); + let archive: str = readfile(libout); + let iface: str = readfile(strings.concat(libout, ".wwi")); + assert(strings.hasprefix(archive, "!\n")); + assert(strings.hasprefix(iface, "//ww:module ")); + runcommandenv(root, strings.concat("overwrite-library-repeat-", tags[si]), + libav, env, (60i64 * (time.second: i64)): time.duration, &out); + expectexit(&out, 0); + assert(same(archive, readfile(libout)) + && same(iface, readfile(strings.concat(libout, ".wwi")))); + if (si == 0) { + librarybytes = strings.dup(archive); + interfacebytes = strings.dup(iface); + } else { + assert(same(librarybytes, archive) + && same(interfacebytes, iface)); + }; + rewritefile(strings.concat(libout, ".wwi"), "caller-interface\n"); + runcommandenv(root, strings.concat("overwrite-library-reject-", tags[si]), + libav, env, (60i64 * (time.second: i64)): time.duration, &out); + expectexit(&out, 1); + assert(same(readfile(libout), archive)); + assert(same(readfile(strings.concat(libout, ".wwi")), + "caller-interface\n")); + if (si == 0) { librarydiag = strings.dup(out.stderr); } + else { assert(same(librarydiag, out.stderr)); }; + rewritefile(libout, "caller-archive\n"); + rewritefile(strings.concat(libout, ".wwi"), iface); + runcommandenv(root, strings.concat("overwrite-library-archive-", + tags[si]), libav, env, + (60i64 * (time.second: i64)): time.duration, &out); + expectexit(&out, 1); + assert(same(readfile(libout), "caller-archive\n")); + assert(same(readfile(strings.concat(libout, ".wwi")), iface)); + if (si == 0) { libraryarchivediag = strings.dup(out.stderr); } + else { assert(same(libraryarchivediag, out.stderr)); }; + + // Compile-only installs check after linking. Running retention executes + // the private test first; a successful run then checks, while a failed + // run never installs or diagnoses the caller destination. + if (os.exists(testbin)) { rewritefile(testbin, "caller-test\n"); } + else { writefile(testbin, "caller-test\n"); }; + let testcompileav: []str = [driver(drivers[si]), "test", "-c", "-w", + testwork, "-I", source, "-o", testbin, "pkg.check"]; + runcommandenv(root, strings.concat("overwrite-test-c-", tags[si]), + testcompileav, env, + (90i64 * (time.second: i64)): time.duration, &out); + expectexit(&out, 1); + assert(same(readfile(testbin), "caller-test\n")); + if (si == 0) { testcompilediag = strings.dup(out.stderr); } + else { assert(same(testcompilediag, out.stderr)); }; + rewritefile(testbin, ""); + runcommandenv(root, strings.concat("overwrite-test-empty-", tags[si]), + testcompileav, env, + (90i64 * (time.second: i64)): time.duration, &out); + expectexit(&out, 0); + let compiledtest: str = readfile(testbin); + assert(strings.hasprefix(compiledtest, "\x7fELF")); + if (si == 0) { testbytes = strings.dup(compiledtest); } + else { assert(same(testbytes, compiledtest)); }; + rewritefile(testbin, "caller-running-test\n"); + let testrunav: []str = [driver(drivers[si]), "test", "-w", testwork, + "-I", source, "-o", testbin, "pkg.check"]; + runcommandenv(root, strings.concat("overwrite-test-run-", tags[si]), + testrunav, env, (90i64 * (time.second: i64)): time.duration, &out); + expectexit(&out, 1); + assert(has(out.stdout, "checked.guarded_run ... ok\n")); + assert(same(readfile(testbin), "caller-running-test\n")); + if (si == 0) { testrundiag = strings.dup(out.stderr); } + else { assert(same(testrundiag, out.stderr)); }; + rewritefile(testbin, compiledtest); + runcommandenv(root, strings.concat("overwrite-test-elf-", tags[si]), + testrunav, env, (90i64 * (time.second: i64)): time.duration, &out); + expectexit(&out, 0); + assert(has(out.stdout, "checked.guarded_run ... ok\n")); + let beforefailure: str = readfile(testbin); + let failav: []str = [driver(drivers[si]), "test", "-w", testwork, + "-I", source, "-o", testbin, "pkg.failed"]; + runcommandenv(root, strings.concat("overwrite-test-fail-", tags[si]), + failav, env, (90i64 * (time.second: i64)): time.duration, &out); + expectexit(&out, 1); + assert(has(out.stdout, "failed.failed_run ... FAIL (exit 1)\n")); + assert(out.stderr.len == 0 && same(readfile(testbin), beforefailure)); + assert(!os.exists(strings.concat(testbin, ".new")) + && !os.exists(strings.concat(testbin, ".install"))); + let testdirav: []str = [driver(drivers[si]), "test", "-c", "-w", + testdirwork, "-I", source, "-o", strings.concat(testdir, "/"), + "pkg.check"]; + runcommandenv(root, strings.concat("overwrite-test-dir-", tags[si]), + testdirav, env, (90i64 * (time.second: i64)): time.duration, &out); + expectexit(&out, 1); + assert(has(out.stderr, "already exists and is a directory")); + assert(same(readfile(strings.concat(testchild, "/marker")), + "test-directory\n")); + assert(!directoryhasfragment(testdir, ".wwtxn.")); + if (si == 0) { testdirdiag = strings.dup(out.stderr); } + else { assert(same(testdirdiag, out.stderr)); }; + + // Assembly and exact-null routes have no public install action. + let asmout: str = strings.concat(root, "/assembly-", tags[si]); + writefile(asmout, "assembly-sentinel\n"); + let asmav: []str = [driver(drivers[si]), "build", "-S", "-w", + work, "-I", source, "-o", asmout, "cmd.app"]; + runcommandenv(root, strings.concat("overwrite-asm-", tags[si]), asmav, + env, (60i64 * (time.second: i64)): time.duration, &out); + expectexit(&out, 0); + assert(same(readfile(asmout), "assembly-sentinel\n")); + let nullav: []str = [driver(drivers[si]), "build", "-I", source, + "-o", "/dev/null", "cmd.app"]; + runcommandenv(root, strings.concat("overwrite-null-", tags[si]), nullav, + env, (60i64 * (time.second: i64)): time.duration, &out); + expectexit(&out, 0); + si += 1; + }; + 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