ww build: honor output directories
This commit is contained in:
@@ -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 459–471](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 473–478,
|
||||
its output branch classifies an existing directory through
|
||||
`os.Stat`, or a spelling ending in `/` or the host path separator, at
|
||||
[lines 508–518](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 519–535](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 537–548](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 1727–1769](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 1975–2000](https://github.com/golang/go/blob/c19862e5f8415b4f24b189d065ed739517c548ba/src/cmd/go/internal/work/exec.go#L1975-L2000),
|
||||
[`cmd/go/internal/work/shell.go`, lines 283–301](https://github.com/golang/go/blob/c19862e5f8415b4f24b189d065ed739517c548ba/src/cmd/go/internal/work/shell.go#L283-L301)).
|
||||
- Official
|
||||
[`build_output.txt`, lines 10–29 and 41–45](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 1–16](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
|
||||
470–471 load and check all selected packages before any output handling at
|
||||
lines 473–548. 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.
|
||||
|
||||
29
docs/spec.md
29
docs/spec.md
@@ -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`
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user