From 9b6b1100f46d26b24f49c15c89f3b5e91ae5148c Mon Sep 17 00:00:00 2001 From: Hojun-Cho Date: Fri, 21 Aug 2026 05:30:10 +0900 Subject: [PATCH] ww: put selected toolchain first in test PATH --- cmd/ww/main.c | 85 +++++++++++++++- docs/build-system.md | 181 ++++++++++++++++++++++++++++++---- docs/spec.md | 19 +++- docs/test-system-v2.md | 41 +++++--- internal/wwpackage/package.ww | 65 +++++++++--- selfhost/cmd/ww/main.ww | 115 ++++++++++++++++++--- test/package/package_test.ww | 176 ++++++++++++++++++++++++++------- 7 files changed, 586 insertions(+), 96 deletions(-) diff --git a/cmd/ww/main.c b/cmd/ww/main.c index 0cf32f61..d32a2c5e 100644 --- a/cmd/ww/main.c +++ b/cmd/ww/main.c @@ -16,6 +16,7 @@ #include #include #include +#include #include #ifndef PATH_MAX @@ -82,6 +83,71 @@ run_argv(const char *prog, char *const argv[]) return 1; } +extern char **environ; + +struct test_environment { + char **values; + char *path; +}; + +/* Pinned cmd/go appends PATH=$GOROOT/bin:$PATH to each test command and + * os/exec keeps the appended duplicate. The selected WW driver's sibling + * directory is the local toolchain-bin analogue. Build tools retain the + * caller's environment; only the test binary receives this array. */ +static int +make_test_environment(const char *toolbin, struct test_environment *out) +{ + const char *oldpath = getenv("PATH"); + if (oldpath == NULL) oldpath = ""; + size_t dirn = strlen(toolbin), oldn = strlen(oldpath); + if (oldn > SIZE_MAX - 7 || dirn > SIZE_MAX - oldn - 7) return -1; + size_t pathn = 5 + dirn + (oldn != 0 ? 1 + oldn : 0); + char *path = malloc(pathn + 1); + if (path == NULL) return -1; + memcpy(path, "PATH=", 5); + memcpy(path + 5, toolbin, dirn); + size_t at = 5 + dirn; + if (oldn != 0) { + path[at++] = ':'; + memcpy(path + at, oldpath, oldn); + at += oldn; + } + path[at] = '\0'; + + size_t nenv = 0; + while (environ[nenv] != NULL) { + if (nenv == SIZE_MAX - 2) { free(path); return -1; } + nenv++; + } + if (nenv > SIZE_MAX / sizeof(char *) - 2) { + free(path); + return -1; + } + char **values = calloc(nenv + 2, sizeof *values); + if (values == NULL) { free(path); return -1; } + size_t n = 0; + int inserted = 0; + for (size_t i = 0; i < nenv; i++) { + if (strncmp(environ[i], "PATH=", 5) == 0) { + if (!inserted) { values[n++] = path; inserted = 1; } + } else { + values[n++] = environ[i]; + } + } + if (!inserted) values[n++] = path; + values[n] = NULL; + out->values = values; + out->path = path; + return 0; +} + +static void +free_test_environment(struct test_environment *env) +{ + free(env->values); + free(env->path); +} + /* run_test_bin — exec the built test binary with an optional name-filter * pattern as argv[1] (lib/test run() reads it via os.args). fork+execv * (not system()) so glob metacharacters in the pattern reach the binary @@ -91,14 +157,28 @@ run_argv(const char *prog, char *const argv[]) static int run_test_bin(const char *bin, const char *pattern) { + char toolbin[PATH_MAX]; + if (realpath(self_dir, toolbin) == NULL) { + fputs("ww: cannot prepare test environment\n", stderr); + return -1; + } + struct test_environment env = {0}; + if (make_test_environment(toolbin, &env) != 0) { + fputs("ww: cannot prepare test environment\n", stderr); + return -1; + } pid_t pid = fork(); - if (pid < 0) { perror("ww: fork"); return -1; } + if (pid < 0) { + perror("ww: fork"); + free_test_environment(&env); + return -1; + } if (pid == 0) { char *xargv[3]; xargv[0] = (char *)bin; if (pattern) { xargv[1] = (char *)pattern; xargv[2] = NULL; } else { xargv[1] = NULL; } - execv(bin, xargv); + execve(bin, xargv, env.values); perror("ww: exec"); _exit(127); } @@ -108,6 +188,7 @@ run_test_bin(const char *bin, const char *pattern) * test PASS. */ pid_t got; do { got = waitpid(pid, &status, 0); } while (got < 0 && errno == EINTR); + free_test_environment(&env); if (got < 0) { perror("ww: waitpid"); return -1; } if (WIFEXITED(status)) return WEXITSTATUS(status); return 1; diff --git a/docs/build-system.md b/docs/build-system.md index 7968d074..67e34a99 100644 --- a/docs/build-system.md +++ b/docs/build-system.md @@ -5849,22 +5849,24 @@ 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=`, and -uppercase `PWD=` entries, then appends `LC_ALL=C`, the product-local absolute -`TMPDIR`, and `PWD=`. Removing prior `PWD` entries reproduces -Go's observable last-wins result because WW's executor intentionally preserves +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=`. 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. +the appended position, and one effective uppercase `PATH`; section 11.32 owns +the latter rule. -The vector and both generated strings are product-local, dynamically sized, +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 -`PWD`, generated `-package` argument, and argv vector. Concurrent children -therefore hold independent fork snapshots; no shared environment vector or -process-global state is mutated. +`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 @@ -5880,15 +5882,19 @@ emission remains byte-sorted and identical to `-j 1`. `-c -o`, builds or publishes but never enters `pkgstartrun`; no execution cwd or run environment is allocated. A published binary subsequently invoked by the user bypasses the coordinator and inherits the user's cwd and environment. -The raw single-file test compatibility route is likewise unchanged. A -directory with no selected test source still creates no support, generated -main, link, run, result, or execution-context state. +The raw single-file test compatibility route retains its caller cwd, `PWD`, +stdin, and stream behavior, but section 11.32 applies the test-process `PATH` +rule at its driver-owned launch. A directory with no selected test source still +creates no support, generated main, link, run, result, or execution-context +state. Build-plan commands retain `dir=""` and their existing tool environment. -Compiler, assembler, in-driver archiver, linker, support generation, generated -main construction, the Cstage driver, and the WWstage driver therefore retain -their exact prior cwd, argv, and environment. The runtime rule required no -`lib/os/exec`, compiler, checker, writer, assembler, linker, or driver change. +Compiler, assembler, in-driver archiver, linker, support generation, and +generated-main construction therefore retain their exact prior cwd, argv, and +environment. The directory cwd rule required no `lib/os/exec`, compiler, +checker, writer, assembler, linker, or driver change; the later test-only PATH +rule is isolated at launch and changes neither build-plan environment nor those +tools. Independent Cstage/WWstage compile-only products remain byte-identical, and changing only `data.txt` or `testdata` changes no unit, `.wwi`, assembly, object, archive, generated-main, or binary bytes. @@ -5916,13 +5922,15 @@ The focused native owner is `directory_test_execution_working_directory` in `test/package/package_test.ww`. It creates only disposable source trees and compares Cstage and WWstage across duplicate and large environments; exact -`getcwd`, effective/count/position of `PWD`; ordinary data, `testdata`, and +`getcwd`, effective/count/position of `PWD`; absent, empty, nonempty, and +duplicate inherited `PATH`; ordinary data, `testdata`, and relative writes; all production/internal/external/test-only shapes; 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`, direct binaries, and raw -single files; exact tool cwd/argv/locale/TMPDIR; persistent data-only reuse and +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 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 @@ -6904,6 +6912,141 @@ and retained-binary contracts. Existing request-transaction, timeout, interruption, and concurrent-driver owners continue to cover those unchanged dimensions. +### 11.32 Implemented selected toolchain first in test `PATH` + +Every test binary actually started by `ww test` now receives one effective +uppercase `PATH` beginning with the canonical absolute directory of the +selected WW driver. An absent or empty inherited value produces only that +directory. A nonempty first effective inherited value follows it after `:`; +ordinary duplicate `PATH=` entries collapse to the one child value. This is +test-process metadata only: build tools and every request that starts no test +process retain their prior environment. + +#### Pinned Go evidence and classification + +The sole authority is official Go 1.26.5 at commit +`c19862e5f8415b4f24b189d065ed739517c548ba`: + +- The `go test` command documentation states that `$GOROOT/bin` is placed at + the beginning of the test process's `PATH`, so an executed test resolves the + `go` command from the invoking toolchain + ([`cmd/go/internal/test/test.go`, lines 87–94](https://github.com/golang/go/blob/c19862e5f8415b4f24b189d065ed739517c548ba/src/cmd/go/internal/test/test.go#L87-L94)). +- `(*runTestActor).Act` begins with `cfg.OrigEnv`, applies + `base.AppendPATH`, then `base.AppendPWD`, and assigns that environment to + each test command + ([`cmd/go/internal/test/test.go`, lines 1661–1668](https://github.com/golang/go/blob/c19862e5f8415b4f24b189d065ed739517c548ba/src/cmd/go/internal/test/test.go#L1661-L1668)). + `AppendPATH` appends `PATH=$GOROOT/bin` for an empty effective inherited + value and `PATH=$GOROOT/bin:` otherwise + ([`cmd/go/internal/base/env.go`, lines 29–45](https://github.com/golang/go/blob/c19862e5f8415b4f24b189d065ed739517c548ba/src/cmd/go/internal/base/env.go#L29-L45)). +- `Cmd.environ` removes duplicate environment keys while preferring the later + entry, making the appended value effective + ([`os/exec/exec.go`, lines 1231–1265](https://github.com/golang/go/blob/c19862e5f8415b4f24b189d065ed739517c548ba/src/os/exec/exec.go#L1231-L1265)). + On the supported Unix model, initial environment lookup uses the first + inherited occurrence + ([`syscall/env_unix.go`, lines 20–44 and 66–84](https://github.com/golang/go/blob/c19862e5f8415b4f24b189d065ed739517c548ba/src/syscall/env_unix.go#L20-L84)). +- Official `test_goroot_PATH.txt` tests both an empty `PATH` and a nonempty + directory containing no executable; the test must find the `go` executable + in the current toolchain's `$GOROOT/bin` + ([lines 1–41](https://github.com/golang/go/blob/c19862e5f8415b4f24b189d065ed739517c548ba/src/cmd/go/testdata/script/test_goroot_PATH.txt#L1-L41)). + +Those source rules and official assertions are behavior directly implemented +or asserted by pinned Go. Using the selected WW driver's sibling directory as +the local toolchain-bin analogue is derived from that implementation: WW has +no `GOROOT`, and that directory already supplies the driver's default compiler, +assembler, linker, and package-test coordinator. Applying the rule to WW's raw +single-file compatibility route is also derived; it is a local input extension, +but it executes the same observable test and initialization code. Canonical +absolute spelling is runtime metadata only and prevents a relative driver path +from leaking the coordinator's later package cwd into `PATH`. + +#### Fresh four-axis audit and direct pre-fix measurements + +The bounded audit considered all four permanent axes and selected only this +test-runtime gap: + +- **Build:** pinned `runBuild` loads all roots and checks package errors before + output/action construction + ([`cmd/go/internal/work/build.go`, lines 459–478](https://github.com/golang/go/blob/c19862e5f8415b4f24b189d065ed739517c548ba/src/cmd/go/internal/work/build.go#L459-L478)); official + `build_json.txt` distinguishes load errors from compiler failures + ([lines 15–26 and 39–45](https://github.com/golang/go/blob/c19862e5f8415b4f24b189d065ed739517c548ba/src/cmd/go/testdata/script/build_json.txt#L15-L45)). + A direct missing-import probe made both WW stages exit `1` with the identical + source diagnostic and no producer or public output. This candidate was + aligned. +- **Test:** direct Cstage and WWstage directory probes, each invoked with + `PATH=/usr/bin:/bin`, both exited `0` while the test printed exactly that + unchanged value. `/home/kimchi/src/ww/out/bin` was absent. These are directly + measured pre-fix WW facts and establish the selected external difference. +- **Package:** pinned `types2.(*Checker).initFiles` rejects package name `_` + ([`cmd/compile/internal/types2/check.go`, lines 311–355](https://github.com/golang/go/blob/c19862e5f8415b4f24b189d065ed739517c548ba/src/cmd/compile/internal/types2/check.go#L311-L355)), + with official anchors in + [`internal/types/testdata/check/blank.go`](https://github.com/golang/go/blob/c19862e5f8415b4f24b189d065ed739517c548ba/src/internal/types/testdata/check/blank.go#L1-L5) + and [`test/blank1.go`](https://github.com/golang/go/blob/c19862e5f8415b4f24b189d065ed739517c548ba/test/blank1.go#L1-L10). + Direct production, imported, and test-only probes were rejected identically + by both WW stages. This candidate was aligned. +- **Import:** pinned `unusedImports` requires every nonblank import binding to + be used + ([`cmd/compile/internal/types2/resolver.go`, lines 706–740](https://github.com/golang/go/blob/c19862e5f8415b4f24b189d065ed739517c548ba/src/cmd/compile/internal/types2/resolver.go#L706-L740)); official + `importdecl0` covers default, alias, dot, and blank forms + ([lines 5–31](https://github.com/golang/go/blob/c19862e5f8415b4f24b189d065ed739517c548ba/src/internal/types/testdata/check/importdecl0/importdecl0a.go#L5-L31)). + A direct unused default-import probe produced identical Cstage/WWstage + diagnostics and no committed work. This candidate was aligned. + +The build, package, and import observations above are directly measured WW +behavior; the linked rules are behavior directly implemented or asserted by +pinned Go. The conclusion that this slice crosses those axes only when code in +a successfully loaded test variant or initialized dependency observes `PATH` +is derived from the pinned launch placement. + +#### Ownership, launch behavior, and preserved boundaries + +The environment is synthesized at the three true test-process launch owners: +`internal/wwpackage.pkgstartrun` for directory products, Cstage +`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. + +The rule covers internal, external, and combined directory products; +dependency initialization; filters and list mode; running retained tests; and +raw single-file tests. It is independently materialized for each concurrent +product. `ww build`, `ww run`, compiler/assembler/linker and generated-main +commands, compile-only and assembly-only tests, no-test products, rejected +requests, and later direct execution of a retained binary receive no test +environment transformation. + +Loading, graph construction, compilation, assembly, linking, action keys, and +artifact production are unchanged. A load, compile, or link failure starts no +test process, so runtime `PATH` is inapplicable and the existing diagnostic +precedence remains. A started test observes the new environment before normal +success, assertion failure, signal, timeout, or interruption. Existing process +groups, cancellation, output capture, and cleanup own those outcomes; the +environment adds no global mutable state. A running retained request remains +private build, private run, then guarded install, so any unsuccessful run +publishes nothing and preserves prior bytes. Parallel products receive separate +environment arrays and retain existing result isolation. + +Canonical physical driver directories do not become package, import, graph, +action, artifact, symbol, `.wwi`, publication, or persistence identity. +Compiled units, interfaces, archives, executables, modes, diagnostics, and +public-output disposition are unchanged. No stored key or byte changed, so +build workdir format remains `18`, test workdir format remains `19`, and +semantic storage format remains `3`. + +The extended WW-native `directory_test_execution_working_directory` observer +proves Cstage/WWstage equality for absent, empty, nonempty, and duplicate +caller `PATH`; all directory test shapes and dependency initialization; +filters, listing, raw single-file, and running retained tests; relative selected +driver canonicalization; build/no-test/compile-only/rejection exclusions; +unchanged compiler, assembler, and linker environments; concurrent products; +runtime failure with prior retained-byte rollback; artifact-byte identity; and +transaction, stage, workdir, and generated-file cleanup. Existing signal, +timeout, interruption, concurrent-driver, and public-output transaction owners +cover the unchanged mechanisms at those boundaries. + ## 12. Candidate architectures and hard-gate decision Five candidates were developed as coherent systems, not as feature bins. diff --git a/docs/spec.md b/docs/spec.md index df62ea14..e514fd59 100644 --- a/docs/spec.md +++ b/docs/spec.md @@ -757,6 +757,17 @@ remaining separate actions. Reachable dependency initialization consequently observes the tested product's directory; a dependency tested as its own product observes its own directory. +Every test binary actually started by `ww test` receives one effective uppercase +`PATH` whose first element is the canonical absolute directory of the selected +WW driver. An absent or empty caller `PATH` yields only that directory; a +nonempty effective caller value follows it after `:`. Normal duplicate `PATH=` +entries collapse to that one value, using the caller's first effective value as +the suffix. This applies to directory products, their dependency initialization, +filters and list mode, running retained tests, and the raw single-file +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. + 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 @@ -781,9 +792,11 @@ Relative ordinary files, `testdata`, and writes resolve there for every executing filter or list path. `ww build`, directory `ww test -c` (including `-c -o`), and a no-selected-test directory execute no test child and receive no execution-directory effect. A published test binary invoked directly, and the -raw single-file compatibility route, inherit the user's invocation cwd, -environment, and three standard descriptors; no package directory, input, or -output policy is embedded or forced by the binary. +raw single-file compatibility route, inherit the user's invocation cwd and +three standard descriptors; no package directory, input, or output policy is +embedded or forced by the binary. Directly invoked retained binaries inherit the +entire caller environment. Raw single-file tests preserve every caller +environment field except the test-only `PATH` transformation above. --- diff --git a/docs/test-system-v2.md b/docs/test-system-v2.md index de75926f..13f4cbe8 100644 --- a/docs/test-system-v2.md +++ b/docs/test-system-v2.md @@ -291,6 +291,18 @@ creates a separate process using the dependency directory. Equivalent direct, recursive, redundant, absolute, and root-symlink spellings converge before this runtime field is assigned. +Every test binary actually executed by either driver stage also receives one +effective uppercase `PATH` beginning with the canonical absolute directory of +the selected WW driver. Missing and empty caller values produce only that +directory; a nonempty first effective caller value follows it after `:`. +Normal duplicate `PATH=` entries collapse to the one child value. Directory +products allocate this environment independently per concurrent child; the raw +single-file route performs the same PATH transformation while retaining its +caller cwd, `PWD`, stdin, and split streams. Running retained tests transform +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. + 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 @@ -312,9 +324,10 @@ linker, support, and generated-main commands retain their build-plan cwd and environment. Captured directory build plans begin with null stdin, which their inherited-stdio tool descendants retain. `ww build`, `ww test -c`, `-c -o`, and no-selected-test products start no test child. A published test binary run -directly and the raw single-file compatibility path inherit the user's -cwd/environment/stdin and contain no forced package-directory or input -behavior. +directly inherits the user's cwd/environment/stdin. The raw single-file +compatibility path inherits caller cwd/stdin and all unrelated environment +entries, with only the test-PATH transformation above; neither route embeds a +package directory or input behavior in the binary. Before package grouping, both directory drivers and the shared recursive coordinator apply Go 1.26.5's filename OS/architecture rule for WW's fixed @@ -401,17 +414,20 @@ The same package owner contains the focused independent temporary directories from an unrelated caller cwd and compares Cstage/WWstage output for production/internal/external/combined and every test-only shape; production and test-only dependency initialization; -recompiled external self-import; duplicate inherited `PWD`; ordinary data, +recompiled external self-import; duplicate inherited `PWD`; absent, empty, +nonempty, and duplicate inherited `PATH`; ordinary data, `testdata`, relative writes, and deliberately nonempty caller stdin; direct/recursive/redundant/absolute/symlink roots; reversed request and creation order; serial and parallel products; filters, list, and no-match execution; -failure and timeout; build/no-test and compile-only paths; direct published -binaries and raw single-file execution; build-tool cwd/argv/environment/stdin; +failure and timeout; build/no-test and compile-only paths; running retained +success and failed-run rollback; direct published binaries and raw single-file +execution; build-tool cwd/argv/environment/stdin; persistent data-only reuse and artifact bytes; input-open failure before capture creation; source-class rejection with empty workdirs; and deterministic post-build child-`chdir` failure isolated from a successful sibling. It pads the inherited environment beyond former fixed observer sizes, requires one appended -product `PWD`, requires EOF for captured actions and caller data for inherited- +product `PWD`, one toolchain-first child `PATH`, requires EOF for captured +actions and caller data for inherited- stdio routes, alternates fd-1/fd-2 write syscalls through every executing variant and initializer, requires one ordered stdout stream on success, assertion failure, signal, timeout, and setup failure, requires direct/raw @@ -520,11 +536,12 @@ real directory test products always run, while no-selected-test directories do not create a process. The byte-identity and bootstrap gates keep building on fresh scratch. `make clean` reclaims every workdir under `out/`. -Directory-product cwd and `PWD` are request-time child-process metadata, not -persisted action inputs or results. A runtime directory-entry failure therefore -does not erase a successfully committed compilation generation. Changing only -ordinary fixture data causes no unit/export/assembly/object/archive/main/binary -change and no producer work beyond the same established warm final relink, +Directory-product cwd, `PWD`, and test `PATH` are request-time child-process +metadata, not persisted action inputs or results. A runtime directory-entry +failure therefore does not erase a successfully committed compilation +generation. Changing only ordinary fixture data causes no +unit/export/assembly/object/archive/main/binary change and no producer work +beyond the same established warm final relink, while the next always-run product observes the new bytes. Build workdir format remains 18, test workdir format remains 19, and semantic storage remains 3. diff --git a/internal/wwpackage/package.ww b/internal/wwpackage/package.ww index 66c722be..030b2fec 100644 --- a/internal/wwpackage/package.ww +++ b/internal/wwpackage/package.ww @@ -268,17 +268,27 @@ fn pkgfreestrs(values: []str) void = { }; }; -// Go's AppendPWD appends, then os/exec keeps the last duplicate value. WW's -// executor preserves duplicates, so discard earlier exact PWD entries here. -fn runenv(tmpdir: str, pwd: str, out: *[]str, tmpowned: *str, - pwdowned: *str) bool = { +// 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 = { + let toolbin: str; + let oom: bool = false; + if (!pkgcanonicaldir(pkgdirname(builder), &toolbin, &oom)) { + if (!oom) { + pkgputln(os.STDERR_FILENO, + "wwtest package: cannot determine toolchain directory"); + }; + return false; + }; let inherited: []str = os.getenvs(); - if (inherited.len > PKG_COUNT_MAX - 3) { + if (inherited.len > PKG_COUNT_MAX - 4) { pkgputln(os.STDERR_FILENO, "wwtest package: package graph is too large"); return false; }; - let allocation: ([]str | nomem) = pkgallocstrs(inherited.len + 3); + let allocation: ([]str | nomem) = pkgallocstrs(inherited.len + 4); let env: []str; match (allocation) { case let value: []str => env = value; @@ -291,6 +301,7 @@ fn runenv(tmpdir: str, pwd: str, out: *[]str, tmpowned: *str, 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=")) { append(env, inherited[i]); }; @@ -302,22 +313,44 @@ fn runenv(tmpdir: str, pwd: str, out: *[]str, tmpowned: *str, pkgfreestrs(env); return false; }; - let pwdenv: str; - if (!pkgstring(&pwdenv, "PWD=", pwd)) { + let pathenv: str; + let pathok: bool = false; + match (os.getenv("PATH")) { + case let inheritedpath: str => { + if (inheritedpath.len == 0) { + pathok = pkgstring(&pathenv, "PATH=", toolbin); + } else { + pathok = pkgstring(&pathenv, "PATH=", toolbin, ":", + inheritedpath); + }; + }; + 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, pwdowned: str) void = { +fn freerunenv(env: []str, tmpowned: str, pathowned: str, pwdowned: str) void = { pkgfreeownedstr(tmpowned); + pkgfreeownedstr(pathowned); pkgfreeownedstr(pwdowned); pkgfreestrs(env); }; @@ -1240,8 +1273,8 @@ fn pkgmakedir(path: str) bool = { }; // Resolve a directory to the kernel's symlink-free absolute spelling. The -// coordinator is single-threaded while planning, so the temporary cwd change -// cannot race a child process launch. +// coordinator is single-threaded; each temporary cwd change is restored before +// it plans further paths or launches another child. fn pkgcanonicaldir(path: str, out: *str, oom: *bool) bool = { *oom = false; let beforeallocation: ([]u8 | nomem) = pkgallocbytes(os.PATH_MAX); @@ -1553,7 +1586,7 @@ fn pkgstartbuild(p: *pkgplan, groups: []pkggroup, builder: str, includes: []str, }; fn pkgstartrun(g: *pkggroup, filters: []str, timeoutarg: str, - list: bool, h: *exec.process) bool = { + list: bool, builder: str, h: *exec.process) bool = { if (filters.len > PKG_COUNT_MAX - 4) { pkgputln(os.STDERR_FILENO, "wwtest package: package graph is too large"); @@ -1581,8 +1614,10 @@ fn pkgstartrun(g: *pkggroup, filters: []str, timeoutarg: str, 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, &env, &tmpowned, &pwdowned)) { + if (!runenv(g.root, g.dir, builder, &env, &tmpowned, + &pathowned, &pwdowned)) { pkgfreeownedstr(packagearg); pkgfreestrs(ra); return false; @@ -1598,7 +1633,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, pwdowned); + freerunenv(env, tmpowned, pathowned, pwdowned); pkgfreeownedstr(packagearg); pkgfreestrs(ra); return true; @@ -2561,7 +2596,7 @@ export fn packagecommand(args: []str) int = { productcompleted += 1; } else { if (!pkgstartrun(g, filters, timeoutarg, list, - &runhandles[gi])) { + builder, &runhandles[gi])) { g.runstartfailed = true; g.state = PKGDONE; productcompleted += 1; diff --git a/selfhost/cmd/ww/main.ww b/selfhost/cmd/ww/main.ww index 743f2956..aca243d4 100644 --- a/selfhost/cmd/ww/main.ww +++ b/selfhost/cmd/ww/main.ww @@ -1632,6 +1632,88 @@ fn sepallocptrs(cap: i32) ([]*u8 | nomem) = { return value; }; +type septestenv = struct { + values: []str, + path: str, +}; + +// Pinned cmd/go gives each test binary PATH=$GOROOT/bin:$PATH. The selected +// WW driver's sibling directory is the local toolchain-bin analogue. Remove +// normal inherited PATH duplicates because lib/os/exec deliberately preserves +// the concrete environment array while Go's os/exec keeps the appended value. +fn sepmaketestenv(toolbin: str, out: *septestenv) bool = { + let inherited: []str = os.getenvs(); + if (inherited.len == SEP_COUNT_MAX) { + cerr("ww: cannot prepare test environment\n"); + return false; + }; + let oldpath: str = ""; + match (os.getenv("PATH")) { + case let value: str => oldpath = value; + case void => void; + }; + let total: i64 = 5i64 + (toolbin.len: i64); + if (oldpath.len != 0) { total += 1i64 + (oldpath.len: i64); }; + if (total > SEP_COUNT_MAX: i64) { + cerr("ww: cannot prepare test environment\n"); + return false; + }; + let pathallocation: ([]u8 | nomem) = sepallocbytes(total: i32); + let pathbytes: []u8; + match (pathallocation) { + case let value: []u8 => pathbytes = value; + case nomem => { + cerr("ww: cannot prepare test environment\n"); + return false; + }; + }; + let prefix: str = "PATH="; + let i: i32 = 0; + for (i < prefix.len) { append(pathbytes, prefix[i]); i += 1; }; + i = 0; + for (i < toolbin.len) { append(pathbytes, toolbin[i]); i += 1; }; + if (oldpath.len != 0) { + append(pathbytes, ':'); + i = 0; + for (i < oldpath.len) { append(pathbytes, oldpath[i]); i += 1; }; + }; + let pathenv: str = strings.frombytes(pathbytes); + let envallocation: ([]str | nomem) = sepallocstrs(inherited.len + 1); + let env: []str; + match (envallocation) { + case let value: []str => env = value; + case nomem => { + os.free(pathenv.ptr: *void, pathenv.len: u64); + cerr("ww: cannot prepare test environment\n"); + return false; + }; + }; + i = 0; + let inserted: bool = false; + for (i < inherited.len) { + if (strings.hasprefix(inherited[i], "PATH=")) { + if (!inserted) { append(env, pathenv); inserted = true; }; + } else { + append(env, inherited[i]); + }; + i += 1; + }; + if (!inserted) { append(env, pathenv); }; + out.values = env; + out.path = pathenv; + return true; +}; + +fn sepfreetestenv(env: *septestenv) void = { + if (env.path.ptr != nil && env.path.len != 0) { + os.free(env.path.ptr: *void, env.path.len: u64); + }; + if (env.values.ptr != nil && env.values.cap != 0) { + os.free(env.values.ptr: *void, + (env.values.cap: u64) * (size(str): u64)); + }; +}; + fn sepallocgraph(pkg: []seppkg, context: []sepcontext) (*sepgraph | nomem) = { let emptyfolds: []sepfoldentry; let value: *sepgraph = alloc(sepgraph{ @@ -9186,21 +9268,30 @@ fn runsingletest(selfdir: *u8, src: *u8, incs: *u8, compileonly: i32, if (pattern != nil) { append(execargv, pathstr(pattern)); }; - let env: []str = os.getenvs(); let result: exec.result; - exec.runstdio(pathstr(outp), execargv, env, &result); let rc: i32 = 1; - if (result.termination == exec.termination.EXIT) { - rc = result.code; - } else { if (result.termination == exec.termination.ERROR) { - if (result.code == 127) { - cerr("ww: execve failed\n"); - rc = 127; - } else { - cerr("ww: process launch/wait failed\n"); - rc = -1; - }; + let env: septestenv; + let toolbin: *u8 = canonicaldir(pathstr(selfdir)); + if (toolbin == nil) { + cerr("ww: cannot prepare test environment\n"); + } else { if (sepmaketestenv(pathstr(toolbin), &env)) { + exec.runstdio(pathstr(outp), execargv, env.values, &result); + sepfreetestenv(&env); + if (result.termination == exec.termination.EXIT) { + rc = result.code; + } else { if (result.termination == exec.termination.ERROR) { + if (result.code == 127) { + cerr("ww: execve failed\n"); + rc = 127; + } else { + cerr("ww: process launch/wait failed\n"); + rc = -1; + }; + }; }; }; }; + if (toolbin != nil) { + os.free(toolbin: *void, cstrlen(toolbin) + 1u64); + }; if (rc == 0 && deferredinstall && sepinstalltestoutput(outp, outstem) != 0) { rc = 1; }; if (owntmp) { diff --git a/test/package/package_test.ww b/test/package/package_test.ww index 6cabaca8..bb09fab0 100644 --- a/test/package/package_test.ww +++ b/test/package/package_test.ww @@ -633,10 +633,11 @@ fn hexbytes(value: str) str = { fn cwdtestenv(stage: str) []str = { let inherited: []str = os.getenvs(); - let env: []str = alloc([], (inherited.len + 8): u64)!; + let env: []str = alloc([], (inherited.len + 10): u64)!; let i: i32 = 0; for (i < inherited.len) { if (!strings.hasprefix(inherited[i], "PWD=") + && !strings.hasprefix(inherited[i], "PATH=") && !strings.hasprefix(inherited[i], "WW_W6C=") && !strings.hasprefix(inherited[i], "WW_W6A=") && !strings.hasprefix(inherited[i], "WW_W6L=") @@ -646,6 +647,7 @@ fn cwdtestenv(stage: str) []str = { }; i += 1; }; + append(env, "PATH=/ww-path-inherited-first"); append(env, "PWD=/ww-cwd-inherited-first"); if (same(stage, "ww")) { append(env, strings.concat("WW_W6C=", driver("w6c"))); @@ -658,10 +660,25 @@ fn cwdtestenv(stage: str) []str = { append(env, strings.concat("WW_W6L=", driver("w6l_ww"))); }; append(env, strings.concat("WW_SRCLIB=", repo(), "/lib")); + append(env, "PATH=/ww-path-inherited-last"); append(env, "PWD=/ww-cwd-inherited-last"); return env; }; +fn cwdassertpath(text: str, label: str, path: str, count: str) void = { + assert(has(text, strings.concat(label, " PATH=", path, "\n"))); + assert(has(text, strings.concat(label, " PATH-count=", count, "\n"))); +}; + +fn cwdasserttestpath(text: str, label: str) void = { + cwdassertpath(text, label, strings.concat(repo(), + "/out/bin:/ww-path-inherited-first"), "1"); +}; + +fn cwdassertcallerpath(text: str, label: str) void = { + cwdassertpath(text, label, "/ww-path-inherited-first", "2"); +}; + fn cwdassertrecord(text: str, label: str, cwd: str, pwd: str, pwdcount: str, pwdlast: str, data: str, testdata: str, input: str) void = { @@ -707,6 +724,7 @@ fn cwdassertpackage(text: str, label: str, dir: str, data: str, testdata: str) void = { cwdassertrecord(text, label, dir, dir, "1", "yes", data, testdata, "eof"); + cwdasserttestpath(text, label); cwdassertmergedstreams(text, label, dir); }; @@ -883,27 +901,17 @@ fn cwdwritedata(dir: str, label: str) void = { " let body: str; body.ptr = bytes.ptr; body.len = bytes.len; put(body);\n", " 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(\"\");\n", - " }; put(\"\\n\");\n", - " let env: []str = os.getenvs(); let count: 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", - " i += 1;\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(\"\");\n }; put(\"\\n\");\n put(label); put(\" PATH=\");\n match (os.getenv(\"PATH\")) {\n case let value: str => put(value); case void => put(\"\");\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", " 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(\"\\n\"); };\n };\n", @@ -1051,6 +1059,46 @@ fn cwdwritedata(dir: str, label: str) void = { assert(same(readfile(strings.concat(caller, "/sentinel")), "parent-sentinel\n")); + // The official Go script exercises both an absent and an explicitly empty + // caller PATH. Each produces only the selected toolchain directory, and the + // two stages expose identical process bytes. + assert(os.remove(strings.concat(p4, "/created.txt")) == 0); + let absentbase: []str = cwdtestenv("ww"); + let absentenv: []str = alloc([], absentbase.len: u64)!; + i = 0; + for (i < absentbase.len) { + if (!strings.hasprefix(absentbase[i], "PATH=")) { + append(absentenv, absentbase[i]); + }; + i += 1; + }; + let absentav: []str = [driver("ww"), "test", "-I", tree, p4]; + runcommandinputenvdir(root, "path-absent-c", absentav, absentenv, caller, + stdinpath, (60i64 * (time.second: i64)): time.duration, &outc); + expectexit(&outc, 0); + cwdassertrecord(outc.stdout, "p4-internal-only", p4, p4, "1", "yes", + "p4-data", "p4-testdata", "eof"); + cwdassertpath(outc.stdout, "p4-internal-only", + strings.concat(repo(), "/out/bin"), "1"); + assert(os.remove(strings.concat(p4, "/created.txt")) == 0); + let emptybase: []str = cwdtestenv("ww_ww"); + let emptyenv: []str = alloc([], (emptybase.len + 1): u64)!; + i = 0; + for (i < emptybase.len) { + if (!strings.hasprefix(emptybase[i], "PATH=")) { + append(emptyenv, emptybase[i]); + }; + i += 1; + }; + append(emptyenv, "PATH="); + let emptyav: []str = [driver("ww_ww"), "test", "-I", tree, p4]; + runcommandinputenvdir(root, "path-empty-ww", emptyav, emptyenv, caller, + stdinpath, (60i64 * (time.second: i64)): time.duration, &outw); + expectexit(&outw, 0); + assert(same(outc.stdout, outw.stdout)); + assert(same(outc.stderr, outw.stderr)); + assert(os.remove(strings.concat(p4, "/created.txt")) == 0); + // Filtering, listing, and a no-match filter all execute the same product // binary. Package initialization therefore has the product cwd even when no // selected test body runs. @@ -1172,6 +1220,7 @@ fn cwdwritedata(dir: str, label: str) void = { assert(same(outc.stderr, outw.stderr)); assert(has(outc.stderr, "test package must match production package or _test\n")); + assert(!has(outc.stdout, " PATH=") && !has(outw.stdout, " PATH=")); assert(directoryisempty(rejectcwork)); assert(directoryisempty(rejectwwork)); @@ -1186,6 +1235,7 @@ fn cwdwritedata(dir: str, label: str) void = { assert(!has(out.stdout, "plain-init")); assert(!has(out.stdout, "cmdno-init")); assert(!has(out.stdout, "cmdno-main")); + assert(!has(out.stdout, " PATH=")); assert(!os.exists(strings.concat(plain, "/created.txt"))); assert(!os.exists(strings.concat(cmdno, "/created.txt"))); let commandbin: str = strings.concat(root, "/cmdno.bin"); @@ -1197,6 +1247,7 @@ fn cwdwritedata(dir: str, label: str) void = { expectexit(&out, 0); assert(os.exists(commandbin)); assert(!has(out.stdout, "cmdno-init") && !has(out.stdout, "cmdno-main")); + assert(!has(out.stdout, " PATH=")); assert(!os.exists(strings.concat(cmdno, "/created.txt"))); // Compile-only paths do not run. A published binary has no embedded cwd or @@ -1212,6 +1263,7 @@ fn cwdwritedata(dir: str, label: str) void = { assert(!os.exists(strings.concat(p2, "/p2.test"))); 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(os.remove(defaultbin) == 0); assert(os.remove(strings.concat(p1, "/created.txt")) == 0); @@ -1232,6 +1284,7 @@ fn cwdwritedata(dir: str, label: str) void = { assert(!os.exists(strings.concat(p1, "/created.txt"))); 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=")); let directbin: []str = [p1cbin, "-package=p1"]; runcommandinputenvdir(root, "cwd-published-direct", directbin, cenv, @@ -1244,13 +1297,16 @@ fn cwdwritedata(dir: str, label: str) void = { cwdassertrecord(out.stdout, "p1-internal", caller, "/ww-cwd-inherited-first", "2", "no", "caller-data", "caller-testdata", "data"); + cwdassertcallerpath(out.stdout, "dep-init"); + cwdassertcallerpath(out.stdout, "p1-internal"); cwdassertsplitstreams(out.stdout, out.stderr, "dep-init", caller); cwdassertsplitstreams(out.stdout, out.stderr, "p1-internal", caller); assert(os.exists(strings.concat(caller, "/created.txt"))); assert(os.remove(strings.concat(caller, "/created.txt")) == 0); - // Raw single-file compatibility is deliberately outside the directory - // coordinator rule and retains its caller execution context. + // Raw single-file compatibility retains its caller cwd/PWD/stdin context, + // but it is still a test process and therefore receives the selected + // toolchain directory first in PATH. let rawc: []str = [driver("ww"), "test", "-I", tree, strings.concat(p4, "/internal_test.ww")]; let raww: []str = [driver("ww_ww"), "test", "-I", tree, @@ -1269,9 +1325,52 @@ 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"); + cwdasserttestpath(outc.stdout, "p4-internal-only"); cwdassertsplitstreams(outc.stdout, outc.stderr, "p4-internal-only", caller); assert(os.remove(strings.concat(caller, "/created.txt")) == 0); + + // A retained running test receives the same environment before guarded + // install. A later runtime failure observes PATH but cannot replace the + // successful binary; both stages preserve identical prior bytes. + let retained: str = strings.concat(root, "/path-retained.test"); + let retainc: []str = [driver("ww"), "test", "-o", retained, + "-I", tree, p1]; + let retainw: []str = [driver("ww_ww"), "test", "-o", retained, + "-I", tree, p1]; + runcommandinputenvdir(root, "path-retain-c", retainc, cenv, caller, + stdinpath, (90i64 * (time.second: i64)): time.duration, &outc); + expectexit(&outc, 0); + cwdassertpackage(outc.stdout, "dep-init", p1, "p1-data", "p1-testdata"); + cwdassertpackage(outc.stdout, "p1-internal", p1, + "p1-data", "p1-testdata"); + let retainedbytes: str = strings.dup(readfile(retained)); + runcommandinputenvdir(root, "path-retain-ww", retainw, wenv, caller, + stdinpath, (90i64 * (time.second: i64)): time.duration, &outw); + expectexit(&outw, 0); + assert(same(outc.stdout, outw.stdout)); + assert(same(outc.stderr, outw.stderr)); + assert(same(retainedbytes, readfile(retained))); + let retainfailc: []str = [driver("ww"), "test", "-o", retained, + "-I", tree, "-I", failtree, p7]; + let retainfailw: []str = [driver("ww_ww"), "test", "-o", retained, + "-I", tree, "-I", failtree, p7]; + runcommandinputenvdir(root, "path-retain-fail-c", retainfailc, cenv, + caller, stdinpath, + (90i64 * (time.second: i64)): time.duration, &outc); + expectexit(&outc, 1); + cwdassertpackage(outc.stdout, "p7-fail", p7, "p7-data", "p7-testdata"); + assert(same(retainedbytes, readfile(retained))); + runcommandinputenvdir(root, "path-retain-fail-ww", retainfailw, wenv, + caller, stdinpath, + (90i64 * (time.second: i64)): time.duration, &outw); + expectexit(&outw, 1); + assert(same(outc.stdout, outw.stdout)); + assert(same(outc.stderr, outw.stderr)); + assert(same(retainedbytes, readfile(retained))); + assert(!os.exists(strings.concat(retained, ".new"))); + assert(!directoryhasfragment(root, "path-retained.test.wwtxn.")); + cwdassertpackage(multiout, "dep-init", p2, "p2-data", "p2-testdata"); cwdassertpackage(multiout, "p2-external", p2, "p2-data", "p2-testdata"); @@ -1344,8 +1443,8 @@ fn cwdwritedata(dir: str, label: str) void = { "#!/bin/sh\n", "printf 'compiler cwd=' >> \"$WW_CWD_TOOL_TRACE\"\n", "/bin/pwd >> \"$WW_CWD_TOOL_TRACE\"\n", - "printf 'compiler env=LC_ALL:%s TMPDIR:%s\\n' \"$LC_ALL\" ", - "\"$TMPDIR\" >> \"$WW_CWD_TOOL_TRACE\"\n", + "printf 'compiler env=LC_ALL:%s TMPDIR:%s PATH:%s\\n' \"$LC_ALL\" ", + "\"$TMPDIR\" \"$PATH\" >> \"$WW_CWD_TOOL_TRACE\"\n", "if IFS= read -r WW_CWD_STDIN; then\n", " printf 'compiler stdin=data\\n' >> \"$WW_CWD_TOOL_TRACE\"\n", "else printf 'compiler stdin=eof\\n' >> \"$WW_CWD_TOOL_TRACE\"; fi\n", @@ -1358,8 +1457,8 @@ fn cwdwritedata(dir: str, label: str) void = { "#!/bin/sh\n", "printf 'assembler cwd=' >> \"$WW_CWD_TOOL_TRACE\"\n", "/bin/pwd >> \"$WW_CWD_TOOL_TRACE\"\n", - "printf 'assembler env=LC_ALL:%s TMPDIR:%s\\n' \"$LC_ALL\" ", - "\"$TMPDIR\" >> \"$WW_CWD_TOOL_TRACE\"\n", + "printf 'assembler env=LC_ALL:%s TMPDIR:%s PATH:%s\\n' \"$LC_ALL\" ", + "\"$TMPDIR\" \"$PATH\" >> \"$WW_CWD_TOOL_TRACE\"\n", "if IFS= read -r WW_CWD_STDIN; then\n", " printf 'assembler stdin=data\\n' >> \"$WW_CWD_TOOL_TRACE\"\n", "else printf 'assembler stdin=eof\\n' >> \"$WW_CWD_TOOL_TRACE\"; fi\n", @@ -1372,8 +1471,8 @@ fn cwdwritedata(dir: str, label: str) void = { "#!/bin/sh\n", "printf 'linker cwd=' >> \"$WW_CWD_TOOL_TRACE\"\n", "/bin/pwd >> \"$WW_CWD_TOOL_TRACE\"\n", - "printf 'linker env=LC_ALL:%s TMPDIR:%s\\n' \"$LC_ALL\" ", - "\"$TMPDIR\" >> \"$WW_CWD_TOOL_TRACE\"\n", + "printf 'linker env=LC_ALL:%s TMPDIR:%s PATH:%s\\n' \"$LC_ALL\" ", + "\"$TMPDIR\" \"$PATH\" >> \"$WW_CWD_TOOL_TRACE\"\n", "if IFS= read -r WW_CWD_STDIN; then\n", " printf 'linker stdin=data\\n' >> \"$WW_CWD_TOOL_TRACE\"\n", "else printf 'linker stdin=eof\\n' >> \"$WW_CWD_TOOL_TRACE\"; fi\n", @@ -1417,9 +1516,14 @@ fn cwdwritedata(dir: str, label: str) void = { assert(has(coldtrace, strings.concat("assembler cwd=", caller, "\n"))); assert(has(coldtrace, strings.concat("linker cwd=", caller, "\n"))); assert(!has(coldtrace, strings.concat("cwd=", p1, "\n"))); - assert(has(coldtrace, "compiler env=LC_ALL:C TMPDIR:/tmp/")); - assert(has(coldtrace, "assembler env=LC_ALL:C TMPDIR:/tmp/")); - assert(has(coldtrace, "linker env=LC_ALL:C TMPDIR:/tmp/")); + assert(has(coldtrace, + "compiler env=LC_ALL:C TMPDIR:/tmp/")); + assert(has(coldtrace, + "assembler env=LC_ALL:C TMPDIR:/tmp/")); + assert(has(coldtrace, + "linker env=LC_ALL:C TMPDIR:/tmp/")); + assert(has(coldtrace, " PATH:/ww-path-inherited-last\n")); + assert(!has(coldtrace, strings.concat(" PATH:", repo(), "/out/bin:"))); assert(has(coldtrace, "compiler stdin=eof\n")); assert(has(coldtrace, "assembler stdin=eof\n")); assert(has(coldtrace, "linker stdin=eof\n")); @@ -1528,7 +1632,7 @@ fn cwdwritedata(dir: str, label: str) void = { "\"$WW_CWD_REAL_DRIVER\" \"$@\"\n", "status=$?\n", "if [ \"$status\" -eq 0 ]; then\n", - " mv -- \"$WW_CWD_HIDE_DIR\" \"$WW_CWD_HIDE_DIR.gone\" || exit 125\n", + " /bin/mv -- \"$WW_CWD_HIDE_DIR\" \"$WW_CWD_HIDE_DIR.gone\" || exit 125\n", " printf 'wrapper-hidden=%s\\n' \"$WW_CWD_HIDE_DIR\" >&2\n", "fi\n", "exit \"$status\"\n")); @@ -1545,7 +1649,7 @@ fn cwdwritedata(dir: str, label: str) void = { driver(hidestages[i]))); append(hideenv, strings.concat("WW_CWD_HIDE_DIR=", p1)); let hideav: []str = [driver("wwtest"), "package", "--ww-driver", - hidewrapper, "-j", "2", "-I", tree, p1, p2]; + "../build-then-hide.sh", "-j", "2", "-I", tree, p1, p2]; runcommandinputenvdir(root, strings.concat("cwd-hide-", hidestages[i]), hideav, hideenv, caller, stdinpath, (120i64 * (time.second: i64)): time.duration, &out); @@ -1554,10 +1658,16 @@ fn cwdwritedata(dir: str, label: str) void = { assert(has(out.stdout, strings.concat("FAIL ", p1, " [p1] (test harness error 2)\n"))); assert(!has(out.stdout, "p1-internal cwd=")); - cwdassertpackage(out.stdout, "dep-init", p2, - "p2-data", "p2-testdata"); - cwdassertpackage(out.stdout, "p2-external", p2, - "p2-data", "p2-testdata"); + cwdassertrecord(out.stdout, "dep-init", p2, p2, "1", "yes", + "p2-data", "p2-testdata", "eof"); + cwdassertrecord(out.stdout, "p2-external", p2, p2, "1", "yes", + "p2-data", "p2-testdata", "eof"); + cwdassertpath(out.stdout, "dep-init", strings.concat(root, + ":/ww-path-inherited-first"), "1"); + cwdassertpath(out.stdout, "p2-external", strings.concat(root, + ":/ww-path-inherited-first"), "1"); + cwdassertmergedstreams(out.stdout, "dep-init", p2); + cwdassertmergedstreams(out.stdout, "p2-external", p2); assert(os.rename(strings.concat(p1, ".gone"), p1) == 0); assert(!os.exists(strings.concat(p1, ".gone"))); hidestdout[i] = strings.dup(out.stdout);