src/util/which.ts — a tool's absolute path, found once
Purpose
Section titled “Purpose”A spawn of a bare name makes Bun walk PATH with a stat per entry. vx
spawned sh for every task and git for every git call, so a
200-task run on a 100-package workspace paid 2,800 stats for the same
sh (14 PATH entries) and 12 for each of five gits. Every spawn site
of a tool vx itself runs — the task shell (runner, sandbox), strace,
git — asks here and hands Bun the absolute path.
Public surface
Section titled “Public surface”export function executablePath(name: string): stringexport function shellArgv(script: string): string[]-
Resolved on vx’s OWN
process.env.PATH, never a task’s. A task’s PATH leads with its project’s and the workspace’snode_modules/.binandexec.env.definemay set it; the shell that parses the command and the git that enumerates the inputs are the machine’s tools, so a dependency shipping ashbin does not become the interpreter of every command in its project (it did: Bun resolved the bareshagainst the task’s PATH). The task’s PATH still decides what the command resolves, inside that shell. -
Memoized per PATH VALUE: one walk per tool while PATH is unchanged (nothing in vx changes it mid-run), and a new walk when it does change, so an embedder or a test that sets PATH is answered for the PATH it has.
-
A miss throws Bun’s own shape for a spawn that could not run (
code: 'ENOENT',Executable not found in $PATH: "<name>"), so a call inside a spawn site’stryreaches the sameisExecutableMissingrefusal (“vx requires git”, “Install a POSIX sh”) as before. A miss is not remembered: it ends the spawn that asked, and a long-lived process (vx watch) finds the tool once it is installed. -
The answer is absolute even for a relative PATH entry (
Bun.whichresolves one against this process’s cwd). -
shellArgvis the task shell’s argv:[sh, '-c', script], and on Windows[bun, 'exec', script], Bun’s shell, since Windows ships nosh(docs/design/windows-2026-09.md§ Decisions 1).
Not here
Section titled “Not here”The cache.inputs.runtime probe (cache/inputs.ts) still spawns a
bare sh, against vx’s ambient PATH (on Windows, shellArgv).
tests/util-which.test.ts (one walk per PATH, a PATH change heard, the
miss’s shape and that it is not remembered); tests/task-shell.test.ts
(a node_modules/.bin/sh parses no task command; $0 is still sh);
tests/syscall-repeats.unsafe.test.ts (the lookup stats of sh and
git do not grow with the number of spawns, counted by strace).