From ca0e533c133a5e168a25d9e2b54529f80f90c37d Mon Sep 17 00:00:00 2001 From: Hojun-Cho Date: Sat, 22 Aug 2026 23:39:06 +0900 Subject: [PATCH] build: route .ww directories as packages --- docs/build-system.md | 132 +++++++++ docs/spec.md | 38 ++- docs/test-system-v2.md | 20 ++ selfhost/cmd/ww/main.ww | 9 +- test/package/package_test.ww | 514 +++++++++++++++++++++++++++++++++-- 5 files changed, 683 insertions(+), 30 deletions(-) diff --git a/docs/build-system.md b/docs/build-system.md index 4f0501eb..41941c1a 100644 --- a/docs/build-system.md +++ b/docs/build-system.md @@ -10244,6 +10244,138 @@ No serialized format changes accompany this omission. Build workdir format remains `18`, test workdir format remains `19`, semantic storage format remains `3`, and the slice adds no cache or persistent record. +### 11.52 Implemented `.ww`-spelled directory build routing + +An existing local literal passed to `ww build` whose requested final basename +ends `.ww` is a directory-package request when ordinary `stat` reports a +directory. This includes a direct directory named `.ww` and a visible +`_test.ww` symlink to a directory. The suffix does not turn the directory into +a raw source and does not trigger the non-directory test-source omission in +11.51. + +#### Pinned authority and applicability + +- **behavior directly implemented or asserted by pinned Go** — official Go + 1.26.5 commit `c19862e5f8415b4f24b189d065ed739517c548ba` implements + symlink-following `Stat` in `cmd/go/internal/fsys/fsys.go:633–638`. + `PackagesAndErrors` enters named-file mode only when an operand ends `.go`, + `Stat` succeeds, and the result is not a directory + (`cmd/go/internal/load/pkg.go:2903–2932`, especially 2911–2918). Local + literals that do not enter that route are matched and directory-loaded by + `cmd/go/internal/modload/load.go:251–307,526–597` and + `cmd/go/internal/search/search.go:276–293`. `runBuild` uses that loader at + `cmd/go/internal/work/build.go:459–477`. +- **behavior directly implemented or asserted by pinned Go** — official + `cmd/go/testdata/script/list_ambiguous_path.txt:1–15,29–36` directly proves + that directory `./foo.go` is a package while regular file `./a.go` is a + `command-line-arguments` package. Official + `cmd/go/testdata/script/mod_symlink_dotgo.txt:1–9,11–17` directly asserts + that `dir.go -> dir` is not a source entry; `go/build/build.go:886–900` + implements that directory-enumeration rule. +- **behavior derived from the pinned implementation** — because the initial + `Stat` follows the terminal symlink, a requested `.go` path whose target is a + directory takes the directory package route under build. That file-kind rule + applies directly to WW's existing local raw-source/directory distinction and + needs no module, manifest, registry, cache, network resolution, generalized + import syntax, or build expression. + +#### Ownership and command boundary + +- **directly measured WW behavior** — before this change, Cstage already + stat-routed a valid `visible_test.ww -> pkgdir` build, produced a normal + directory graph and executable, and returned status 0 with empty streams. + WWstage instead returned status 1, empty stdout, exact + `ww: cannot read source\n` stderr, and no artifact. The same split occurred + for `visible.ww -> pkgdir` and a real directory named `literal.ww`; a + `regular_test.ww` symlink to a regular source retained the aligned 11.51 + omission. +- **directly measured WW behavior** — the true owner is the + requested-kind branch in `selfhost/cmd/ww/main.ww::dobuild`, after its + existing `Stat`, prefix exclusion, and non-directory `_test.ww` check and + before logical resolution. Cstage and both test fronts already have the + required classification and do not change. +- **behavior directly implemented or asserted by pinned Go** — `go run` has a + deliberately different front door: `cmd/go/internal/run/run.go:95–112` + consumes leading `.go` suffix operands as named files without build/test's + directory guard, after which `GoFilesPackage` rejects a directory at + `cmd/go/internal/load/pkg.go:3274–3281`. +- **behavior derived from the pinned implementation** — therefore this slice + does not change the shared WW resolver or `ww run`. Multiple operands, + recursive patterns, logical names, ordinary directories, regular and + non-regular sources, dangling symlinks, raw `ww test`, `test -c`, and + `test -S` retain their existing front doors. + +#### Build, test, package, and import effects + +- **directly measured WW behavior** — build is the primary + axis. The selected request enters the same directory enumeration, graph, + action, output, and transaction route as its direct target and the Cstage + spelling. A requested `_test.ww` basename is only request metadata; ordinary + directory production selection still excludes actual `*_test.ww` entries. +- **directly measured WW behavior** — package selection is + the mechanism. The requested symlink spelling and physical target remain + loader/presentation metadata; downstream canonical local package, graph, + action, symbol, artifact, `.wwi`, publication, and persistence identities are + unchanged. No fake raw root or spelling-specific generation is created. +- **directly measured WW behavior** — imports are exactly + the selected directory package's existing dotted imports. The change adds no + qualifier, syntax, search order, edge kind, cycle rule, or identity. It makes + the established graph reachable instead of failing before directory load. +- **directly measured WW behavior** — raw `ww test` was already aligned because + both stages independently stat literal test requests. Running tests, `-c` + retained binaries, `-S` directory diagnostics, stdout/stderr, and binary + bytes matched through the `.ww` symlink before the build fix. Test-process + topology and `-run` matching are explicit non-effects. + +#### Diagnostics, artifacts, and lifecycle + +- **directly measured WW behavior** — CLI parsing, + multi-root/tree delegation, leading-prefix selection, and non-directory + `_test.ww` header diagnostics keep their precedence. After directory + selection, no-source, package/import/source, output, producer, publication, + and collision diagnostics use the existing directory order and requested + path presentation. The false raw-source read diagnostic disappears only for + this build route. +- **directly measured WW behavior** — file output, + output-directory publication, exact `/dev/null`, assembly-only `-S`, default + output, and persistent `-w` reuse use the existing directory actions and + transaction. Cold success creates the same units, `.wwi`, assembly, objects, + archives, initializer products, and optional executable as Cstage. Cold + source or producer failure commits no partial generation; warm injected + compiler failure and signaled-linker failure preserve every prior public and + semantic byte and leave no `.new`, install backup, or `.wwtxn.*`. +- **directly measured WW behavior** — source/import diagnostics, compiler + failure, and a signaled linker preserve the equivalent directory request's + prior output and work bytes. The focused concurrent row proves isolated + Cstage/WWstage selected requests. The classifier itself is request-local and + read-only; unchanged directory machinery continues to own assembler, + archiver, ordinary linker and publication failures, shared action locking, + driver interruption, process reaping, rollback, and filesystem cleanup. +- **directly measured WW behavior** — direct `SIGTERM` of either driver while a + selected warm build is blocked in compilation is an inherited lifecycle + non-effect, not part of the classifier change. Both stages terminate with + shell status 143, reap the complete owned process group, and preserve the + prior public output and committed semantic bytes, but the existing directory + machinery leaves three request-private `.new` files. That independently + verified cleanup gap remains open and this slice does not describe it as + fixed. +- **directly measured WW behavior** — Cstage and WWstage agree byte-for-byte + on status, stdout, stderr, diagnostics, public output, and every semantic + artifact for selected success and failure rows. Complete + within-stage preservation includes the invoking driver's `.wwtool.ww` + provenance snapshot; cross-stage semantic comparison excludes only those + intentionally different producer bytes. + +The focused WW-native observer directly measures direct and symlinked `.ww` +directories, the visible `_test.ww` spelling, regular-file omission, directory +no-source and source/import precedence, a reachable dotted dependency, +default/file/directory/null/assembly outputs, cold and warm +reuse/invalidation/rollback, compiler failure and signaled-linker interruption, +runtime reachability, selected-route concurrency, cleanup, and the exact +test/run command boundaries. No serialized representation changes: build +workdir format remains `18`, test workdir format remains `19`, semantic storage +format remains `3`, and no cache or result record is added. + ## 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 7ffc3777..64eec652 100644 --- a/docs/spec.md +++ b/docs/spec.md @@ -358,6 +358,22 @@ ImportPath = ident { "." ident } . symbol, `.wwi`, publication, or persistence identity. Build workdir format remains 18, test workdir format remains 19, and semantic storage format remains 3. +- A local literal passed to `ww build` whose requested final basename ends + `.ww` remains a directory request when ordinary `stat` reports a directory, + including when the spelling is a symlink or ends `_test.ww`. The directory + target therefore bypasses raw named-source prefix and test-source + classification and enters ordinary directory enumeration. Actual eligible + production entries, package declarations, imports, graph actions, producers, + output policy, publication, persistence, invalidation, rollback, and cleanup + are exactly those of the existing directory package route. The requested + spelling and physical target are loader/presentation metadata only and create + no new package, dotted-import, graph, action, symbol, artifact, `.wwi`, + publication, or persistence identity. Raw `ww test`, `ww test -c`, and + `ww test -S` already use the same directory classification and are unchanged. + `ww run` retains its separate named-operand front door. Multiple operands, + recursive and logical requests, regular and non-regular source targets, and + dangling symlinks are not changed. Build workdir format remains 18, test + workdir format remains 19, and semantic storage format remains 3. - `import acme.codec;` loads the canonical package `acme.codec`. If that package declares `package wire;`, the importing file sees its exported names as `wire.Name`; `codec.Name` is not an additional binding. An explicit alias @@ -465,14 +481,24 @@ ImportPath = ident { "." ident } . the action list; non-main command dependencies retain ordinary actions, and a selection containing no command rejects before tools or output creation. A raw command-line `.ww` source uses its source basename without `.ww`, and - a raw non-main source is the same no-command rejection. Missing - trailing-slash hierarchies are created transactionally from `0777`, filtered - by the caller's umask. + a raw non-main source is the same no-command rejection. An existing + directory whose requested basename ends `.ww` is not such a raw source: it + retains directory-package output naming. With no `-o`, its requested leaf + is used verbatim (for example, `directory_test.ww`); with an existing + output-directory `-o`, publication instead uses the canonical selected + target leaf (for example, `directory-target`). + Missing trailing-slash hierarchies are created transactionally from `0777`, + filtered by the caller's umask. Loading and package/import diagnostics precede no-main, derived-path, duplicate-destination, implicit-default, and other output preflight; all of - those checks precede creation. Producer failure or interruption preserves - existing directory contents and removes only request-created prefixes and - stages. A non-directory output retains the single-product file/archive rule. + those checks precede creation. Producer failure and coordinator-owned + interruption preserve existing directory contents and remove only + request-created prefixes and stages. Direct external `SIGTERM` of a build + driver is a verified-open exception: both stages preserve public and + committed work bytes and reap their process group, but may leave `.new` + staging files that make a later persistent-work request reject until those + files are removed. A non-directory output retains the single-product + file/archive rule. If a lone command's synthesized default basename already names a directory, loading and graph validation complete and the build rejects before tools without diff --git a/docs/test-system-v2.md b/docs/test-system-v2.md index 35c51e84..c2d4c8d9 100644 --- a/docs/test-system-v2.md +++ b/docs/test-system-v2.md @@ -265,6 +265,26 @@ directory/recursive selection, package/import identities, graph/action identities, persistence formats (build 18, test 19, semantic 3), and test process topology are explicit non-effects. +An existing local directory whose requested build basename ends `.ww` +(including a visible `_test.ww` symlink to a directory) remains a directory +package, not a raw named test source. WWstage `ww build` now uses the same +symlink-following requested-kind decision as Cstage and enters the established +directory coordinator. The directory's actual `*_test.ww` entries remain +excluded from production and available to `ww test`; the requested suffix does +not itself create a test variant. Raw `ww test`, `test -c`, and `test -S` were +already stat-routed and do not change. The focused package observer directly +measures Cstage/WWstage build diagnostics and semantic/public artifact-byte +parity, direct and symlinked `.ww` directories, a dotted dependency, output +modes, cold/warm transaction behavior, compiler and signaled-linker failure +preservation, selected-route concurrency, cleanup, and the exact test-command +behavior. Direct driver interruption remains owned by the unchanged directory +machinery and is not claimed fixed here. The shared resolver and `ww run` are +deliberately unchanged—and the observer +retains their pre-existing stage difference—because pinned Go's run command +has a distinct named-file front door. +No test product, result, package/import identity, or serialized format changes; +build remains 18, test remains 19, and semantic storage remains 3. + List mode uses that same product process and initialization boundary but starts no per-test child. The shared language harness emits only selected qualified test names, one per line in descriptor order. A valid filter selecting no tests diff --git a/selfhost/cmd/ww/main.ww b/selfhost/cmd/ww/main.ww index fe644b91..3fc3a9b7 100644 --- a/selfhost/cmd/ww/main.ww +++ b/selfhost/cmd/ww/main.ww @@ -9069,7 +9069,14 @@ fn dobuild(selfdir: *u8, argv: **u8, argc: i32, start: i32) i32 = { return 1; }; let isdir: i32 = 0; - let resolved: *u8 = resolvemodule(selfdir, src, incs.ptr, &isdir); + let resolved: *u8 = nil; + if (requestedliteral && !requestednondirectory + && cstrendswithlit(src, ".ww")) { + isdir = 1; + resolved = src; + } else { + resolved = resolvemodule(selfdir, src, incs.ptr, &isdir); + }; if (resolved == nil) { cerrpath("ww build: cannot find module ", src, "\n"); return 1; diff --git a/test/package/package_test.ww b/test/package/package_test.ww index 1c5e5c54..0f1bd096 100644 --- a/test/package/package_test.ww +++ b/test/package/package_test.ww @@ -1466,14 +1466,20 @@ fn cwdwritedata(dir: str, label: str) void = { }; // A visible raw test-source build observes only its initial package/import -// header. It must not turn that observation into a package, import, action, -// artifact, or work-state identity. +// header and then omits it. A .ww-spelled directory, including a symlink with +// a _test.ww spelling, instead enters the ordinary directory-package route. +// Neither classification may redefine package or dotted-import identity. @test fn named_test_sources_are_header_loaded_then_omitted() void = { let root: str = fresh(); let source: str = strings.concat(root, "/source"); - let dirsource: str = strings.concat(source, "/directory"); + let include: str = strings.concat(root, "/include"); + let dependency: str = strings.concat(include, "/domain/value"); + let dirsource: str = strings.concat(source, "/directory.ww"); let dirtarget: str = strings.concat(source, "/directory-target"); - mkdirall(source); mkdirall(dirsource); mkdirall(dirtarget); + let emptydir: str = strings.concat(source, "/empty.ww"); + let baddirtarget: str = strings.concat(source, "/broken-target"); + mkdirall(source); mkdirall(dependency); mkdirall(dirsource); mkdirall(dirtarget); + mkdirall(emptydir); mkdirall(baddirtarget); let ignored: str = strings.concat(source, "/raw_windows_test.ww"); let platform: str = strings.concat(source, "/platform_windows_test.ww"); @@ -1486,11 +1492,18 @@ fn cwdwritedata(dir: str, label: str) void = { let linktarget: str = strings.concat(source, "/target.ww"); let link: str = strings.concat(source, "/requested_test.ww"); let dirlink: str = strings.concat(source, "/directory_test.ww"); + let baddirlink: str = strings.concat(source, "/broken_test.ww"); let rawmain: str = strings.concat(source, "/raw.ww"); let runsource: str = strings.concat(source, "/run_test.ww"); let testroute: str = strings.concat(source, "/route_test.ww"); let warmpath: str = strings.concat(source, "/warm.ww"); let warmtest: str = strings.concat(source, "/warm_test.ww"); + let dirtargettest: str = strings.concat(dirtarget, "/ignored_test.ww"); + let dirtargettestexcluded: str = + "package main;\nimport missing.test.only;\n"; + let dirtargettestselected: str = strings.concat( + "package main;\nimport domain.value;\n", + "@test fn directory_selected() void = { assert(value.number() == 5); };\n"); let header: str = strings.concat( "package raw_test;\nimport absent.pkg;\n", "this body is deliberately malformed and never reaches a producer {\n", @@ -1513,24 +1526,40 @@ fn cwdwritedata(dir: str, label: str) void = { "package route;\n", "@test fn still_selected() void = { assert(true); };\n")); writefile(warmpath, "package main;\nfn main() i32 = { return 6; };\n"); + writefile(strings.concat(dependency, "/value.ww"), strings.concat( + "package value;\n", + "export fn number() i32 = { return 5; };\n")); writefile(strings.concat(dirsource, "/main.ww"), "package main;\nfn main() i32 = { return 3; };\n"); - writefile(strings.concat(dirtarget, "/main.ww"), - "package main;\nfn main() i32 = { return 5; };\n"); + writefile(strings.concat(dirsource, "/ignored_test.ww"), + "package main;\nimport missing.test.only;\n"); + writefile(strings.concat(dirtarget, "/main.ww"), strings.concat( + "package main;\nimport domain.value;\n", + "fn main() i32 = { return value.number(); };\n")); + writefile(dirtargettest, dirtargettestexcluded); + writefile(strings.concat(baddirtarget, "/main.ww"), + "package main;\nimport ;\n"); assert(os.symlink(linktarget, link) == 0); assert(os.symlink(dirtarget, dirlink) == 0); + assert(os.symlink(baddirtarget, baddirlink) == 0); let compilerwrapper: str = strings.concat(root, "/testonly-w6c.sh"); let assemblerwrapper: str = strings.concat(root, "/testonly-w6a.sh"); let linkerwrapper: str = strings.concat(root, "/testonly-w6l.sh"); writeexecutable(compilerwrapper, strings.concat( "#!/bin/sh\nprintf 'compile\\n' >> \"$WW_TESTONLY_CTRACE\"\n", + "if test \"$WW_TESTONLY_FAIL_C\" = 1; then\n", + " printf 'injected directory compiler failure\\n' >&2\n", + " exit 97\nfi\n", "exec \"$WW_TESTONLY_REAL_C\" \"$@\"\n")); writeexecutable(assemblerwrapper, strings.concat( "#!/bin/sh\nprintf 'assemble\\n' >> \"$WW_TESTONLY_ATRACE\"\n", "exec \"$WW_TESTONLY_REAL_A\" \"$@\"\n")); writeexecutable(linkerwrapper, strings.concat( "#!/bin/sh\nprintf 'link\\n' >> \"$WW_TESTONLY_LTRACE\"\n", + "if test \"$WW_TESTONLY_LINK_MODE\" = signal; then\n", + " kill -TERM \"$$\"\n", + "fi\n", "exec \"$WW_TESTONLY_REAL_L\" \"$@\"\n")); let stages: []str = ["ww", "ww_ww"]; @@ -1552,15 +1581,32 @@ fn cwdwritedata(dir: str, label: str) void = { let ccompiled: str = ""; let ctestasm: str = ""; let crawbin: str = ""; + let cdirectdirbin: str = ""; + let cdirectdirartifacts: str = ""; + let clinkdirbin: str = ""; + let clinkdirartifacts: str = ""; + let clinkdirasm: str = ""; + let clinkdirplainasm: str = ""; + let clinkdirchanged: str = ""; + let clinkdirchangedartifacts: str = ""; + let compilerfaildiag: str = ""; + let linkersignaldiag: str = ""; + let directorytestrunout: str = ""; + let directorytestrunerr: str = ""; + let directorytestrunartifacts: str = ""; + let directorytestbin: str = ""; + let directorytestcompileartifacts: str = ""; + let directorytestasm: str = ""; let baseenv: []str = os.getenvs(); let si: i32 = 0; for (si < stages.len) { + rewritefile(dirtargettest, dirtargettestexcluded); let ctrace: str = strings.concat(root, "/testonly-c-", tags[si]); let atrace: str = strings.concat(root, "/testonly-a-", tags[si]); let ltrace: str = strings.concat(root, "/testonly-l-", tags[si]); writefile(ctrace, ""); writefile(atrace, ""); writefile(ltrace, ""); append(tracecs, ctrace); append(traceas, atrace); append(tracels, ltrace); - let env: []str = alloc([], (baseenv.len + 10): u64)!; + let env: []str = alloc([], (baseenv.len + 12): u64)!; let ei: i32 = 0; for (ei < baseenv.len) { if (!strings.hasprefix(baseenv[ei], "WW_W6C=") @@ -1580,6 +1626,10 @@ fn cwdwritedata(dir: str, label: str) void = { append(env, strings.concat("WW_TESTONLY_REAL_C=", driver(compilers[si]))); append(env, strings.concat("WW_TESTONLY_REAL_A=", driver(assemblers[si]))); append(env, strings.concat("WW_TESTONLY_REAL_L=", driver(linkers[si]))); + let failcindex: i32 = env.len; + append(env, "WW_TESTONLY_FAIL_C="); + let linkmodeindex: i32 = env.len; + append(env, "WW_TESTONLY_LINK_MODE="); append(environments, env); let out: commandout; @@ -1745,29 +1795,289 @@ fn cwdwritedata(dir: str, label: str) void = { && readfile(ctrace).len == 0 && readfile(atrace).len == 0 && readfile(ltrace).len == 0); + // Directory loading wins before raw-source suffix classification. Empty + // and malformed directories retain directory diagnostics, including the + // requested symlink spelling of a selected source. + let emptywork: str = strings.concat(root, "/empty-dir-work-", tags[si]); + let emptyout: str = strings.concat(root, "/empty-dir-output-", tags[si]); + mkdirall(emptywork); + let emptyav: []str = [driver(stages[si]), "build", "-w", emptywork, + "-I", include, "-o", emptyout, emptydir]; + runcommandenv(root, strings.concat("testonly-empty-directory-", tags[si]), + emptyav, env, (30i64 * (time.second: i64)): time.duration, &out); + expectexit(&out, 1); + let emptywant: str = strings.concat("ww: ", emptydir, + ": directory contains no WW package sources\n"); + assert(out.stdout.len == 0 && same(out.stderr, emptywant) + && !has(out.stderr, "cannot read source") && directoryisempty(emptywork) + && !os.exists(emptyout) && readfile(ctrace).len == 0 + && readfile(atrace).len == 0 && readfile(ltrace).len == 0); + let baddirwork: str = strings.concat(root, "/bad-dir-work-", tags[si]); + let baddirout: str = strings.concat(root, "/bad-dir-output-", tags[si]); + mkdirall(baddirwork); + let baddirav: []str = [driver(stages[si]), "build", "-w", baddirwork, + "-I", include, "-o", baddirout, baddirlink]; + runcommandenv(root, strings.concat("testonly-bad-directory-", tags[si]), + baddirav, env, (30i64 * (time.second: i64)): time.duration, &out); + expectexit(&out, 1); + let baddirwant: str = strings.concat(baddirlink, + "/main.ww:2:8: error: expected identifier, got ;\n"); + assert(out.stdout.len == 0 && same(out.stderr, baddirwant) + && !has(out.stderr, "cannot read source") && directoryisempty(baddirwork) + && !os.exists(baddirout) && readfile(ctrace).len == 0 + && readfile(atrace).len == 0 && readfile(ltrace).len == 0); + + // A real directory whose requested basename ends in .ww remains a + // directory request. The suffix does not make the directory a raw source. + let directdirwork: str = strings.concat(root, "/direct-dir-work-", + tags[si]); + let directdirout: str = strings.concat(root, "/direct-dir-output-", + tags[si]); + mkdirall(directdirwork); + let directdirav: []str = [driver(stages[si]), "build", "-w", + directdirwork, "-I", include, "-o", directdirout, dirsource]; + runcommandenv(root, strings.concat("testonly-direct-directory-", + tags[si]), directdirav, env, + (60i64 * (time.second: i64)): time.duration, &out); + expectexit(&out, 0); + assert(out.stdout.len == 0 && out.stderr.len == 0 + && os.exists(directdirout) && readfile(ctrace).len != 0 + && readfile(atrace).len != 0 && readfile(ltrace).len != 0); + let directdirbytes: str = strings.dup(readfile(directdirout)); + let directdirartifacts: str = strings.dup( + artifacttreesnapshot(directdirwork)); + if (si == 0) { + cdirectdirbin = strings.dup(directdirbytes); + cdirectdirartifacts = strings.dup(directdirartifacts); + } else { + assert(same(cdirectdirbin, directdirbytes) + && same(cdirectdirartifacts, directdirartifacts)); + }; + let directrun: []str = [directdirout]; + runcommand(root, strings.concat("testonly-direct-directory-run-", + tags[si]), directrun, time.second, &out); + expectexit(&out, 3); + rewritefile(ctrace, ""); rewritefile(atrace, ""); rewritefile(ltrace, ""); + // A directory target remains a directory request despite a test-looking - // requested spelling; this is deliberately the one producer-positive row. + // requested symlink spelling. Both stages reach the ordinary directory + // graph and produce byte-identical commands and semantic artifacts. let linkdirwork: str = strings.concat(root, "/link-dir-work-", tags[si]); let linkdirout: str = strings.concat(root, "/link-dir-output-", tags[si]); mkdirall(linkdirwork); let linkdirav: []str = [driver(stages[si]), "build", "-w", linkdirwork, - "-o", linkdirout, dirlink]; + "-I", include, "-o", linkdirout, dirlink]; runcommandenv(root, strings.concat("testonly-directory-link-", tags[si]), linkdirav, env, (60i64 * (time.second: i64)): time.duration, &out); + expectexit(&out, 0); + assert(out.stdout.len == 0 && out.stderr.len == 0 + && os.exists(linkdirout) && readfile(ctrace).len != 0 + && readfile(atrace).len != 0 && readfile(ltrace).len != 0); + let linkdirbytes: str = strings.dup(readfile(linkdirout)); + let linkdirartifacts: str = strings.dup( + artifacttreesnapshot(linkdirwork)); if (si == 0) { - expectexit(&out, 0); - assert(out.stdout.len == 0 && out.stderr.len == 0 - && os.exists(linkdirout) && readfile(ctrace).len != 0 - && readfile(atrace).len != 0 && readfile(ltrace).len != 0); + clinkdirbin = strings.dup(linkdirbytes); + clinkdirartifacts = strings.dup(linkdirartifacts); } else { - // The selected non-directory classifier must not absorb this - // pre-existing WWstage resolution difference into the slice. - expectexit(&out, 1); - assert(out.stdout.len == 0 - && same(out.stderr, "ww: cannot read source\n") - && !os.exists(linkdirout) && readfile(ctrace).len == 0 - && readfile(atrace).len == 0 && readfile(ltrace).len == 0); + assert(same(clinkdirbin, linkdirbytes) + && same(clinkdirartifacts, linkdirartifacts)); }; + let linkdirrun: []str = [linkdirout]; + runcommand(root, strings.concat("testonly-directory-link-run-", tags[si]), + linkdirrun, time.second, &out); + expectexit(&out, 5); + rewritefile(ctrace, ""); rewritefile(atrace, ""); rewritefile(ltrace, ""); + runcommandenv(root, strings.concat("testonly-directory-link-warm-", + tags[si]), linkdirav, env, + (60i64 * (time.second: i64)): time.duration, &out); + expectexit(&out, 0); + assert(out.stdout.len == 0 && out.stderr.len == 0 + && same(linkdirbytes, readfile(linkdirout)) + && same(clinkdirartifacts, artifacttreesnapshot(linkdirwork)) + && readfile(ctrace).len == 0 && readfile(atrace).len == 0 + && readfile(ltrace).len != 0); + rewritefile(ctrace, ""); rewritefile(atrace, ""); rewritefile(ltrace, ""); + + // Output-directory presentation comes from the canonical selected target, + // while exact-null and assembly-only disposition do not change selection. + let linkoutdir: str = strings.concat(root, "/link-output-directory-", + tags[si]); + let linkoutwork: str = strings.concat(root, "/link-output-work-", + tags[si]); + mkdirall(linkoutdir); mkdirall(linkoutwork); + writefile(strings.concat(linkoutdir, "/sentinel"), "caller-owned\n"); + let linkoutav: []str = [driver(stages[si]), "build", "-w", linkoutwork, + "-I", include, "-o", linkoutdir, dirlink]; + runcommandenv(root, strings.concat("testonly-link-output-directory-", + tags[si]), linkoutav, env, + (60i64 * (time.second: i64)): time.duration, &out); + expectexit(&out, 0); + let linkoutchild: str = strings.concat(linkoutdir, "/directory-target"); + assert(out.stdout.len == 0 && out.stderr.len == 0 + && same(readfile(strings.concat(linkoutdir, "/sentinel")), + "caller-owned\n") + && same(linkdirbytes, readfile(linkoutchild)) + && readfile(ctrace).len != 0 && readfile(atrace).len != 0 + && readfile(ltrace).len != 0); + rewritefile(ctrace, ""); rewritefile(atrace, ""); rewritefile(ltrace, ""); + let linknullwork: str = strings.concat(root, "/link-null-work-", tags[si]); + mkdirall(linknullwork); + let linknullav: []str = [driver(stages[si]), "build", "-w", linknullwork, + "-I", include, "-o", "/dev/null", dirlink]; + runcommandenv(root, strings.concat("testonly-link-null-", tags[si]), + linknullav, env, (60i64 * (time.second: i64)): time.duration, &out); + expectexit(&out, 0); + assert(out.stdout.len == 0 && out.stderr.len == 0 + && readfile(ctrace).len != 0 && readfile(atrace).len != 0 + && readfile(ltrace).len != 0 + && artifacttreesnapshot(linknullwork).len != 0); + rewritefile(ctrace, ""); rewritefile(atrace, ""); rewritefile(ltrace, ""); + let asmdirwork: str = strings.concat(root, "/link-asm-work-", tags[si]); + let asmdirstem: str = strings.concat(root, "/link-asm-output-", tags[si]); + mkdirall(asmdirwork); + let asmdirav: []str = [driver(stages[si]), "build", "-S", "-w", + asmdirwork, "-I", include, "-o", asmdirstem, dirlink]; + runcommandenv(root, strings.concat("testonly-link-assembly-", tags[si]), + asmdirav, env, (60i64 * (time.second: i64)): time.duration, &out); + expectexit(&out, 0); + assert(out.stdout.len == 0 && out.stderr.len == 0 + && !os.exists(asmdirstem) && readfile(ctrace).len != 0 + && readfile(atrace).len == 0 && readfile(ltrace).len == 0); + let asmdirsnapshot: str = strings.dup( + artifacttreesnapshot(asmdirwork)); + assert(asmdirsnapshot.len != 0); + if (si == 0) { clinkdirasm = strings.dup(asmdirsnapshot); } + else { assert(same(clinkdirasm, asmdirsnapshot)); }; + + // Without -o, the requested directory spelling owns presentation only. + // Plain -S keeps that selection but stops before public output, assembler, + // and link actions just like the explicit assembly-only form. + let defaultcaller: str = strings.concat(root, "/link-default-caller-", + tags[si]); + let defaultwork: str = strings.concat(root, "/link-default-work-", + tags[si]); + mkdirall(defaultcaller); mkdirall(defaultwork); + let defaultdirav: []str = [driver(stages[si]), "build", "-w", + defaultwork, "-I", include, dirlink]; + rewritefile(ctrace, ""); rewritefile(atrace, ""); rewritefile(ltrace, ""); + runcommandenvdir(root, strings.concat("testonly-link-default-", tags[si]), + defaultdirav, env, defaultcaller, + (60i64 * (time.second: i64)): time.duration, &out); + expectexit(&out, 0); + let defaultdirout: str = strings.concat(defaultcaller, + "/directory_test.ww"); + assert(out.stdout.len == 0 && out.stderr.len == 0 + && same(linkdirbytes, readfile(defaultdirout)) + && !os.exists(strings.concat(defaultcaller, "/directory-target")) + && same(clinkdirartifacts, artifacttreesnapshot(defaultwork)) + && readfile(ctrace).len != 0 && readfile(atrace).len != 0 + && readfile(ltrace).len != 0); + let defaultrun: []str = [defaultdirout]; + runcommand(root, strings.concat("testonly-link-default-run-", tags[si]), + defaultrun, time.second, &out); + expectexit(&out, 5); + let plainasmcaller: str = strings.concat(root, "/link-plain-asm-caller-", + tags[si]); + let plainasmwork: str = strings.concat(root, "/link-plain-asm-work-", + tags[si]); + mkdirall(plainasmcaller); mkdirall(plainasmwork); + let plainasmav: []str = [driver(stages[si]), "build", "-S", "-w", + plainasmwork, "-I", include, dirlink]; + rewritefile(ctrace, ""); rewritefile(atrace, ""); rewritefile(ltrace, ""); + runcommandenvdir(root, strings.concat("testonly-link-plain-assembly-", + tags[si]), plainasmav, env, plainasmcaller, + (60i64 * (time.second: i64)): time.duration, &out); + expectexit(&out, 0); + assert(out.stdout.len == 0 && out.stderr.len == 0 + && !os.exists(strings.concat(plainasmcaller, "/directory_test.ww")) + && readfile(ctrace).len != 0 && readfile(atrace).len == 0 + && readfile(ltrace).len == 0); + let plainasmsnapshot: str = strings.dup( + artifacttreesnapshot(plainasmwork)); + assert(same(asmdirsnapshot, plainasmsnapshot)); + if (si == 0) { clinkdirplainasm = strings.dup(plainasmsnapshot); } + else { assert(same(clinkdirplainasm, plainasmsnapshot)); }; + + // A failed invalidation leaves the prior command and committed work + // byte-for-byte intact. A later successful invalidation and restoration + // reuse the same directory identity rather than a raw-source identity. + let dirtargetmain: str = strings.concat(dirtarget, "/main.ww"); + let dirtargetoriginal: str = strings.concat( + "package main;\nimport domain.value;\n", + "fn main() i32 = { return value.number(); };\n"); + let dirtargetchanged: str = strings.concat( + "package main;\nimport domain.value;\n", + "fn main() i32 = { return value.number() + 2; };\n"); + let linkdirworkbefore: str = strings.dup(treesnapshot(linkdirwork)); + rewritefile(dirtargetmain, dirtargetchanged); + env[failcindex] = "WW_TESTONLY_FAIL_C=1"; + rewritefile(ctrace, ""); rewritefile(atrace, ""); rewritefile(ltrace, ""); + runcommandenv(root, strings.concat("testonly-link-rollback-", tags[si]), + linkdirav, env, (60i64 * (time.second: i64)): time.duration, &out); + expectexit(&out, 1); + assert(out.stdout.len == 0 + && has(out.stderr, "injected directory compiler failure\n") + && same(linkdirbytes, readfile(linkdirout)) + && same(linkdirworkbefore, treesnapshot(linkdirwork)) + && readfile(ctrace).len != 0 && readfile(atrace).len == 0 + && readfile(ltrace).len == 0 + && !os.exists(strings.concat(linkdirout, ".new")) + && !directoryhasnew(linkdirwork) + && !directoryhasfragment(linkdirwork, ".wwtxn.")); + if (si == 0) { compilerfaildiag = strings.dup(out.stderr); } + else { assert(same(compilerfaildiag, out.stderr)); }; + env[failcindex] = "WW_TESTONLY_FAIL_C="; + env[linkmodeindex] = "WW_TESTONLY_LINK_MODE=signal"; + rewritefile(ctrace, ""); rewritefile(atrace, ""); rewritefile(ltrace, ""); + runcommandenv(root, strings.concat("testonly-link-interrupted-", tags[si]), + linkdirav, env, (60i64 * (time.second: i64)): time.duration, &out); + expectexit(&out, 1); + assert(out.stdout.len == 0 && out.stderr.len != 0 + && has(out.stderr, "ww: w6l failed\n") + && same(linkdirbytes, readfile(linkdirout)) + && same(linkdirworkbefore, treesnapshot(linkdirwork)) + && readfile(ctrace).len != 0 && readfile(atrace).len != 0 + && readfile(ltrace).len != 0 + && !os.exists(strings.concat(linkdirout, ".new")) + && !directoryhasnew(linkdirwork) + && !directoryhasfragment(linkdirwork, ".wwtxn.")); + if (si == 0) { linkersignaldiag = strings.dup(out.stderr); } + else { assert(same(linkersignaldiag, out.stderr)); }; + env[linkmodeindex] = "WW_TESTONLY_LINK_MODE="; + rewritefile(ctrace, ""); rewritefile(atrace, ""); rewritefile(ltrace, ""); + runcommandenv(root, strings.concat("testonly-link-invalidated-", tags[si]), + linkdirav, env, (60i64 * (time.second: i64)): time.duration, &out); + expectexit(&out, 0); + let changedbytes: str = strings.dup(readfile(linkdirout)); + let changedartifacts: str = strings.dup( + artifacttreesnapshot(linkdirwork)); + assert(out.stdout.len == 0 && out.stderr.len == 0 + && !same(linkdirbytes, changedbytes) + && readfile(ctrace).len != 0 && readfile(atrace).len != 0 + && readfile(ltrace).len != 0); + if (si == 0) { + clinkdirchanged = strings.dup(changedbytes); + clinkdirchangedartifacts = strings.dup(changedartifacts); + } else { + assert(same(clinkdirchanged, changedbytes) + && same(clinkdirchangedartifacts, changedartifacts)); + }; + runcommand(root, strings.concat("testonly-link-invalidated-run-", tags[si]), + linkdirrun, time.second, &out); + expectexit(&out, 7); + rewritefile(dirtargetmain, dirtargetoriginal); + rewritefile(ctrace, ""); rewritefile(atrace, ""); rewritefile(ltrace, ""); + runcommandenv(root, strings.concat("testonly-link-restored-", tags[si]), + linkdirav, env, (60i64 * (time.second: i64)): time.duration, &out); + expectexit(&out, 0); + assert(out.stdout.len == 0 && out.stderr.len == 0 + && same(linkdirbytes, readfile(linkdirout)) + && same(clinkdirartifacts, artifacttreesnapshot(linkdirwork)) + && readfile(ctrace).len != 0 && readfile(atrace).len != 0 + && readfile(ltrace).len != 0 + && !directoryhasnew(linkdirwork) + && !directoryhasfragment(linkdirwork, ".wwtxn.")); rewritefile(ctrace, ""); rewritefile(atrace, ""); rewritefile(ltrace, ""); // A valid retained raw source seeds state. Renaming only its requested @@ -1819,8 +2129,99 @@ fn cwdwritedata(dir: str, label: str) void = { && same(cwarmartifacts, artifacttreesnapshot(warmwork))); }; - // Test and run keep their existing raw-source semantics, while a visible - // non-test raw build and a multi-operand error do not enter this slice. + // Test, test -c, and test -S already route this symlink as a directory. + // Temporarily make its otherwise build-excluded test source valid and pin + // exact stage parity without changing the negative production-build rows. + rewritefile(dirtargettest, dirtargettestselected); + rewritefile(ctrace, ""); rewritefile(atrace, ""); rewritefile(ltrace, ""); + let directorytestwork: str = strings.concat(root, "/directory-test-work-", + tags[si]); + mkdirall(directorytestwork); + let directorytestav: []str = [driver(stages[si]), "test", "-w", + directorytestwork, "-I", include, dirlink]; + runcommandenv(root, strings.concat("testonly-directory-test-", tags[si]), + directorytestav, env, + (60i64 * (time.second: i64)): time.duration, &out); + expectexit(&out, 0); + assert(out.stderr.len == 0 + && has(out.stdout, "directory_selected ... ok\n") + && readfile(ctrace).len != 0 && readfile(atrace).len != 0 + && readfile(ltrace).len != 0); + let directorytestrunstate: str = strings.dup( + artifacttreesnapshot(directorytestwork)); + if (si == 0) { + directorytestrunout = strings.dup(out.stdout); + directorytestrunerr = strings.dup(out.stderr); + directorytestrunartifacts = strings.dup(directorytestrunstate); + } else { + assert(same(directorytestrunout, out.stdout) + && same(directorytestrunerr, out.stderr) + && same(directorytestrunartifacts, directorytestrunstate)); + }; + let directorycompiled: str = strings.concat(root, + "/directory-test-compiled-", tags[si]); + let directorycompilework: str = strings.concat(root, + "/directory-test-compile-work-", tags[si]); + mkdirall(directorycompilework); + let directorycompileav: []str = [driver(stages[si]), "test", "-c", "-w", + directorycompilework, "-I", include, "-o", directorycompiled, dirlink]; + runcommandenv(root, strings.concat("testonly-directory-test-compile-", + tags[si]), directorycompileav, env, + (60i64 * (time.second: i64)): time.duration, &out); + expectexit(&out, 0); + assert(out.stdout.len == 0 && out.stderr.len == 0 + && os.exists(directorycompiled)); + let directorycompiledbytes: str = strings.dup(readfile(directorycompiled)); + let directorycompiledstate: str = strings.dup( + artifacttreesnapshot(directorycompilework)); + if (si == 0) { + directorytestbin = strings.dup(directorycompiledbytes); + directorytestcompileartifacts = strings.dup(directorycompiledstate); + } else { + assert(same(directorytestbin, directorycompiledbytes) + && same(directorytestcompileartifacts, directorycompiledstate)); + }; + let directorytestasmwork: str = strings.concat(root, + "/directory-test-assembly-work-", tags[si]); + let directorytestasmout: str = strings.concat(root, + "/directory-test-assembly-output-", tags[si]); + mkdirall(directorytestasmwork); + let directorytestasmav: []str = [driver(stages[si]), "test", "-S", "-w", + directorytestasmwork, "-I", include, "-o", directorytestasmout, dirlink]; + rewritefile(ctrace, ""); rewritefile(atrace, ""); rewritefile(ltrace, ""); + runcommandenv(root, strings.concat("testonly-directory-test-assembly-", + tags[si]), directorytestasmav, env, + (60i64 * (time.second: i64)): time.duration, &out); + expectexit(&out, 2); + assert(out.stdout.len == 0 + && same(out.stderr, "ww test: -S needs a single test file\n") + && !os.exists(directorytestasmout) + && directoryisempty(directorytestasmwork) + && readfile(ctrace).len == 0 && readfile(atrace).len == 0 + && readfile(ltrace).len == 0); + if (si == 0) { directorytestasm = strings.dup(out.stderr); } + else { assert(same(directorytestasm, out.stderr)); }; + + // Run was not changed by the build-only dispatcher fix. Cstage follows + // the selected directory and executes it; WWstage retains its exact + // pre-existing raw-source failure for the same requested symlink. + let directoryrunav: []str = [driver(stages[si]), "run", "-I", include, + dirlink]; + runcommandenv(root, strings.concat("testonly-directory-run-", tags[si]), + directoryrunav, env, + (60i64 * (time.second: i64)): time.duration, &out); + if (si == 0) { + expectexit(&out, 5); + assert(out.stdout.len == 0 && out.stderr.len == 0); + } else { + expectexit(&out, 1); + assert(out.stdout.len == 0 + && same(out.stderr, "ww: cannot read source\n")); + }; + rewritefile(dirtargettest, dirtargettestexcluded); + + // Raw-source test and run retain their separate existing semantics, while + // a visible non-test raw build and multi-operand error stay out of this slice. rewritefile(ctrace, ""); rewritefile(atrace, ""); rewritefile(ltrace, ""); let testwork: str = strings.concat(root, "/route-work-", tags[si]); mkdirall(testwork); @@ -1891,9 +2292,76 @@ fn cwdwritedata(dir: str, label: str) void = { assert(same(commentbad[0], commentbad[1])); assert(same(multibad[0], multibad[1])); + // Simultaneous selected-directory requests use isolated work and output + // transactions while retaining exact cross-stage product and artifact bytes. + let ti: i32 = 0; + for (ti < stages.len) { + rewritefile(tracecs[ti], ""); + rewritefile(traceas[ti], ""); + rewritefile(tracels[ti], ""); + ti += 1; + }; + let pdcwork: str = strings.concat(root, "/parallel-directory-c-work"); + let pdwwork: str = strings.concat(root, "/parallel-directory-ww-work"); + let pdcout: str = strings.concat(root, "/parallel-directory-c-output"); + let pdwout: str = strings.concat(root, "/parallel-directory-ww-output"); + mkdirall(pdcwork); mkdirall(pdwwork); + let pdcav: []str = [driver("ww"), "build", "-w", pdcwork, "-I", include, + "-o", pdcout, dirlink]; + let pdwav: []str = [driver("ww_ww"), "build", "-w", pdwwork, "-I", + include, "-o", pdwout, dirlink]; + let pdc: exec.command; + pdc.path = pdcav[0]; pdc.argv = pdcav; pdc.env = environments[0]; + pdc.dir = repo(); + pdc.stdoutpath = strings.concat(root, "/parallel-directory-c.stdout"); + pdc.stderrpath = strings.concat(root, "/parallel-directory-c.stderr"); + pdc.deadline = time.add(time.now(time.clock.monotonic), + (60i64 * (time.second: i64)): time.duration); + pdc.grace = (100i64 * (time.millisecond: i64)): time.duration; + let pdw: exec.command; + pdw.path = pdwav[0]; pdw.argv = pdwav; pdw.env = environments[1]; + pdw.dir = repo(); + pdw.stdoutpath = strings.concat(root, "/parallel-directory-ww.stdout"); + pdw.stderrpath = strings.concat(root, "/parallel-directory-ww.stderr"); + pdw.deadline = time.add(time.now(time.clock.monotonic), + (60i64 * (time.second: i64)): time.duration); + pdw.grace = (100i64 * (time.millisecond: i64)): time.duration; + let pdcp: exec.process; + let pdwp: exec.process; + exec.start(&pdcp, &pdc); exec.start(&pdwp, &pdw); + let pdcdone: bool = false; + let pdwdone: bool = false; + for (!pdcdone || !pdwdone) { + if (!pdcdone) { pdcdone = exec.poll(&pdcp); }; + if (!pdwdone) { pdwdone = exec.poll(&pdwp); }; + if (!pdcdone || !pdwdone) { + time.sleep(time.millisecond, time.clock.monotonic); + }; + }; + assert(pdcp.result.errno == 0 && pdcp.result.cleanuperrno == 0 + && pdcp.result.termination == exec.termination.EXIT + && pdcp.result.code == 0 && pdwp.result.errno == 0 + && pdwp.result.cleanuperrno == 0 + && pdwp.result.termination == exec.termination.EXIT + && pdwp.result.code == 0); + assert(readfile(pdc.stdoutpath).len == 0 + && readfile(pdc.stderrpath).len == 0 + && readfile(pdw.stdoutpath).len == 0 + && readfile(pdw.stderrpath).len == 0 + && same(clinkdirbin, readfile(pdcout)) + && same(clinkdirbin, readfile(pdwout)) + && same(clinkdirartifacts, artifacttreesnapshot(pdcwork)) + && same(clinkdirartifacts, artifacttreesnapshot(pdwwork)) + && readfile(tracecs[0]).len != 0 && readfile(traceas[0]).len != 0 + && readfile(tracels[0]).len != 0 && readfile(tracecs[1]).len != 0 + && readfile(traceas[1]).len != 0 && readfile(tracels[1]).len != 0 + && !directoryhasnew(pdcwork) && !directoryhasnew(pdwwork) + && !directoryhasfragment(pdcwork, ".wwtxn.") + && !directoryhasfragment(pdwwork, ".wwtxn.")); + // Two simultaneous omitted requests have no work/output collision and no // producer invocation in either stage. - let ti: i32 = 0; + ti = 0; for (ti < stages.len) { rewritefile(tracecs[ti], ""); rewritefile(traceas[ti], "");