ww build: honor output directories

This commit is contained in:
2026-08-21 02:32:56 +09:00
parent 71a9da2058
commit ce91f6662c
7 changed files with 1768 additions and 107 deletions

View File

@@ -6533,6 +6533,233 @@ runtime cwd/environment/stdin. Build runtime behavior is inapplicable, and
caller-output rollback, occupied caller stages, and output permissions are
inapplicable to the exact discard branch because it creates no public inode.
### 11.30 Implemented single-root build output directories
An explicit `ww build -o OUT` now treats `OUT` as a directory when ordinary
`stat` reports an existing directory or the spelling ends in `/`. The rule is
independent of package count. A selected command directory publishes
`OUT/<requested-import-leaf>` (falling back to the selected local directory
leaf when no contextual identity exists); a raw command-line source publishes
`OUT/<source-basename-without-.ww>`. A missing trailing-slash hierarchy is
created from `0777`, filtered by caller umask, through the existing checked
directory ledger. All roots load first, but independently selected non-main
roots are omitted from the directory branch's action list. A selection
containing no command—including a raw non-main root—rejects as
`ww: no main packages to build` without running a producer or changing the
output directory. After a lone command's default basename is synthesized, an
existing directory at that
basename instead rejects as
`ww: build output "<name>" already exists and is a directory`; a non-main
package has no default public output and is unaffected.
#### Pinned Go evidence and fact classification
The sole authority is official Go 1.26.5 at commit
`c19862e5f8415b4f24b189d065ed739517c548ba`:
- `runBuild` completes package loading and package-error checking before output
handling at
[`cmd/go/internal/work/build.go`, lines 459471](https://github.com/golang/go/blob/c19862e5f8415b4f24b189d065ed739517c548ba/src/cmd/go/internal/work/build.go#L459-L471).
After synthesizing a lone `main` package's default output at lines 473478,
its output branch classifies an existing directory through
`os.Stat`, or a spelling ending in `/` or the host path separator, at
[lines 508518](https://github.com/golang/go/blob/c19862e5f8415b4f24b189d065ed739517c548ba/src/cmd/go/internal/work/build.go#L508-L518).
It then creates install actions only for packages named `main`, targets each
at the output directory joined with `DefaultExecName`, rejects an empty
command action list as `no main packages to build`, and executes that graph
at
[lines 519535](https://github.com/golang/go/blob/c19862e5f8415b4f24b189d065ed739517c548ba/src/cmd/go/internal/work/build.go#L519-L535).
The non-directory single-output branch is separate at
[lines 537548](https://github.com/golang/go/blob/c19862e5f8415b4f24b189d065ed739517c548ba/src/cmd/go/internal/work/build.go#L537-L548).
- `DefaultExecName` uses the final import-path element for a directory package
and the source basename for command-line files
([`cmd/go/internal/load/pkg.go`, lines 17271769](https://github.com/golang/go/blob/c19862e5f8415b4f24b189d065ed739517c548ba/src/cmd/go/internal/load/pkg.go#L1727-L1769)).
- `BuildInstallFunc` creates the target parent before installation, and
`Shell.Mkdir` implements that operation as `os.MkdirAll(dir, 0777)`
([`cmd/go/internal/work/exec.go`, lines 19752000](https://github.com/golang/go/blob/c19862e5f8415b4f24b189d065ed739517c548ba/src/cmd/go/internal/work/exec.go#L1975-L2000),
[`cmd/go/internal/work/shell.go`, lines 283301](https://github.com/golang/go/blob/c19862e5f8415b4f24b189d065ed739517c548ba/src/cmd/go/internal/work/shell.go#L283-L301)).
- Official
[`build_output.txt`, lines 1029 and 4145](https://github.com/golang/go/blob/c19862e5f8415b4f24b189d065ed739517c548ba/src/cmd/go/testdata/script/build_output.txt#L10-L45)
asserts a raw source's default basename, a missing trailing-slash directory,
and an existing directory destination.
- Official
[`build_multi_main.txt`, lines 116](https://github.com/golang/go/blob/c19862e5f8415b4f24b189d065ed739517c548ba/src/cmd/go/testdata/script/build_multi_main.txt#L1-L16)
asserts command fan-out beneath one directory, no-main rejection, the
implicit-default-existing-directory error, and raw command-line-source
placement beneath an existing output directory.
Load-before-output ordering, `stat`/separator classification, command-only
installation, import-leaf naming, destination joining, no-main rejection, and
the `0777` parent-creation request are directly implemented by the pinned
source. Raw-source basename and existing/missing directory behavior are
directly asserted by official testdata. The resulting parent mode after umask
is derived from the pinned `MkdirAll` call. Application to
exactly one directory package is derived from `runBuild`: the directory branch
tests output form, not package cardinality, while the cardinality check exists
only in the later non-directory branch. These facts apply to WW's local
manifest-free command and raw-source products without importing Go's module,
cache, registry, distribution, or network behavior. The installed host Go
version is not authority.
#### Direct pre-fix measurements
Fresh public Cstage (`out/bin/ww`) and WWstage (`out/bin/ww_ww`) probes measured
the same divergence before production edits:
- one command directory plus an existing output directory spelled without `/`
returned 0, renamed the caller's directory to a PID-bearing transaction
backup, installed a 4,268-byte ELF executable at the directory pathname, and
diagnosed failure to remove the nonempty backup;
- the same existing directory spelled with `/` returned 1 with
`ww: cannot preserve transaction destination .../`;
- a missing nested spelling ending in `/` returned 1 while trying to acquire
`<OUT>/.sepwork` before the hierarchy existed;
- a raw `main.ww` plus an existing trailing-slash destination failed through
the same transaction-destination path instead of producing `OUT/main`; and
- a non-main directory plus an existing directory spelled without `/` returned
0, replaced the directory pathname by an archive, wrote its `.wwi` beside
that pathname, and stranded the former directory as a transaction backup;
and
- for both a command directory and a raw command-line source, an existing
directory at the synthesized default basename was renamed to a transaction
backup and replaced by the executable; both stages returned 0 and diagnosed
inability to remove the deliberately nonempty backup.
Completion review measured four additional stage-identical pre-completion
behaviors before their production edits. A mixed command/non-main request
compiled the independent non-main root in both stages; a raw non-main root
returned 0 and published an archive plus `.wwi`; a contextual `alias` symlink
to physical directory `physical` published `OUT/physical`; and an exactly
arranged umask `000` produced newly created output parents with mode `0700`.
Pinned Go instead loads then omits the independent non-main action, rejects the
raw no-command selection, uses the requested import leaf, and requests parent
mode `0777`. The preserved traces and stat results are in the session evidence
ledger.
Final review then measured four stage-identical load-precedence leaks before
the completion edit. A non-main root with a missing import reported only
`no main packages`; a command with a missing import and an overlong derived
directory destination reported only the path error; two colliding command
basenames, one with a missing import, reported only the duplicate-destination
error; and a recursive command with a missing import plus an implicit default
directory collision reported only the collision. Pinned `runBuild` lines
470471 load and check all selected packages before any output handling at
lines 473548. Missing-package diagnostics therefore precede no-main,
derived-path, duplicate-destination, and implicit-default checks. Extending
that boundary to WW's transactional duplicate guard is derived from the pinned
ordering because the guard is WW-local output preflight. The exact probes and
outputs are preserved in the evidence ledger.
Those are directly measured WW facts. Both drivers interpreted every non-null
single-root `-o` as one file and object stem. The compiler, assembler, linker,
archive writer, and shared package coordinator were not the cause.
#### Ownership, actions, publication, and identity
`internal/wwpackage.packagecommand` remains the shared package-request output
owner. It classifies output directories, discovers and groups the complete
selection, and passes every selected root plus output-preflight metadata into
the shared separate-build executor. Only after that executor has loaded all
packages and imports does it retain command roots, derive their complete action
closure, and evaluate no-main, path-length, duplicate-destination, and
implicit-default checks. A separate presentation field carries the requested
import leaf (or local-path fallback) through collision preflight and
publication; canonical physical directory metadata remains loader metadata.
The coordinator commits the retained products through one request transaction.
The dispatch part of the gap belonged to the early single-root compatibility
choice in `cmd/ww.do_build` and `selfhost/cmd/ww.dobuild`; completion review
also closed the coordinator's independent-non-main action,
physical-leaf-presentation, and load-precedence leaks.
After ordinary argument parsing and root resolution, both drivers now apply the
same exact Unix classification. A directory root with a directory output enters
the existing package coordinator. An explicit logical root carries its
unchanged logical root identity while the corresponding argument is replaced
by the already-resolved loader route; a default invocation inserts exactly one
`.`; a literal directory remains literal. Thus the shared loader, grouping,
graph, action, output-preflight, and publication rules operate exactly as they
do for a larger request.
The raw-file compatibility route remains driver-owned. It derives the joined
command path, uses that path as the existing direct action's product and stem,
and passes the classified directory to the shared transaction's checked
creation ledger. A bounds failure is carried as preflight metadata so source
and import errors, and raw no-main rejection, retain pinned load-first
precedence before the path diagnostic. It does not manufacture a package
request or change raw-source graph identity. `stat` follows a symlinked output
directory; lexical publication remains beneath the requested symlink spelling.
Exact `/dev/null` is classified first by the completed discard rule and never
enters this directory branch.
For an implicit default, the drivers pass the existing-directory collision as
preflight metadata to the common separate-build executor. The executor waits
until package/import loading, contextual checks, and graph-cycle validation
have established the root action kind. It rejects a command before scratch,
workdir, tool, stage, or destination acquisition, but lets a non-main package
perform its unchanged no-public-output build. This keeps raw and directory
compatibility routes on the same diagnostic-precedence rule without deriving
kind from a path, filename, declared-name guess, or driver-side source scan.
Loading and source/import rejection precede every output-derived rejection,
including no-main, derived-path length, duplicate destination, and implicit
default collision; all precede output creation. A selection with no command
rejects after full package/import loading and graph validation but before
compiler, assembler, linker, or directory mutation. In a mixed request,
independently selected non-main roots have no action; a non-main package
reachable as a command dependency still performs its ordinary semantic action.
Repeated exact roots retain canonical graph/action deduplication. Successful
commands compile, assemble, archive, and link normally; `-S -w` remains
action-only and publishes no command. Directory form still selects only
commands and therefore retains no-main rejection, but destination length,
duplicate publication names, implicit destination collision, and output-parent
creation belong to the install action that `-S` never reaches. They are
inapplicable to that assembly-only request. An explicit external workdir
prevents an adjacent `<command>.sepwork`; without `-w`, that established WW
scratch tree remains an ordinary retained build artifact beneath the output
directory.
Existing destination contents and symlink targets survive successful
publication. Compiler failure, linker failure, or linker signal preserves the
prior command and every committed persistent artifact, removes `.new` and
transaction stages, and rolls back only directory prefixes created for the
failed request. Every caller-output prefix is requested as `0777` and filtered
once by the caller umask; persistent/private work directories keep their
separate modes. The raw route has the same missing-directory rollback through
the shared creation ledger. Independent Cstage and WWstage processes use
disjoint output, work, stage, and process ownership and may complete
concurrently. `ww build` itself has no runtime action; direct execution of the
published command verifies its ordinary program exit result, while runtime
failure/timeout policy remains owned by `ww run` and `ww test`.
This is output disposition and dispatch only. Dotted package/import identity,
declared package name, physical source directory metadata, file-local import
bindings, graph edges, action and storage keys, symbols, `.wwi`, compiler,
assembler, archive and linker semantic inputs, artifact bytes, invalidation,
and public-file output-mode formula are unchanged. Correcting caller-output
parent creation metadata does not enter any semantic identity. There is no
persisted-byte contract change: build workdir format remains `18`, test
workdir format remains `19`, and semantic storage remains `3`.
The WW-native owner `single_root_build_output_directory` covers literal,
logical, default-dot, symlinked, raw-file, explicit existing/missing, and
implicit-default existing-directory forms in both stages; basename selection
and directory-content preservation; logical alias versus physical-leaf
separation; command rejection, raw no-main rejection, non-main no-output
behavior, long raw-output and load-error precedence; mixed command-only and
repeated roots; skipped-root import rejection; and no-main, directory-derived
path, duplicate-destination, and recursive implicit-collision precedence with
zero tool activity; exact `0777` missing-parent creation under umask `000`;
the already-aligned retained-test control; `-S -w` command selection with no
install-only preflight or output-directory creation; cold, warm,
and invalidated persistence; exact compiler/assembler/linker action traces;
successful program exit results; compiler and linker failure; linker signal;
prior-state and newly-created-directory rollback; `.new` and transaction
cleanup; concurrent stage isolation; executable and semantic-artifact bytes;
and exact diagnostic parity. Existing owners continue to cover public-file
output umasks, occupied stages, generalized multi-product transactions, test
runtime failure and timeout, null discard, and broader package/import graph
matrices.
## 12. Candidate architectures and hard-gate decision
Five candidates were developed as coherent systems, not as feature bins.

View File

@@ -310,6 +310,35 @@ ImportPath = ident { "." ident } .
`fn main`; path and directory spelling do not classify commands. An ordinary
import of a package declared `main` is rejected, except for the toolchain's
colocated external-test wiring.
- For `ww build`, an explicit `-o` names an output directory when ordinary
`stat` reports an existing directory (following symlinks) or its spelling
ends in `/`. This classification is independent of whether one or many
package roots were requested. Each selected command is published beneath
that directory using the final component of its requested contextual import
name, with the selected local directory leaf as the fallback when no such
identity exists. Independently selected non-main roots load but do not enter
the action list; non-main command dependencies retain ordinary actions, and
a selection containing no command rejects before tools or output creation.
A raw command-line `.ww` source uses its source basename without `.ww`, and
a raw non-main source is the same no-command rejection. Missing
trailing-slash hierarchies are created transactionally from `0777`, filtered
by the caller's umask.
Loading and package/import diagnostics precede no-main, derived-path,
duplicate-destination, implicit-default, and other output preflight; all of
those checks precede creation. Producer failure or interruption preserves
existing directory contents and removes only request-created prefixes and
stages. A non-directory output retains the single-product file/archive rule.
If a lone
command's synthesized default basename already names a directory, loading
and graph validation complete and the build rejects before tools without
changing that directory; a non-main package synthesizes no default public
output. Output paths and directory metadata never become package, import,
graph, action,
symbol, artifact, `.wwi`, or persistence identity.
Assembly-only `-S` retains the directory form's command-action selection and
no-main rejection, but it reaches no install action: destination length,
duplicate publication names, implicit destination collision, and output
parent creation are therefore inapplicable.
- A newly published `ww build` command is created with permission `0777`
filtered by the invoking process's umask. A newly published non-command
archive, and the adjacent WW interface required to consume it, use `0666`

View File

@@ -234,6 +234,28 @@ package with no selected test source validates production
but publishes nothing and does not create an otherwise unneeded output
directory. Successful `-c` output is silent apart from no-test reporting.
The package suite also pins the adjacent `ww build` output-directory branch.
After normal loading, an existing directory (including a symlink to one) or an
explicit spelling ending in `/` receives each command under its requested
import-leaf name (or local directory-leaf fallback) even for exactly one
selected root; a raw `.ww` command uses its source basename. A missing
trailing-slash hierarchy is created from `0777`, filtered by umask, by the
request transaction. A directory or raw no-main-only selection and an import
failure run no tools and do not create output. Independently selected non-main
siblings in a mixed request load but have no action; command dependencies still
build. Package/import rejection precedes no-main, derived destination length,
duplicate command basename, and implicit default-directory preflight. If the
synthesized default command basename already exists as a directory,
load/graph validation precedes a zero-tool rejection and preserves that
directory; a non-main package has no corresponding default output.
Persistent work reuse, invalidation, failures, interruption, and
concurrent drivers retain the same action and rollback rules; output form does
not enter package/import/action identity. This build branch starts no runtime
process, while direct execution of its published binary remains an artifact
check rather than part of `ww build`. Assembly-only `-S` still selects command
actions and rejects a no-command directory request, but it performs no
install-only destination validation or output-directory creation.
The retained executable is byte-identical to the temporary runnable and has
executable mode `0777` filtered by the caller's umask, but it is never the path
executed by the coordinator. Publication participates in the driver's one
@@ -312,6 +334,24 @@ precede those coordinator-owned source-validation parses.
Separate compilation is the only driver build path; no compatibility mode
switch remains.
`test/package/package_test.ww` owns the focused dual-stage
`single_root_build_output_directory` observer. It covers existing and missing
directory spellings; trailing and non-trailing forms; literal, logical,
default-dot, symlink, raw-file, and implicit-default routes; build/test
adjacency; mixed and repeated roots; command-only publication, no-main and
implicit-collision rejection, non-main default behavior; import
diagnostic precedence over no-main, derived-path, duplicate-destination, and
recursive implicit-collision checks; `-S -w` command selection with long,
duplicate, implicit, and missing-directory publication preflight omitted;
cold, warm, and invalidated persistence;
compiler, assembler, and linker action traces; compiler/linker failure and
linker signal; prior-state and directory-creation rollback; concurrent
isolation; executable modes and runtime exit values; `.new`/transaction
cleanup; and Cstage/WWstage diagnostic, binary, assembly, and persistent
semantic-artifact byte parity. General output permissions, occupied stages,
multi-product transactions, null discard, and test runtime timeout remain with
their existing observers.
`test/package/package_test.ww` also owns the dual-stage
`declared_name_identity_and_file_import_scope` and
`explicit_import_alias_binding_modes` observers. They generate temporary