diff --git a/cmd/ww/main.c b/cmd/ww/main.c index c124c349..ad29cfb5 100644 --- a/cmd/ww/main.c +++ b/cmd/ww/main.c @@ -38,7 +38,7 @@ static const char *usage = " -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"; +" . current directory package (default when no path is given)\n"; static char *self_dir; static const char *self_path; @@ -7158,6 +7158,36 @@ build_import_leaf(const char *identity, char *out, size_t outsz) snprintf(out, outsz, "%s", leaf); } +static int +build_current_dir_spelling(const char *path) +{ + if (path[0] == '\0' || path[0] == '/') return 0; + const char *p = path; + int found = 0; + for (;;) { + while (*p == '/') p++; + if (*p == '\0') return found; + if (*p++ != '.' || (*p != '\0' && *p != '/')) return 0; + found = 1; + } +} + +static void +build_local_dir_leaf(const char *resolved, char *out, size_t outsz) +{ + char canonical[PATH_MAX]; + const char *selected = resolved; + if (build_current_dir_spelling(resolved) + && realpath(resolved, canonical) != NULL) + selected = canonical; + char tmp[PATH_MAX]; + memcpy(tmp, selected, strlen(selected) + 1); + size_t n = strlen(tmp); + while (n > 1 && tmp[n - 1] == '/') tmp[--n] = '\0'; + const char *base = strrchr(tmp, '/'); + snprintf(out, outsz, "%s", base ? base + 1 : tmp); +} + static int resolve_module(const char *name, const char *incs, char *out, size_t outsz, int *is_dir) @@ -7428,15 +7458,12 @@ do_build(int argc, char **argv) } 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); - size_t n = strlen(tmp); - while (n > 1 && tmp[n-1] == '/') tmp[--n] = '\0'; - const char *b = strrchr(tmp, '/'); - snprintf(out, sizeof out, "%s", b ? b + 1 : tmp); + build_local_dir_leaf(resolved, out, sizeof out); } else { basename_no_ext(resolved, out, sizeof out); } + if (!outflag[0] && is_dir && build_current_dir_spelling(resolved)) + objstem = out; const char *default_output_dir = !outflag[0] && build_output_dir(out) ? out : NULL; if (discard_output) { diff --git a/docs/build-system.md b/docs/build-system.md index 74a17c8a..df0c1fe9 100644 --- a/docs/build-system.md +++ b/docs/build-system.md @@ -8119,6 +8119,133 @@ This is runtime/coordinator presentation, not a persisted-byte contract. Build workdir format remains `18`, test workdir format remains `19`, and semantic storage format remains `3`. +### 11.40 Implemented current-directory default build names + +An empty `ww build` package list selects the current directory just as an +explicit `.`, `./`, or equivalent sequence of single-dot components does. For +one command package and no explicit `-o`, the public executable is now named by +the selected directory's final component. The selector is loading syntax; it +is not the literal output pathname. Thus a command built while the current +directory is `tool` publishes `tool`, not `.`, and cold private build artifacts +use `tool.sepwork`, not `..sepwork`. + +A non-main current-directory package uses the same corrected cold scratch stem, +but still has no link or public installation action. An explicit `-o`, exact +`-o /dev/null`, an ordinary non-current directory operand, and a contextual +dotted package request keep their established output rules. + +#### Pinned Go evidence and fact classification + +The sole semantic authority is official Go 1.26.5 at commit +`c19862e5f8415b4f24b189d065ed739517c548ba`: + +- `search.CleanPatterns` turns an empty package-pattern list into exactly `.`; + `ImportPathsQuiet` then treats that local pattern as a directory selection + ([`cmd/go/internal/search/search.go`, lines 441–480](https://github.com/golang/go/blob/c19862e5f8415b4f24b189d065ed739517c548ba/src/cmd/go/internal/search/search.go#L441-L480)). +- `work.runBuild` loads packages and checks load errors before output planning. + With exactly one loaded `main` package and no `-o`, it selects + `DefaultExecName` + ([`cmd/go/internal/work/build.go`, lines 459–478](https://github.com/golang/go/blob/c19862e5f8415b4f24b189d065ed739517c548ba/src/cmd/go/internal/work/build.go#L459-L478)). +- `load.(*Package).exeFromImportPath` takes the final loaded import-path + element, while `DefaultExecName` uses a source basename only for a + command-line-files package + ([`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)). +- Official command testdata distinguishes module naming from the manifest-free + GOPATH case and requires bare `go build` to create the directory-named `src` + executable + ([`clean_binary.txt`, lines 15–28](https://github.com/golang/go/blob/c19862e5f8415b4f24b189d065ed739517c548ba/src/cmd/go/testdata/script/clean_binary.txt#L15-L28)). + A second manifest-free script changes to `m`, runs bare `go build`, and + requires executable `m` + ([`gccgo_m.txt`, lines 4–14](https://github.com/golang/go/blob/c19862e5f8415b4f24b189d065ed739517c548ba/src/cmd/go/testdata/script/gccgo_m.txt#L4-L14)); + `build_static.txt` likewise builds and executes the default `hello` + ([lines 11–14](https://github.com/golang/go/blob/c19862e5f8415b4f24b189d065ed739517c548ba/src/cmd/go/testdata/script/build_static.txt#L11-L14)). + +Those selection, loading, default-name branches and script assertions are +**behavior directly implemented or asserted by pinned Go**. WW has no module +or manifest identity for a literal root, so using the selected local directory +leaf as its already-specified presentation fallback is **behavior derived from +the pinned implementation**. The directory remains loader metadata and does +not become canonical package or import identity. + +#### Fresh four-axis audit and direct pre-fix measurements + +The bounded audit examined every permanent axis before choosing this build +presentation difference. Both public stages were measured at the same source +paths: + +- **Go-like build:** multi-command directory selection without `-o` was already + aligned. For a valid command in directory `a`, however, bare `build`, + `build .`, and `build ./` each exited 1 in both stages with empty stdout and + the same 55 stderr bytes (SHA-256 + `2e8030a9eeb7187fbc1cb4ee9d794c352f1c31c92c884f8ae09a57e39bfe86db`): + `ww: build output "." already exists and is a directory`. Explicit `-o` + succeeded and produced byte-identical runnable binaries, proving that only + default presentation was wrong. This was the selected gap. +- **Go-like test:** an ordinary valid zero-match directory run exited 0 in both + stages with identical warning, accounting, and `[no tests to run]` result + bytes. The selected build branch does not enter test product naming, + filtering, capture, execution, result annotation, or retained test output. +- **Go-like package:** a directory containing two selected declared package + names was rejected in both stages with identical diagnostics. The selected + change occurs after package loading and does not alter source eligibility, + declaration checks, package kind, graph nodes, or actions. +- **Go-like import:** a dotted `cyclea -> cycleb -> cyclea` graph was rejected + in both stages with identical cycle diagnostics. The selected change does + not alter spelling, search, visibility, resolution, canonical identity, or + graph edges. + +The pre-fix statuses, streams, hashes, diagnostics, artifacts, and runtime +results are **directly measured WW behavior**. The cited implementation and +testdata are **behavior directly implemented or asserted by pinned Go**. The +cross-axis non-effects follow from the bounded post-load output branch and are +**behavior derived from the pinned implementation**. + +#### Ownership, final behavior, and preserved boundaries + +`cmd/ww.do_build` and `selfhost/cmd/ww.dobuild` are semantic twins and the sole +owners of this rule. Their current-directory predicate accepts only relative +paths whose components are all exactly `.`. Only in that branch do they +canonicalize the selected directory and take its final component for output and +cold scratch presentation. Ordinary literal directory operands retain their +lexical leaf, and contextual roots retain their dotted identity leaf. + +Direct post-fix probes through both stages show that bare, dot, and dot-slash +builds exit 0 with empty stdout/stderr, publish mode-executable binaries named +`a`, and produce `a.sepwork` with no `..sepwork`. All six binaries are +byte-identical (SHA-256 +`866c1eb875dad271d37572f43fb9d9b0eb6a2344d2e61646e655bb09f7909bf6`). +Their unit, interface, assembly, object, archive, and init artifacts are also +byte-identical between stages and spellings. Current-directory library builds +still publish nothing and link nothing; their unit, interface, assembly, +object, and archive bytes remain stage-identical under `libcurrent.sepwork`. + +Loading and source selection precede this branch. A missing dotted import +therefore retains its byte-identical diagnostic and creates neither output nor +scratch. Graph and action construction, compiler/assembler/archiver/linker +semantics, runtime behavior, and artifact content are unchanged. A genuine +directory occupying the derived output still rejects before tools and names +the derived leaf in its diagnostic. + +Cold and warm persistent builds retain their existing keys and reuse rules. A +source invalidation reruns producers; an injected compiler failure preserves +the prior executable and committed persistent generation, publishes no stage, +and leaves no `.new`, `.install`, or `.wwtxn.*` residue. Restoring producer +success installs the changed executable. Existing concurrency, interruption, +process-group, rollback, publication, and cleanup owners gain no shared state or +new process path. + +The WW-native `current_directory_build_default_output` observer proves both +stages, all three current-directory spellings, executable mode/runtime/byte +parity, explicit and null output controls, non-main non-publication, corrected +cold scratch, missing-import precedence, cold/warm reuse, invalidation, +producer-failure rollback, prior-output preservation, genuine collision, and +residue absence. Existing package/import graph, byte-identity, concurrent +transaction, and interruption observers remain authoritative for mechanisms +this presentation rule does not change. + +No persisted-byte contract changed. Build workdir format remains `18`, test +workdir format remains `19`, and semantic storage format remains `3`. + ## 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 ed24db7e..85953152 100644 --- a/docs/spec.md +++ b/docs/spec.md @@ -340,8 +340,17 @@ ImportPath = ident { "." ident } . 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, + output. With no path operand, the current directory is selected as if `.` had + been supplied. For a lone literal current-directory command and no explicit + `-o`, `.`, `./`, and equivalent single-dot-component spellings derive the + default executable name from the canonical selected directory's final + component; cold adjacent scratch uses that same leaf plus `.sepwork`. The dot + spelling itself is never an output name. A current-directory non-main package + uses the corrected scratch presentation but still links and publishes + nothing. This physical leaf is fallback presentation metadata only: it never + displaces a requested contextual dotted identity or becomes package, import, + graph, action, artifact, symbol, `.wwi`, or persistence identity. + 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 diff --git a/selfhost/cmd/ww/main.ww b/selfhost/cmd/ww/main.ww index eb352b0b..cf3a4f26 100644 --- a/selfhost/cmd/ww/main.ww +++ b/selfhost/cmd/ww/main.ww @@ -8629,7 +8629,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|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"; + 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 . current directory package (default when no path is given)\n"; os.write(fd, s.ptr, s.len: u64); }; @@ -8716,6 +8716,43 @@ fn defaultimportoutpath(identity: *u8) *u8 = { return out.ptr; }; +fn buildcurrentdirspelling(path: *u8) bool = { + if (path[0u64] == 0u8 || path[0u64] == '/': u8) { return false; }; + let i: u64 = 0u64; + let found: bool = false; + for (true) { + for (path[i] == '/': u8) { i += 1u64; }; + if (path[i] == 0u8) { return found; }; + if (path[i] != '.': u8) { return false; }; + i += 1u64; + if (path[i] != 0u8 && path[i] != '/': u8) { return false; }; + found = true; + }; + return false; +}; + +fn buildlocaldirleaf(resolved: *u8) *u8 = { + let selected: *u8 = resolved; + if (buildcurrentdirspelling(resolved)) { + let canonical: *u8 = canonicaldir(pathstr(resolved)); + if (canonical != nil) { selected = canonical; }; + }; + let rlen: u64 = cstrlen(selected); + for (rlen > 1u64) { + if (selected[rlen - 1u64] != '/': u8) { break; }; + rlen -= 1u64; + }; + let bo: u64 = basenameoff(selected, rlen); + let outbuf: []u8 = alloc([], (os.PATH_MAX: u64))!; + outbuf.len = os.PATH_MAX; + let out: *u8 = outbuf.ptr; + let i: u64 = bo; + let off: u64 = 0u64; + for (i < rlen) { out[off] = selected[i]; off += 1u64; i += 1u64; }; + cstrseal(out, off); + return out; +}; + fn dobuild(selfdir: *u8, argv: **u8, argc: i32, start: i32) i32 = { let src: *u8 = nil; let srcindex: i32 = -1; @@ -8897,22 +8934,14 @@ fn dobuild(selfdir: *u8, argv: **u8, argc: i32, start: i32) i32 = { } else { if (isdir != 0 && rootidentity != nil) { out = defaultimportoutpath(rootidentity); } else { if (isdir != 0) { - let rlen: u64 = cstrlen(resolved); - for (rlen > 1u64) { - if (resolved[rlen - 1u64] != 47u8) { break; }; - rlen -= 1u64; - }; - let bo: u64 = basenameoff(resolved, rlen); - let outbuf: []u8 = alloc([], (os.PATH_MAX: u64))!; - outbuf.len = os.PATH_MAX; - out = outbuf.ptr; - let i: u64 = bo; - let off: u64 = 0u64; - for (i < rlen) { out[off] = resolved[i]; off += 1u64; i += 1u64; }; - cstrseal(out, off); + out = buildlocaldirleaf(resolved); } else { out = defaultoutpath(resolved); }; }; }; + if ((outflag == nil || outflag[0u64] == 0u8) && isdir != 0 + && buildcurrentdirspelling(resolved)) { + objstem = out; + }; let defaultoutputdir: *u8 = nil; if ((outflag == nil || outflag[0u64] == 0u8) && buildoutputdir(out)) { defaultoutputdir = out; diff --git a/test/package/package_test.ww b/test/package/package_test.ww index e9f1bc67..66b1ef5f 100644 --- a/test/package/package_test.ww +++ b/test/package/package_test.ww @@ -14074,6 +14074,227 @@ fn runtimepath(relative: str) str = { clean(root); }; +// Empty build patterns select the current directory. A literal dot is +// loading syntax, not the caller-visible output name: command publication and +// cold scratch use the selected directory leaf without making that physical +// leaf package or import identity. +@test fn current_directory_build_default_output() void = { + let root: str = fresh(); + let command: str = strings.concat(root, "/current-command"); + let library: str = strings.concat(root, "/current-library"); + let bad: str = strings.concat(root, "/bad-current"); + mkdirall(command); mkdirall(library); mkdirall(bad); + let commandfile: str = strings.concat(command, "/main.ww"); + let original: str = "package main;\nfn main() i32 = { return 23; };\n"; + let changed: str = "package main;\nfn main() i32 = { return 24; };\n"; + writefile(commandfile, original); + writefile(strings.concat(library, "/library.ww"), strings.concat( + "package declared_library;\n", + "export fn value() i32 = { return 7; };\n")); + writefile(strings.concat(bad, "/main.ww"), strings.concat( + "package main;\nimport missing.current;\n", + "fn main() i32 = { return missing.current.value(); };\n")); + + let compilerwrapper: str = strings.concat(root, "/current-w6c.sh"); + writeexecutable(compilerwrapper, strings.concat( + "#!/bin/sh\nprintf 'compile\\n' >> \"$WW_CURRENT_TRACE\"\n", + "if test \"$WW_CURRENT_FAIL\" = 1; then\n", + " printf 'injected current-directory compiler failure\\n' >&2\n", + " exit 97\nfi\nexec \"$WW_CURRENT_REAL_C\" \"$@\"\n")); + + let stages: []str = ["ww", "ww_ww"]; + let compilers: []str = ["w6c", "w6c_ww"]; + let tags: []str = ["c", "ww"]; + let spellings: []str = ["", ".", "./"]; + let spellingtags: []str = ["bare", "dot", "slash"]; + let baseenv: []str = os.getenvs(); + let commandref: str = ""; + let changedref: str = ""; + let missingref: str = ""; + let failref: str = ""; + let collisionref: str = ""; + let out: commandout; + let si: i32 = 0; + for (si < stages.len) { + rewritefile(commandfile, original); + let defaultbin: str = strings.concat(command, "/current-command"); + let defaultscratch: str = strings.concat(defaultbin, ".sepwork"); + let legacyscratch: str = strings.concat(command, "/..sepwork"); + + // Bare, dot, and dot-slash spellings select the same package and + // produce the same directory-named executable and cold artifacts. + let pi: i32 = 0; + for (pi < spellings.len) { + let av: []str = [driver(stages[si]), "build"]; + if (spellings[pi].len != 0) { append(av, spellings[pi]); }; + runcommanddir(root, strings.concat("current-", tags[si], "-", + spellingtags[pi]), command, av, + (60i64 * (time.second: i64)): time.duration, &out); + expectexit(&out, 0); + assert(out.stdout.len == 0 && out.stderr.len == 0); + assert(os.exists(defaultbin) && os.exists(defaultscratch)); + assert(!os.exists(legacyscratch)); + assert((permissionmode(defaultbin) & 73u32) == 73u32); + let built: str = readfile(defaultbin); + if (commandref.len == 0) { commandref = strings.dup(built); } + else { assert(same(commandref, built)); }; + let runav: []str = [defaultbin]; + runcommand(root, strings.concat("current-run-", tags[si], "-", + spellingtags[pi]), runav, time.second, &out); + expectexit(&out, 23); + assert(!directoryhasnew(command) + && !directoryhasfragment(command, ".install") + && !directoryhasfragment(command, ".wwtxn.")); + assert(os.remove(defaultbin) == 0); + clean(defaultscratch); + pi += 1; + }; + + // Explicit output and exact null output keep their established + // disposition and never create the default current-directory name. + let explicitout: str = strings.concat(root, "/explicit-", tags[si]); + let explicitav: []str = [driver(stages[si]), "build", "-o", + explicitout]; + runcommanddir(root, strings.concat("current-explicit-", tags[si]), + command, explicitav, + (60i64 * (time.second: i64)): time.duration, &out); + expectexit(&out, 0); + assert(out.stdout.len == 0 && out.stderr.len == 0); + assert(same(commandref, readfile(explicitout)) + && !os.exists(defaultbin) && !os.exists(defaultscratch)); + assert(os.remove(explicitout) == 0); + clean(strings.concat(explicitout, ".sepwork")); + let nullav: []str = [driver(stages[si]), "build", "-o", "/dev/null"]; + runcommanddir(root, strings.concat("current-null-", tags[si]), command, + nullav, (60i64 * (time.second: i64)): time.duration, &out); + expectexit(&out, 0); + assert(out.stdout.len == 0 && out.stderr.len == 0); + assert(!os.exists(defaultbin) && !os.exists(defaultscratch) + && !os.exists(legacyscratch)); + + // A non-main current-directory build compiles and archives privately, + // but neither links nor publishes a directory-named file. + let librarybin: str = strings.concat(library, "/current-library"); + let libraryscratch: str = strings.concat(librarybin, ".sepwork"); + let librarylegacy: str = strings.concat(library, "/..sepwork"); + let libraryav: []str = [driver(stages[si]), "build"]; + runcommanddir(root, strings.concat("current-library-", tags[si]), + library, libraryav, + (60i64 * (time.second: i64)): time.duration, &out); + expectexit(&out, 0); + assert(out.stdout.len == 0 && out.stderr.len == 0); + assert(!os.exists(librarybin) && os.exists(libraryscratch) + && !os.exists(librarylegacy)); + assert(!directoryhasnew(library) + && !directoryhasfragment(library, ".wwtxn.")); + clean(libraryscratch); + + // Import loading retains precedence over output planning. Failure + // creates neither the corrected output nor either scratch spelling. + let badbin: str = strings.concat(bad, "/bad-current"); + let badav: []str = [driver(stages[si]), "build"]; + runcommanddir(root, strings.concat("current-missing-", tags[si]), bad, + badav, (60i64 * (time.second: i64)): time.duration, &out); + expectexit(&out, 1); + assert(out.stdout.len == 0 + && has(out.stderr, "cannot find package missing.current\n")); + assert(!os.exists(badbin) + && !os.exists(strings.concat(badbin, ".sepwork")) + && !os.exists(strings.concat(bad, "/..sepwork"))); + if (si == 0) { missingref = strings.dup(out.stderr); } + else { assert(same(missingref, out.stderr)); }; + + // Persistent cold/warm reuse keeps the same output. An invalidated + // producer failure preserves both that prior output and clean public + // state; a later successful invalidation publishes the changed binary. + let work: str = strings.concat(root, "/current-work-", tags[si]); + let trace: str = strings.concat(root, "/current-trace-", tags[si]); + mkdirall(work); writefile(trace, ""); + let env: []str = alloc([], (baseenv.len + 4): u64)!; + let ei: i32 = 0; + for (ei < baseenv.len) { + if (!strings.hasprefix(baseenv[ei], "WW_W6C=") + && !strings.hasprefix(baseenv[ei], "WW_CURRENT_")) { + append(env, baseenv[ei]); + }; + ei += 1; + }; + append(env, strings.concat("WW_W6C=", compilerwrapper)); + append(env, strings.concat("WW_CURRENT_REAL_C=", + driver(compilers[si]))); + append(env, strings.concat("WW_CURRENT_TRACE=", trace)); + let failindex: i32 = env.len; + append(env, "WW_CURRENT_FAIL="); + let persistav: []str = [driver(stages[si]), "build", "-w", work]; + runcommandenvdir(root, strings.concat("current-cold-", tags[si]), + persistav, env, command, + (60i64 * (time.second: i64)): time.duration, &out); + expectexit(&out, 0); + assert(out.stdout.len == 0 && out.stderr.len == 0 + && readfile(trace).len != 0); + let prior: str = readfile(defaultbin); + assert(same(commandref, prior)); + rewritefile(trace, ""); + runcommandenvdir(root, strings.concat("current-warm-", tags[si]), + persistav, env, command, + (60i64 * (time.second: i64)): time.duration, &out); + expectexit(&out, 0); + assert(out.stdout.len == 0 && out.stderr.len == 0 + && readfile(trace).len == 0 && same(prior, readfile(defaultbin))); + rewritefile(commandfile, changed); rewritefile(trace, ""); + env[failindex] = "WW_CURRENT_FAIL=1"; + runcommandenvdir(root, strings.concat("current-fail-", tags[si]), + persistav, env, command, + (60i64 * (time.second: i64)): time.duration, &out); + expectexit(&out, 1); + assert(readfile(trace).len != 0 + && has(out.stderr, "injected current-directory compiler failure\n") + && has(out.stderr, "ww: w6c failed for ") + && same(prior, readfile(defaultbin))); + assert(!directoryhasnew(command) && !directoryhasnew(work) + && !directoryhasfragment(command, ".install") + && !directoryhasfragment(command, ".wwtxn.") + && !directoryhasfragment(work, ".wwtxn.")); + if (si == 0) { failref = strings.dup(out.stderr); } + else { assert(same(failref, out.stderr)); }; + env[failindex] = "WW_CURRENT_FAIL="; rewritefile(trace, ""); + runcommandenvdir(root, strings.concat("current-changed-", tags[si]), + persistav, env, command, + (60i64 * (time.second: i64)): time.duration, &out); + expectexit(&out, 0); + assert(out.stdout.len == 0 && out.stderr.len == 0 + && readfile(trace).len != 0); + let changedbytes: str = readfile(defaultbin); + assert(!same(prior, changedbytes)); + if (changedref.len == 0) { changedref = strings.dup(changedbytes); } + else { assert(same(changedref, changedbytes)); }; + let changedrun: []str = [defaultbin]; + runcommand(root, strings.concat("current-changed-run-", tags[si]), + changedrun, time.second, &out); + expectexit(&out, 24); + assert(os.remove(defaultbin) == 0); + clean(work); + rewritefile(commandfile, original); + + // A genuine destination-directory collision still rejects at output + // preflight, now naming the derived leaf rather than the dot selector. + mkdirall(defaultbin); + runcommanddir(root, strings.concat("current-collision-", tags[si]), + command, libraryav, + (60i64 * (time.second: i64)): time.duration, &out); + expectexit(&out, 1); + assert(out.stdout.len == 0 && same(out.stderr, strings.concat( + "ww: build output \"current-command\" already exists and is a directory\n"))); + assert(!os.exists(defaultscratch) && !os.exists(legacyscratch)); + if (si == 0) { collisionref = strings.dup(out.stderr); } + else { assert(same(collisionref, out.stderr)); }; + clean(defaultbin); + si += 1; + }; + assert(!directoryhasnew(root) && !directoryhasfragment(root, ".wwtxn.")); + 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