ww: preserve original environment for directory tests

This commit is contained in:
2026-08-21 12:57:55 +09:00
parent bcc3aa9336
commit 8a5fc8977f
5 changed files with 424 additions and 67 deletions

View File

@@ -5850,25 +5850,24 @@ coordinator publication paths are absolute, so the child directory cannot
reinterpret them. No runtime coordinator-global `chdir` was added; its cwd and
`PWD` remain unchanged.
Each started product also receives a newly allocated run environment. It
retains inherited entries in order except exact `TMPDIR=`, `LC_ALL=`, uppercase
`PATH=`, and uppercase `PWD=` entries, then appends `LC_ALL=C`, the
product-local absolute `TMPDIR`, the selected toolchain `PATH`, and
`PWD=<pkggroup.dir>`. Removing prior `PWD` and `PATH` entries reproduces Go's
observable last-wins result because WW's executor intentionally preserves
duplicates and WW `os.getenv` returns the first one. Case-distinct and malformed
entries remain untouched. The product observes exactly one uppercase `PWD`, at
the appended position, and one effective uppercase `PATH`; section 11.32 owns
the latter rule.
Each started product also receives a newly allocated run environment. Section
11.36 supersedes this slice's former test-process locale and temporary-directory
policy: the vector is now a Go-like original-environment snapshot. It keeps the
first occurrence of each normal case-sensitive key, omits later normal
duplicates and raw empty entries, preserves nonempty malformed entries, excludes
inherited uppercase `PATH` and `PWD`, and then appends the selected toolchain
`PATH` and `PWD=<pkggroup.dir>`. Caller `LC_ALL` and `TMPDIR` therefore reach the
user test; build-plan tools retain their separate pinned values.
The vector and its generated strings are product-local, dynamically sized,
and published only after every checked allocation succeeds. Partial failure
frees only initialized owned storage and never frees borrowed inherited
strings. `exec.start` synchronously deep-copies the command before returning,
after which the coordinator frees its run vector, generated `TMPDIR`, generated
`PATH`, generated `PWD`, generated `-package` argument, and argv vector.
Concurrent children therefore hold independent fork snapshots; no shared
environment vector or process-global state is mutated.
The vector, normalization table, and generated strings are product-local,
dynamically sized, and published only after every checked allocation succeeds.
Partial failure frees only initialized owned storage and never frees borrowed
inherited strings. The normalization table is gone before launch. `exec.start`
synchronously deep-copies the command before returning, after which the
coordinator frees its run vector, generated `PATH`, generated `PWD`, generated
`-package` argument, and argv vector. Concurrent children therefore hold
independent fork snapshots; no shared environment vector or process-global
state is mutated.
All dependency initialization occurs inside that product process. A production
or test-only dependency reached by package `p` sees `p`'s directory. If the
@@ -5931,8 +5930,8 @@ production and test-only dependency initialization; recompiled external
self-import; direct/recursive/redundant/absolute/symlink roots; reversed roots
and creation order; `-j 1`/parallel execution; filters/list/no-match; failure
and timeout; no-test and build paths; `-c`, `-c -o`, running retained tests,
direct binaries, and raw single files; exact tool cwd/argv/locale/TMPDIR/PATH;
persistent data-only reuse and
direct binaries, and raw single files; original test variables and exact tool
cwd/argv/locale/TMPDIR/PATH; persistent data-only reuse and
artifact/binary identity; and a deterministic post-build directory removal
where the affected product reports `ENOENT` while its sibling succeeds.
Checked command-global allocation-failure parity remains owned by
@@ -7006,11 +7005,11 @@ The environment is synthesized at the three true test-process launch owners:
`run_test_bin`, and WWstage `runsingletest` for raw single-file tests. Each
uses the selected driver directory's canonical absolute spelling. The first
effective uppercase inherited value is the suffix; absent and empty values
have no suffix; ordinary uppercase duplicates are removed. Unrelated entries,
including case-distinct and malformed names, retain their previous order and
bytes. Directory products retain their established appended locale, temporary
directory, and final `PWD`; raw tests retain caller cwd, `PWD`, stdin, and split
streams.
have no suffix; ordinary uppercase duplicates are removed. For directory
products, section 11.36 owns unrelated entries: a Go-like original-environment
snapshot retains caller locale and temporary-directory values, case-distinct
and nonempty malformed entries, and the final package `PWD`. Raw tests retain
caller cwd, `PWD`, other environment entries, stdin, and split streams.
The rule covers internal, external, and combined directory products;
dependency initialization; filters and list mode; running retained tests; and
@@ -7466,6 +7465,169 @@ large scheduling sets, failure rollback, stage parity, and artifact-byte
identity. No persisted-byte contract changed: build workdir format remains
`18`, test workdir format remains `19`, and semantic storage format remains `3`.
### 11.36 Implemented original environment for directory test processes
Every directory-owned test binary actually started by `ww test` now receives a
Go-like snapshot of the caller environment instead of the package build plan's
locale and temporary directory. On the supported Unix boundary, the snapshot
keeps the first occurrence of every normal case-sensitive `key=value`, omits
later normal duplicates and raw empty entries, and preserves nonempty malformed
entries in order. The existing selected-toolchain `PATH` and package-directory
`PWD` are then appended as the only test-command overrides. Caller `LC_ALL`,
`TMPDIR`, empty-valued variables, case-distinct keys, and arbitrary variables
therefore reach initialization and test code.
#### Pinned Go evidence and fact classification
The sole authority is official Go 1.26.5 at commit
`c19862e5f8415b4f24b189d065ed739517c548ba`:
- `invoke` initializes `cfg.OrigEnv` from
`toolchain.FilterEnv(os.Environ())` before command work
([`cmd/go/main.go`, lines 290305](https://github.com/golang/go/blob/c19862e5f8415b4f24b189d065ed739517c548ba/src/cmd/go/main.go#L290-L305)).
`FilterEnv` removes only Go's internal toolchain-switch count variable
([`cmd/go/internal/toolchain/select.go`, lines 5059 and 7485](https://github.com/golang/go/blob/c19862e5f8415b4f24b189d065ed739517c548ba/src/cmd/go/internal/toolchain/select.go#L50-L85));
WW has no corresponding switch state.
- `OrigEnv` is the startup environment and user binaries during `go test` use
it instead of build-tool `CmdEnv`
([`cmd/go/internal/cfg/cfg.go`, lines 328333](https://github.com/golang/go/blob/c19862e5f8415b4f24b189d065ed739517c548ba/src/cmd/go/internal/cfg/cfg.go#L328-L333)).
- Unix `copyenv`, `Getenv`, and `Environ` retain the first occurrence of a
normal case-sensitive key, clear later duplicates, omit cleared or empty
entries from `Environ`, and leave nonempty malformed entries present
([`syscall/env_unix.go`, lines 2050](https://github.com/golang/go/blob/c19862e5f8415b4f24b189d065ed739517c548ba/src/syscall/env_unix.go#L20-L50),
[6684](https://github.com/golang/go/blob/c19862e5f8415b4f24b189d065ed739517c548ba/src/syscall/env_unix.go#L66-L84),
and [135145](https://github.com/golang/go/blob/c19862e5f8415b4f24b189d065ed739517c548ba/src/syscall/env_unix.go#L135-L145)).
- `(*runTestActor).Act` clips `cfg.OrigEnv`, applies `AppendPATH` and
`AppendPWD`, assigns the result to the package-directory command, and runs it
([`cmd/go/internal/test/test.go`, lines 16611697](https://github.com/golang/go/blob/c19862e5f8415b4f24b189d065ed739517c548ba/src/cmd/go/internal/test/test.go#L1661-L1697)).
The two appenders are defined at
[`cmd/go/internal/base/env.go`, lines 1545](https://github.com/golang/go/blob/c19862e5f8415b4f24b189d065ed739517c548ba/src/cmd/go/internal/base/env.go#L15-L45),
and explicit-command duplicate removal prefers their later `PATH` and `PWD`
values
([`os/exec/exec.go`, lines 12311308](https://github.com/golang/go/blob/c19862e5f8415b4f24b189d065ed739517c548ba/src/os/exec/exec.go#L1231-L1308)).
- Official `test_env_term.txt` passes an explicitly empty caller `TERM` to a
test and requires it to remain empty
([lines 114](https://github.com/golang/go/blob/c19862e5f8415b4f24b189d065ed739517c548ba/src/cmd/go/testdata/script/test_env_term.txt#L1-L14)).
`test_cache_inputs.txt` changes caller `TESTKEY` and its `TestLookupEnv`
requires the arbitrary variable to be present
([lines 1938](https://github.com/golang/go/blob/c19862e5f8415b4f24b189d065ed739517c548ba/src/cmd/go/testdata/script/test_cache_inputs.txt#L19-L38)
and [269280](https://github.com/golang/go/blob/c19862e5f8415b4f24b189d065ed739517c548ba/src/cmd/go/testdata/script/test_cache_inputs.txt#L269-L280)).
Those source rules and official assertions are behavior directly implemented or
asserted by pinned Go. That an ordinary caller `LC_ALL`, `TMPDIR`, case-distinct
key, or other variable survives unchanged is behavior derived from the pinned
pipeline: none is removed or replaced after the original snapshot. First-value
normalization, malformed-entry retention, raw-empty omission, and the separation
from build-tool `CmdEnv` are directly implemented by the cited source.
#### Fresh four-axis audit and direct pre-fix measurements
The bounded audit considered all four permanent axes and selected only this
test-runtime difference:
- **Go-like build:** pinned unresolved relocation handling gives missing
`main.main` its dedicated link failure
([`cmd/link/internal/ld/errors.go`, lines 4565](https://github.com/golang/go/blob/c19862e5f8415b4f24b189d065ed739517c548ba/src/cmd/link/internal/ld/errors.go#L45-L65)),
asserted by `TestUndefinedRelocErrors`
([`cmd/link/internal/ld/ld_test.go`, lines 2045](https://github.com/golang/go/blob/c19862e5f8415b4f24b189d065ed739517c548ba/src/cmd/link/internal/ld/ld_test.go#L20-L45)).
Both WW stages rejected a selected `package main` without `fn main`, emitted
identical linker diagnostics, and created no output. This candidate was
aligned.
- **Go-like test:** a direct `execve` arranger supplied duplicate `LC_ALL`,
`TMPDIR`, arbitrary, and `PWD` variables; one empty-valued normal variable; a
case-distinct key; repeated nonempty malformed entries; and one raw empty
entry. Both stages exited `0` with empty stderr and byte-identical stdout
(SHA-256
`5b541239c8e9109c512b6ec7b8c59d4b18bf79391096fb14891d8de108786993`).
The test reported caller arbitrary/empty/case-distinct values, but reported
`LC_ALL` and `TMPDIR` as changed, two visible occurrences of the arbitrary
normal key, and the raw empty entry still present. These are directly measured
pre-fix WW facts and establish the selected difference.
- **Go-like package:** `MultiplePackageError` and the directory scan reject
conflicting selected declarations
([`go/build/build.go`, lines 538549](https://github.com/golang/go/blob/c19862e5f8415b4f24b189d065ed739517c548ba/src/go/build/build.go#L538-L549)
and [939967](https://github.com/golang/go/blob/c19862e5f8415b4f24b189d065ed739517c548ba/src/go/build/build.go#L939-L967));
`TestMultiplePackageImport` plus official `testdata/multi` asserts the result
([`go/build/build_test.go`, lines 105124](https://github.com/golang/go/blob/c19862e5f8415b4f24b189d065ed739517c548ba/src/go/build/build_test.go#L105-L124)).
Both WW stages rejected an `alpha`/`beta` source directory with byte-identical
diagnostics and no output. This candidate was aligned.
- **Go-like import:** `unusedImports` and `errorUnusedPkg` require a nonblank
renamed import to be used
([`cmd/compile/internal/types2/resolver.go`, lines 706740](https://github.com/golang/go/blob/c19862e5f8415b4f24b189d065ed739517c548ba/src/cmd/compile/internal/types2/resolver.go#L706-L740));
official `importdecl0` asserts the alias case
([`internal/types/testdata/check/importdecl0/importdecl0a.go`, lines 531](https://github.com/golang/go/blob/c19862e5f8415b4f24b189d065ed739517c548ba/src/internal/types/testdata/check/importdecl0/importdecl0a.go#L5-L31)).
Both WW stages rejected an unused local alias for dotted import `fmt` with
the same semantic diagnostic and no output. This candidate was aligned.
The command observations above are directly measured WW behavior. The linked
rules are behavior directly implemented or asserted by pinned Go. Applying the
original-environment pipeline at WW's directory-product launcher while leaving
its raw single-file compatibility route intact is behavior derived from the
pinned launch boundary and WW's local input model.
#### Ownership, final behavior, and preserved boundaries
`internal/wwpackage.runenv`, called only by `pkgstartrun`, is the semantic owner.
It walks the coordinator's inherited vector in order, uses a bounded fallible
open-addressed key table to retain the first normal case-sensitive occurrence,
omits raw empty entries, retains nonempty malformed entries, and excludes exact
uppercase `PATH` and `PWD`. It then appends the existing canonical selected
driver `PATH` and `PWD=<pkggroup.dir>`. The table is freed before launch;
`exec.start` deep-copies the command, after which the product-local vector and
its two generated strings are freed. Concurrent products share no writable
environment storage and the coordinator process is never mutated.
`toolenv` remains the separate build-plan owner. Compiler, assembler, in-driver
archiver, linker, support generation, generated-main construction, request
scratch, cwd, argv, stdin, diagnostics, and failure precedence are unchanged;
those tools still receive their established `LC_ALL=C` and request-private
`TMPDIR`. The raw single-file route already preserved caller locale and
temporary-directory values and remains outside this directory-owned
normalization slice. A directly invoked retained binary still inherits its
invoker's concrete environment without coordinator policy.
Loading and platform source selection are unchanged. Production, internal-test,
external-test, recompiled-for-test, support, and generated-main graph/action
identity remain separate and unchanged. Canonical dotted package/import
identity, declared names, file import bindings, physical-directory metadata,
symbols, `.wwi`, source units, assembly, objects, archives, executables, modes,
and artifact bytes do not contain the run environment. Imported or dependency
initialization code observes the corrected values only inside the selected
product process; no physical path or environment value becomes package, import,
graph, action, artifact, publication, or persistence identity.
`ww build`, `ww run`, directory `ww test -c` or `-S`, no-test products, and
loading/compiler/linker rejection start no test process, allocate no run
environment, and retain their prior diagnostics and outputs. A started test
observes the corrected snapshot before success, assertion failure, signal,
timeout, interruption, or child-created descendants. Those outcomes continue
through the existing process-group, capture, ordered-result, cancellation, and
cleanup owners. For a running retained request, private build and run still
precede guarded installation: runtime failure publishes nothing and preserves
prior bytes, while success installs the same private executable bytes. Producer
failure, output guard failure, and cleanup-only failure retain their existing
rollback and diagnostic precedence.
Persistent work records and artifact invalidation are unchanged; test results
are never cached. Environment-only changes perform the established warm final
link and always run the private test, without changing committed action bytes.
No `.new`, `.install`, `.wwtxn.*`, capture, result, process, or request scratch
survives its existing cleanup boundary. This runtime-only metadata change alters
no persisted-byte contract, so build workdir format remains `18`, test workdir
format remains `19`, and semantic storage format remains `3`.
The expanded WW-native `directory_test_execution_working_directory` observer
proves both-stage equality for nonempty and empty-valued variables; caller
`LC_ALL` and `TMPDIR`; first-wins normal duplicates; case-distinct and repeated
malformed entries; raw-empty omission; package `PWD` and selected-toolchain
`PATH`; production and test-only dependency initialization; internal, external,
combined, and test-only products; filters/list/no-match; recursive/equivalent
selection; serial/parallel scheduling; failure, signal, timeout, and child setup
failure; retained success and rollback; cold/warm/data-only persistence;
unchanged artifact and binary bytes; build/no-test/compile-only/rejection
nonexecution; unchanged raw/direct compatibility; exact tool locale/TMPDIR; and
stage, transaction, capture, and workdir cleanup.
## 12. Candidate architectures and hard-gate decision
Five candidates were developed as coherent systems, not as feature bins.

View File

@@ -770,6 +770,17 @@ compatibility route. It does not apply to build tools, `ww build`, `ww run`,
compile-only or assembly-only test requests, no-test products, or later direct
execution of a retained binary.
An executed directory product otherwise receives the caller's original Unix
environment, not the build plan's tool environment. The snapshot keeps the
first occurrence of each normal case-sensitive `key=value`, omits later normal
duplicates and raw empty entries, and retains every nonempty malformed entry in
order. It then applies only the effective `PATH` above and the package `PWD`.
Consequently caller `LC_ALL`, `TMPDIR`, empty-valued variables, case-distinct
keys, and arbitrary variables remain visible to production or test-only
initialization and test bodies. The compiler, assembler, archiver, linker, and
coordinator scratch continue to use their separate build-plan locale and
temporary directory.
The same coordinator-executed product reads standard input from the null
device. Its first read observes EOF regardless of the terminal, pipe, or file
connected to the invoking command. Each parallel product owns a separate null

View File

@@ -305,6 +305,16 @@ the private run and install only after success. Compile-only, assembly-only,
no-test, rejected, and build requests start no test process and therefore have
no test-PATH state.
For a directory product, the rest of the child vector is a Go-like snapshot of
the caller environment rather than the build-tool environment: the first
case-sensitive occurrence of every normal `key=value` survives, later normal
duplicates and raw empty entries do not, and nonempty malformed entries remain
in order. Only the effective `PATH` and package `PWD` are then appended.
Caller `LC_ALL`, `TMPDIR`, empty values, case-distinct keys, and arbitrary
variables therefore reach dependency initialization and selected tests.
Build-plan compiler, assembler, archiver, linker, and scratch environments keep
their existing pinned locale and request-private temporary directory.
Every such product also receives an independently opened null device as fd 0.
Caller terminal, pipe, and file bytes remain with the coordinator; serial and
parallel products observe immediate EOF rather than consuming a shared input

View File

@@ -116,6 +116,11 @@ fn pkgallocstrs(cap: i32) ([]str | nomem) = {
return value;
};
fn pkgalloci32(cap: i32) ([]i32 | nomem) = {
let value: []i32 = alloc([], cap: u64)?;
return value;
};
fn pkgallocbytes(cap: i32) ([]u8 | nomem) = {
let value: []u8 = alloc([], cap: u64)?;
return value;
@@ -268,11 +273,81 @@ fn pkgfreestrs(values: []str) void = {
};
};
// Go's AppendPATH and AppendPWD append, then os/exec keeps the last duplicate
// values. WW's executor preserves duplicates, so discard earlier exact entries
// and materialize the selected driver's sibling directory first in PATH.
fn runenv(tmpdir: str, pwd: str, builder: str, out: *[]str, tmpowned: *str,
pathowned: *str, pwdowned: *str) bool = {
fn pkgenvkeylen(entry: str) i32 = {
let i: i32 = 0;
for (i < entry.len) {
if (entry[i] == '=') { return i; };
i += 1;
};
return -1;
};
fn pkgenvkeyequal(a: str, an: i32, b: str, bn: i32) bool = {
if (an != bn) { return false; };
let i: i32 = 0;
for (i < an) {
if (a[i] != b[i]) { return false; };
i += 1;
};
return true;
};
fn pkgenvkeyis(entry: str, n: i32, name: str) bool = {
return pkgenvkeyequal(entry, n, name, name.len);
};
fn pkgenvhash(entry: str, n: i32) u64 = {
let h: u64 = 14695981039346656037u64;
let i: i32 = 0;
for (i < n) {
h = h ^ (entry[i]: u64);
h = h * 1099511628211u64;
i += 1;
};
return h;
};
fn pkgenvtablecap(entries: i32) i32 = {
if (entries == 0) { return 0; };
if (entries > PKG_COUNT_MAX / 2) { return -1; };
let need: i32 = entries * 2;
let cap: i32 = PKG_INITIAL_CAP;
for (cap < need) {
if (cap > PKG_COUNT_MAX / 2) { return -1; };
cap *= 2;
};
return cap;
};
// Go snapshots user-test variables through Unix os.Environ before appending
// PATH and PWD. WW's raw environment walker and executor do no such duplicate
// normalization, so the launch owner must materialize that snapshot itself.
fn pkgenvfirst(slots: []i32, inherited: []str, at: i32, n: i32) bool = {
let slot: i32 = (pkgenvhash(inherited[at], n)
% (slots.len: u64)): i32;
let probes: i32 = 0;
for (probes < slots.len) {
let prior: i32 = slots[slot];
if (prior < 0) {
slots[slot] = at;
return true;
};
let priorn: i32 = pkgenvkeylen(inherited[prior]);
if (pkgenvkeyequal(inherited[at], n,
inherited[prior], priorn)) {
return false;
};
slot += 1;
if (slot == slots.len) { slot = 0; };
probes += 1;
};
return false;
};
// Go's appended PATH and PWD survive os/exec's later-value selection. WW's
// executor preserves its concrete vector, so inherited copies are excluded.
fn runenv(pwd: str, builder: str, out: *[]str, pathowned: *str,
pwdowned: *str) bool = {
let toolbin: str;
let oom: bool = false;
if (!pkgcanonicaldir(pkgdirname(builder), &toolbin, &oom)) {
@@ -283,35 +358,71 @@ fn runenv(tmpdir: str, pwd: str, builder: str, out: *[]str, tmpowned: *str,
return false;
};
let inherited: []str = os.getenvs();
if (inherited.len > PKG_COUNT_MAX - 4) {
if (inherited.len > PKG_COUNT_MAX - 2) {
pkgputln(os.STDERR_FILENO,
"wwtest package: package graph is too large");
return false;
};
let allocation: ([]str | nomem) = pkgallocstrs(inherited.len + 4);
let allocation: ([]str | nomem) = pkgallocstrs(inherited.len + 2);
let env: []str;
match (allocation) {
case let value: []str => env = value;
case nomem => {
pkgputln(os.STDERR_FILENO, "wwtest package: out of memory");
return false;
};
};
};
let keyed: i32 = 0;
let i: i32 = 0;
for (i < inherited.len) {
if (!strings.hasprefix(inherited[i], "TMPDIR=")
&& !strings.hasprefix(inherited[i], "LC_ALL=")
&& !strings.hasprefix(inherited[i], "PATH=")
&& !strings.hasprefix(inherited[i], "PWD=")) {
let n: i32 = pkgenvkeylen(inherited[i]);
if (n >= 0 && !pkgenvkeyis(inherited[i], n, "PATH")
&& !pkgenvkeyis(inherited[i], n, "PWD")) {
keyed += 1;
};
i += 1;
};
let tablecap: i32 = pkgenvtablecap(keyed);
if (tablecap < 0) {
pkgputln(os.STDERR_FILENO,
"wwtest package: package graph is too large");
pkgfreestrs(env);
return false;
};
let slots: []i32;
slots.ptr = nil: *i32;
slots.len = 0;
slots.cap = 0;
if (tablecap != 0) {
let tableallocation: ([]i32 | nomem) = pkgalloci32(tablecap);
match (tableallocation) {
case let value: []i32 => slots = value;
case nomem => {
pkgputln(os.STDERR_FILENO,
"wwtest package: out of memory");
pkgfreestrs(env);
return false;
};
};
slots.len = tablecap;
i = 0;
for (i < slots.len) { slots[i] = -1; i += 1; };
};
i = 0;
for (i < inherited.len) {
let n: i32 = pkgenvkeylen(inherited[i]);
if (n < 0) {
if (inherited[i].len != 0) { append(env, inherited[i]); };
} else if (!pkgenvkeyis(inherited[i], n, "PATH")
&& !pkgenvkeyis(inherited[i], n, "PWD")
&& pkgenvfirst(slots, inherited, i, n)) {
append(env, inherited[i]);
};
i += 1;
};
append(env, "LC_ALL=C");
let tmpenv: str;
if (!pkgstring(&tmpenv, "TMPDIR=", tmpdir)) {
pkgfreestrs(env);
return false;
if (slots.ptr != nil) {
os.free(slots.ptr: *void,
(slots.cap: u64) * (size(i32): u64));
};
let pathenv: str;
let pathok: bool = false;
@@ -327,29 +438,24 @@ fn runenv(tmpdir: str, pwd: str, builder: str, out: *[]str, tmpowned: *str,
case void => pathok = pkgstring(&pathenv, "PATH=", toolbin);
};
if (!pathok) {
pkgfreeownedstr(tmpenv);
pkgfreestrs(env);
return false;
};
let pwdenv: str;
if (!pkgstring(&pwdenv, "PWD=", pwd)) {
pkgfreeownedstr(tmpenv);
pkgfreeownedstr(pathenv);
pkgfreestrs(env);
return false;
};
append(env, tmpenv);
append(env, pathenv);
append(env, pwdenv);
*out = env;
*tmpowned = tmpenv;
*pathowned = pathenv;
*pwdowned = pwdenv;
return true;
};
fn freerunenv(env: []str, tmpowned: str, pathowned: str, pwdowned: str) void = {
pkgfreeownedstr(tmpowned);
fn freerunenv(env: []str, pathowned: str, pwdowned: str) void = {
pkgfreeownedstr(pathowned);
pkgfreeownedstr(pwdowned);
pkgfreestrs(env);
@@ -1624,11 +1730,9 @@ fn pkgstartrun(g: *pkggroup, filters: []str, timeoutarg: str,
let i: i32 = 0;
for (i < filters.len) { append(ra, filters[i]); i += 1; };
let env: []str;
let tmpowned: str;
let pathowned: str;
let pwdowned: str;
if (!runenv(g.root, g.dir, builder, &env, &tmpowned,
&pathowned, &pwdowned)) {
if (!runenv(g.dir, builder, &env, &pathowned, &pwdowned)) {
pkgfreeownedstr(packagearg);
pkgfreestrs(ra);
return false;
@@ -1644,7 +1748,7 @@ fn pkgstartrun(g: *pkggroup, filters: []str, timeoutarg: str,
rcmd.deadline.nsec = 0i64;
rcmd.grace = 0i64: time.duration;
exec.start(h, &rcmd);
freerunenv(env, tmpowned, pathowned, pwdowned);
freerunenv(env, pathowned, pwdowned);
pkgfreeownedstr(packagearg);
pkgfreestrs(ra);
return true;

View File

@@ -633,11 +633,16 @@ fn hexbytes(value: str) str = {
fn cwdtestenv(stage: str) []str = {
let inherited: []str = os.getenvs();
let env: []str = alloc([], (inherited.len + 10): u64)!;
let env: []str = alloc([], (inherited.len + 24): u64)!;
let i: i32 = 0;
for (i < inherited.len) {
if (!strings.hasprefix(inherited[i], "PWD=")
&& !strings.hasprefix(inherited[i], "PATH=")
&& !strings.hasprefix(inherited[i], "LC_ALL=")
&& !strings.hasprefix(inherited[i], "TMPDIR=")
&& !strings.hasprefix(inherited[i], "WW_CWD_ORIGINAL=")
&& !strings.hasprefix(inherited[i], "WW_CWD_EMPTY=")
&& !strings.hasprefix(inherited[i], "lc_all=")
&& !strings.hasprefix(inherited[i], "WW_W6C=")
&& !strings.hasprefix(inherited[i], "WW_W6A=")
&& !strings.hasprefix(inherited[i], "WW_W6L=")
@@ -649,6 +654,14 @@ fn cwdtestenv(stage: str) []str = {
};
append(env, "PATH=/ww-path-inherited-first");
append(env, "PWD=/ww-cwd-inherited-first");
append(env, "LC_ALL=C.UTF-8");
append(env, "TMPDIR=/ww-caller-tmp-first");
append(env, "WW_CWD_ORIGINAL=first");
append(env, "WW_CWD_EMPTY=");
append(env, "lc_all=case-kept");
append(env, "WW_CWD_MALFORMED");
append(env, "");
append(env, "WW_CWD_MALFORMED");
if (same(stage, "ww")) {
append(env, strings.concat("WW_W6C=", driver("w6c")));
append(env, strings.concat("WW_W6A=", driver("w6a")));
@@ -660,6 +673,9 @@ fn cwdtestenv(stage: str) []str = {
append(env, strings.concat("WW_W6L=", driver("w6l_ww")));
};
append(env, strings.concat("WW_SRCLIB=", repo(), "/lib"));
append(env, "LC_ALL=C");
append(env, "TMPDIR=/ww-caller-tmp-last");
append(env, "WW_CWD_ORIGINAL=last");
append(env, "PATH=/ww-path-inherited-last");
append(env, "PWD=/ww-cwd-inherited-last");
return env;
@@ -679,6 +695,42 @@ fn cwdassertcallerpath(text: str, label: str) void = {
cwdassertpath(text, label, "/ww-path-inherited-first", "2");
};
fn cwdassertenvvalues(text: str, label: str) void = {
assert(has(text, strings.concat(label, " LC_ALL=C.UTF-8\n")));
assert(has(text, strings.concat(label,
" TMPDIR=/ww-caller-tmp-first\n")));
assert(has(text, strings.concat(label,
" WW_CWD_ORIGINAL=first\n")));
assert(has(text, strings.concat(label, " WW_CWD_EMPTY=\n")));
assert(has(text, strings.concat(label, " lc_all=case-kept\n")));
};
fn cwdassertdirectoryenv(text: str, label: str) void = {
cwdassertenvvalues(text, label);
assert(has(text, strings.concat(label, " LC_ALL-count=1\n")));
assert(has(text, strings.concat(label, " TMPDIR-count=1\n")));
assert(has(text, strings.concat(label,
" WW_CWD_ORIGINAL-count=1\n")));
assert(has(text, strings.concat(label,
" WW_CWD_EMPTY-count=1\n")));
assert(has(text, strings.concat(label, " lc_all-count=1\n")));
assert(has(text, strings.concat(label, " malformed-count=2\n")));
assert(has(text, strings.concat(label, " empty-entry-count=0\n")));
};
fn cwdassertcallerenv(text: str, label: str) void = {
cwdassertenvvalues(text, label);
assert(has(text, strings.concat(label, " LC_ALL-count=2\n")));
assert(has(text, strings.concat(label, " TMPDIR-count=2\n")));
assert(has(text, strings.concat(label,
" WW_CWD_ORIGINAL-count=2\n")));
assert(has(text, strings.concat(label,
" WW_CWD_EMPTY-count=1\n")));
assert(has(text, strings.concat(label, " lc_all-count=1\n")));
assert(has(text, strings.concat(label, " malformed-count=2\n")));
assert(has(text, strings.concat(label, " empty-entry-count=1\n")));
};
fn cwdassertrecord(text: str, label: str, cwd: str, pwd: str,
pwdcount: str, pwdlast: str, data: str, testdata: str,
input: str) void = {
@@ -724,6 +776,7 @@ fn cwdassertpackage(text: str, label: str, dir: str, data: str,
testdata: str) void = {
cwdassertrecord(text, label, dir, dir, "1", "yes", data, testdata,
"eof");
cwdassertdirectoryenv(text, label);
cwdasserttestpath(text, label);
cwdassertmergedstreams(text, label, dir);
};
@@ -795,10 +848,10 @@ fn cwdwritedata(dir: str, label: str) void = {
clean(root);
};
// Directory-package execution is one product-level runtime property. This
// fixture keeps every source tree disposable while covering the variant,
// dependency, routing, concurrency, persistence, and failure boundaries that
// could otherwise accidentally derive a cwd from an action or request name.
// Directory-package execution context is one product-level runtime property.
// This fixture keeps every source tree disposable while covering the variant,
// dependency, environment, routing, concurrency, persistence, and failure
// boundaries that could otherwise leak build-plan or caller process state.
@test fn directory_test_execution_working_directory() void = {
let root: str = fresh();
let caller: str = strings.concat(root, "/caller");
@@ -872,12 +925,22 @@ fn cwdwritedata(dir: str, label: str) void = {
"};\n",
"fn put(value: str) void = { putfd(os.STDOUT_FILENO, value); };\n",
"fn puterr(value: str) void = { putfd(os.STDERR_FILENO, value); };\n",
"fn putcount(value: i32) void = {\n",
" if (value == 0) { put(\"0\\n\"); } else { if (value == 1) {\n",
" put(\"1\\n\"); } else { if (value == 2) { put(\"2\\n\");\n",
" } else { put(\"many\\n\"); }; }; };\n",
"};\n",
"export fn streams(label: str, context: str) void = {\n",
" put(strings.concat(\"STREAM \", label, \" out-1 cwd=\", context, \"\\n\"));\n",
" puterr(strings.concat(\"STREAM \", label, \" err-1 cwd=\", context, \"\\n\"));\n",
" put(strings.concat(\"STREAM \", label, \" out-2 cwd=\", context, \"\\n\"));\n",
" puterr(strings.concat(\"STREAM \", label, \" err-2 cwd=\", context, \"\\n\"));\n",
"};\n");
let envsource: str = strings.concat(
" put(label); put(\" LC_ALL=\");\n match (os.getenv(\"LC_ALL\")) {\n case let value: str => put(value); case void => put(\"<unset>\");\n }; put(\"\\n\");\n put(label); put(\" TMPDIR=\");\n match (os.getenv(\"TMPDIR\")) {\n case let value: str => put(value); case void => put(\"<unset>\");\n }; put(\"\\n\");\n put(label); put(\" WW_CWD_ORIGINAL=\");\n match (os.getenv(\"WW_CWD_ORIGINAL\")) {\n case let value: str => put(value); case void => put(\"<unset>\");\n }; put(\"\\n\");\n put(label); put(\" WW_CWD_EMPTY=\");\n match (os.getenv(\"WW_CWD_EMPTY\")) {\n case let value: str => put(value); case void => put(\"<unset>\");\n }; put(\"\\n\");\n put(label); put(\" lc_all=\");\n match (os.getenv(\"lc_all\")) {\n case let value: str => put(value); case void => put(\"<unset>\");\n }; put(\"\\n\");\n",
" let lccount: i32 = 0; let tmpcount: i32 = 0;\n let originalcount: i32 = 0; let emptyvaluecount: i32 = 0;\n let lowercount: i32 = 0; let malformedcount: i32 = 0;\n let emptyentrycount: i32 = 0;\n i = 0; for (i < env.len) {\n if (strings.hasprefix(env[i], \"LC_ALL=\")) { lccount += 1; };\n if (strings.hasprefix(env[i], \"TMPDIR=\")) { tmpcount += 1; };\n if (strings.hasprefix(env[i], \"WW_CWD_ORIGINAL=\")) { originalcount += 1; };\n if (strings.hasprefix(env[i], \"WW_CWD_EMPTY=\")) { emptyvaluecount += 1; };\n if (strings.hasprefix(env[i], \"lc_all=\")) { lowercount += 1; };\n if (strings.compare(env[i], \"WW_CWD_MALFORMED\") == 0) { malformedcount += 1; };\n if (env[i].len == 0) { emptyentrycount += 1; };\n i += 1;\n };\n",
" put(label); put(\" PWD-count=\"); putcount(count);\n put(label); put(\" PWD-last=\");\n if (last == env.len - 1) { put(\"yes\\n\"); } else { put(\"no\\n\"); };\n put(label); put(\" PATH-count=\"); putcount(pathcount);\n",
" put(label); put(\" LC_ALL-count=\"); putcount(lccount);\n put(label); put(\" TMPDIR-count=\"); putcount(tmpcount);\n put(label); put(\" WW_CWD_ORIGINAL-count=\"); putcount(originalcount);\n put(label); put(\" WW_CWD_EMPTY-count=\"); putcount(emptyvaluecount);\n put(label); put(\" lc_all-count=\"); putcount(lowercount);\n put(label); put(\" malformed-count=\"); putcount(malformedcount);\n put(label); put(\" empty-entry-count=\"); putcount(emptyentrycount);\n");
writefile(strings.concat(probe, "/probe.ww"), strings.concat(
"package probe;\n",
"import os;\n",
@@ -902,16 +965,7 @@ fn cwdwritedata(dir: str, label: str) void = {
" if (body.len == 0 || body[body.len - 1] != '\\n') { put(\"\\n\"); };\n",
"};\n",
"export fn run(label: str) void = {\n let buf: [4096]u8;\n let n: i64 = os.getcwd(&buf[0], size([4096]u8)); assert(n > 1i64);\n let cwd: str; cwd.ptr = &buf[0]; cwd.len = (n - 1i64): i32;\n streams(label, cwd);\n put(label); put(\" cwd=\"); put(cwd); put(\"\\n\");\n put(label); put(\" PWD=\");\n match (os.getenv(\"PWD\")) {\n case let value: str => put(value); case void => put(\"<unset>\");\n }; put(\"\\n\");\n put(label); put(\" PATH=\");\n match (os.getenv(\"PATH\")) {\n case let value: str => put(value); case void => put(\"<unset>\");\n }; put(\"\\n\");\n let env: []str = os.getenvs(); let count: i32 = 0; let pathcount: i32 = 0; let last: i32 = -1;\n let i: i32 = 0; for (i < env.len) {\n if (strings.hasprefix(env[i], \"PWD=\")) { count += 1; last = i; };\n if (strings.hasprefix(env[i], \"PATH=\")) { pathcount += 1; };\n i += 1;\n };\n",
" put(label); put(\" PWD-count=\");\n",
" if (count == 0) { put(\"0\\n\"); } else { if (count == 1) {\n",
" put(\"1\\n\"); } else { if (count == 2) { put(\"2\\n\");\n",
" } else { put(\"many\\n\"); }; }; };\n",
" put(label); put(\" PWD-last=\");\n",
" if (last == env.len - 1) { put(\"yes\\n\"); } else { put(\"no\\n\"); };\n",
" put(label); put(\" PATH-count=\");\n",
" if (pathcount == 0) { put(\"0\\n\"); } else { if (pathcount == 1) {\n",
" put(\"1\\n\"); } else { if (pathcount == 2) { put(\"2\\n\");\n",
" } else { put(\"many\\n\"); }; }; };\n",
envsource,
" emitfile(label, \"data\", \"data.txt\");\n",
" emitfile(label, \"testdata\", \"testdata/input.txt\");\n",
" let input: [1]u8;\n let inputn: i64 = os.read(os.STDIN_FILENO, &input[0], 1u64);\n put(label); put(\" stdin=\");\n if (inputn == 0i64) { put(\"eof\\n\"); } else {\n if (inputn == 1i64) { put(\"data\\n\"); } else { put(\"<error>\\n\"); };\n };\n",
@@ -1078,6 +1132,7 @@ fn cwdwritedata(dir: str, label: str) void = {
expectexit(&outc, 0);
cwdassertrecord(outc.stdout, "p4-internal-only", p4, p4, "1", "yes",
"p4-data", "p4-testdata", "eof");
cwdassertdirectoryenv(outc.stdout, "p4-internal-only");
cwdassertpath(outc.stdout, "p4-internal-only",
strings.concat(repo(), "/out/bin"), "1");
assert(os.remove(strings.concat(p4, "/created.txt")) == 0);
@@ -1224,6 +1279,8 @@ fn cwdwritedata(dir: str, label: str) void = {
assert(has(outc.stderr,
"test package must match production package or <package>_test\n"));
assert(!has(outc.stdout, " PATH=") && !has(outw.stdout, " PATH="));
assert(!has(outc.stdout, " LC_ALL=")
&& !has(outw.stdout, " LC_ALL="));
assert(directoryisempty(rejectcwork));
assert(directoryisempty(rejectwwork));
@@ -1239,6 +1296,8 @@ fn cwdwritedata(dir: str, label: str) void = {
assert(!has(out.stdout, "cmdno-init"));
assert(!has(out.stdout, "cmdno-main"));
assert(!has(out.stdout, " PATH="));
assert(!has(out.stdout, " LC_ALL=")
&& !has(out.stdout, " TMPDIR="));
assert(!os.exists(strings.concat(plain, "/created.txt")));
assert(!os.exists(strings.concat(cmdno, "/created.txt")));
let commandbin: str = strings.concat(root, "/cmdno.bin");
@@ -1251,6 +1310,8 @@ fn cwdwritedata(dir: str, label: str) void = {
assert(os.exists(commandbin));
assert(!has(out.stdout, "cmdno-init") && !has(out.stdout, "cmdno-main"));
assert(!has(out.stdout, " PATH="));
assert(!has(out.stdout, " LC_ALL=")
&& !has(out.stdout, " TMPDIR="));
assert(!os.exists(strings.concat(cmdno, "/created.txt")));
// Compile-only paths do not run. A published binary has no embedded cwd or
@@ -1267,6 +1328,8 @@ fn cwdwritedata(dir: str, label: str) void = {
assert(!os.exists(strings.concat(p2, "/created.txt")));
assert(!has(out.stdout, "dep-init") && !has(out.stdout, "p2-external"));
assert(!has(out.stdout, " PATH="));
assert(!has(out.stdout, " LC_ALL=")
&& !has(out.stdout, " TMPDIR="));
assert(os.remove(defaultbin) == 0);
assert(os.remove(strings.concat(p1, "/created.txt")) == 0);
@@ -1288,6 +1351,8 @@ fn cwdwritedata(dir: str, label: str) void = {
assert(same(readfile(p1cbin), readfile(p1wbin)));
assert(!has(outc.stdout, "dep-init") && !has(outw.stdout, "dep-init"));
assert(!has(outc.stdout, " PATH=") && !has(outw.stdout, " PATH="));
assert(!has(outc.stdout, " LC_ALL=")
&& !has(outw.stdout, " LC_ALL="));
let directbin: []str = [p1cbin, "-package=p1"];
runcommandinputenvdir(root, "cwd-published-direct", directbin, cenv,
@@ -1300,6 +1365,8 @@ fn cwdwritedata(dir: str, label: str) void = {
cwdassertrecord(out.stdout, "p1-internal", caller,
"/ww-cwd-inherited-first", "2", "no", "caller-data",
"caller-testdata", "data");
cwdassertcallerenv(out.stdout, "dep-init");
cwdassertcallerenv(out.stdout, "p1-internal");
cwdassertcallerpath(out.stdout, "dep-init");
cwdassertcallerpath(out.stdout, "p1-internal");
cwdassertsplitstreams(out.stdout, out.stderr, "dep-init", caller);
@@ -1328,6 +1395,7 @@ fn cwdwritedata(dir: str, label: str) void = {
cwdassertrecord(outc.stdout, "p4-internal-only", caller,
"/ww-cwd-inherited-first", "2", "yes", "caller-data",
"caller-testdata", "data");
cwdassertcallerenv(outc.stdout, "p4-internal-only");
cwdasserttestpath(outc.stdout, "p4-internal-only");
cwdassertsplitstreams(outc.stdout, outc.stderr,
"p4-internal-only", caller);
@@ -1666,6 +1734,8 @@ fn cwdwritedata(dir: str, label: str) void = {
"p2-data", "p2-testdata", "eof");
cwdassertrecord(out.stdout, "p2-external", p2, p2, "1", "yes",
"p2-data", "p2-testdata", "eof");
cwdassertdirectoryenv(out.stdout, "dep-init");
cwdassertdirectoryenv(out.stdout, "p2-external");
cwdassertpath(out.stdout, "dep-init", strings.concat(root,
":/ww-path-inherited-first"), "1");
cwdassertpath(out.stdout, "p2-external", strings.concat(root,