build: route .ww directories as packages

This commit is contained in:
2026-08-22 23:39:06 +09:00
parent 2e5451a601
commit ca0e533c13
5 changed files with 683 additions and 30 deletions

View File

@@ -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:633638`.
`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:29032932`, especially 29112918). Local
literals that do not enter that route are matched and directory-loaded by
`cmd/go/internal/modload/load.go:251307,526597` and
`cmd/go/internal/search/search.go:276293`. `runBuild` uses that loader at
`cmd/go/internal/work/build.go:459477`.
- **behavior directly implemented or asserted by pinned Go** — official
`cmd/go/testdata/script/list_ambiguous_path.txt:115,2936` 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:19,1117` directly asserts
that `dir.go -> dir` is not a source entry; `go/build/build.go:886900`
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:95112`
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:32743281`.
- **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.

View File

@@ -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

View File

@@ -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

View File

@@ -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;

View File

@@ -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], "");