Compare commits

...

2 Commits

Author SHA1 Message Date
Claude Bot
2bacab4e43 fix(test): address review feedback for issue 26207 test
- Skip Unix-specific shebang test on Windows using test.skipIf(isWindows)
- Replace shell execSync chmod with fs.chmodSync

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-18 02:45:10 +00:00
Claude Bot
11c56071e8 fix(run): validate NODE env path exists before using it
When NODE or npm_node_execpath environment variables point to a
non-existent file, bun should fall back to creating its own node
symlink or searching PATH for node, rather than trusting the env
var blindly.

This fixes issues where scripts with #!/usr/bin/env node shebangs
fail when using `bun run --filter` or `bun run --workspaces` if
the NODE env var is set to an invalid path.

Fixes #26207

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-18 02:34:04 +00:00
2 changed files with 131 additions and 3 deletions

View File

@@ -49,10 +49,15 @@ pub const Loader = struct {
}
pub fn getNodePath(this: *Loader, fs: *Fs.FileSystem, buf: *bun.PathBuffer) ?[:0]const u8 {
// Check NODE or npm_node_execpath env var, but only use it if the file actually exists
if (this.get("NODE") orelse this.get("npm_node_execpath")) |node| {
@memcpy(buf[0..node.len], node);
buf[node.len] = 0;
return buf[0..node.len :0];
if (node.len > 0 and node.len < bun.MAX_PATH_BYTES) {
@memcpy(buf[0..node.len], node);
buf[node.len] = 0;
if (bun.sys.isExecutableFilePath(buf[0..node.len :0])) {
return buf[0..node.len :0];
}
}
}
if (which(buf, this.get("PATH") orelse return null, fs.top_level_dir, "node")) |node| {

View File

@@ -0,0 +1,123 @@
// https://github.com/oven-sh/bun/issues/26207
// bun run --filter and --workspaces should fall back to bun's node symlink
// when NODE env var points to a non-existent path
import { expect, test } from "bun:test";
import { chmodSync } from "fs";
import { bunEnv, bunExe, isWindows, tempDirWithFiles } from "harness";
test("bun run --workspaces creates node symlink when NODE env points to non-existent path", async () => {
const dir = tempDirWithFiles("workspaces-node-fallback", {
"package.json": JSON.stringify({
name: "root",
workspaces: ["packages/*"],
}),
"packages/a/package.json": JSON.stringify({
name: "a",
scripts: {
test: "node -e \"console.log('node works')\"",
},
}),
});
// Set NODE to a non-existent path and remove system node from PATH
const env = {
...bunEnv,
NODE: "/nonexistent/path/to/node",
PATH: "/usr/bin", // PATH without node
};
const proc = Bun.spawn({
cmd: [bunExe(), "run", "--workspaces", "test"],
env,
cwd: dir,
stdout: "pipe",
stderr: "pipe",
});
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
// Should succeed because bun creates a symlink to its own node
expect(stdout).toContain("node works");
expect(exitCode).toBe(0);
});
test("bun run --filter creates node symlink when NODE env points to non-existent path", async () => {
const dir = tempDirWithFiles("filter-node-fallback", {
"package.json": JSON.stringify({
name: "root",
workspaces: ["packages/*"],
}),
"packages/a/package.json": JSON.stringify({
name: "a",
scripts: {
test: "node -e \"console.log('node works from filter')\"",
},
}),
});
// Set NODE to a non-existent path and remove system node from PATH
const env = {
...bunEnv,
NODE: "/nonexistent/path/to/node",
PATH: "/usr/bin", // PATH without node
};
const proc = Bun.spawn({
cmd: [bunExe(), "run", "--filter", "*", "test"],
env,
cwd: dir,
stdout: "pipe",
stderr: "pipe",
});
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
// Should succeed because bun creates a symlink to its own node
expect(stdout).toContain("node works from filter");
expect(exitCode).toBe(0);
});
// Skip on Windows: shebang scripts (#!/usr/bin/env node) are Unix-specific
test.skipIf(isWindows)("bun run --workspaces runs scripts that have #!/usr/bin/env node shebang", async () => {
const dir = tempDirWithFiles("workspaces-shebang", {
"package.json": JSON.stringify({
name: "root",
workspaces: ["packages/*"],
}),
"packages/a/package.json": JSON.stringify({
name: "a",
scripts: {
build: "./build.js",
},
}),
// Create an executable script with node shebang
"packages/a/build.js": "#!/usr/bin/env node\nconsole.log('build script ran');",
});
// Make the script executable
chmodSync(`${dir}/packages/a/build.js`, 0o755);
// Remove system node from PATH, and clear NODE/npm_node_execpath to avoid
// interfering with bun's node symlink creation
const env = {
...bunEnv,
NODE: undefined,
npm_node_execpath: undefined,
PATH: "/usr/bin", // PATH without node
};
const proc = Bun.spawn({
cmd: [bunExe(), "run", "--workspaces", "build"],
env,
cwd: dir,
stdout: "pipe",
stderr: "pipe",
});
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
// Should succeed because bun creates a symlink to its own node
expect(stdout).toContain("build script ran");
expect(exitCode).toBe(0);
});