From 83f2eb2f2a11e6d7a27905c48d8716587419f8cc Mon Sep 17 00:00:00 2001 From: dave caruso Date: Sun, 31 Mar 2024 12:47:53 -0700 Subject: [PATCH 1/5] Revert "fix!: do not lookup cwd in which (#9691)" This reverts commit 4869ebff24e9bbcbc782c63c5c70153325fcf3e9. --- packages/bun-types/bun.d.ts | 3 +- src/StandaloneModuleGraph.zig | 1 + src/bun.js/api/BunObject.zig | 8 +++++ src/bun.js/api/bun/subprocess.zig | 4 +-- src/cli/bunx_command.zig | 6 ++++ src/cli/create_command.zig | 4 +-- src/cli/install_completions_command.zig | 2 +- src/cli/run_command.zig | 12 +++---- src/cli/upgrade_command.zig | 2 +- src/env_loader.zig | 13 ++++---- src/install/install.zig | 4 +-- src/install/lifecycle_script_runner.zig | 2 +- src/open.zig | 18 +++++------ src/shell/interpreter.zig | 6 ++-- src/which.zig | 34 +++++++++++++------ test/js/bun/util/which.test.ts | 43 ++++++++----------------- 16 files changed, 88 insertions(+), 74 deletions(-) diff --git a/packages/bun-types/bun.d.ts b/packages/bun-types/bun.d.ts index 2bb70f39716e4c..418c7f3b6cbeb8 100644 --- a/packages/bun-types/bun.d.ts +++ b/packages/bun-types/bun.d.ts @@ -43,8 +43,9 @@ declare module "bun" { * * @param {string} command The name of the executable or script * @param {string} options.PATH Overrides the PATH environment variable + * @param {string} options.cwd Limits the search to a particular directory in which to searc */ - function which(command: string, options?: { PATH?: string }): string | null; + function which(command: string, options?: { PATH?: string; cwd?: string }): string | null; /** * Get the column count of a string as it would be displayed in a terminal. diff --git a/src/StandaloneModuleGraph.zig b/src/StandaloneModuleGraph.zig index d658f771cfb643..a33aaa725a5915 100644 --- a/src/StandaloneModuleGraph.zig +++ b/src/StandaloneModuleGraph.zig @@ -716,6 +716,7 @@ pub const StandaloneModuleGraph = struct { if (bun.which( &whichbuf, bun.getenvZ("PATH") orelse return error.FileNotFound, + "", bun.argv()[0], )) |path| { return bun.toFD((try std.fs.cwd().openFileZ(path, .{})).handle); diff --git a/src/bun.js/api/BunObject.zig b/src/bun.js/api/BunObject.zig index f507e53b19113c..5ccec9a2474fa0 100644 --- a/src/bun.js/api/BunObject.zig +++ b/src/bun.js/api/BunObject.zig @@ -563,18 +563,26 @@ pub fn which( path_str = ZigString.Slice.fromUTF8NeverFree( globalThis.bunVM().bundler.env.get("PATH") orelse "", ); + cwd_str = ZigString.Slice.fromUTF8NeverFree( + globalThis.bunVM().bundler.fs.top_level_dir, + ); if (arguments.nextEat()) |arg| { if (!arg.isEmptyOrUndefinedOrNull() and arg.isObject()) { if (arg.get(globalThis, "PATH")) |str_| { path_str = str_.toSlice(globalThis, globalThis.bunVM().allocator); } + + if (arg.get(globalThis, "cwd")) |str_| { + cwd_str = str_.toSlice(globalThis, globalThis.bunVM().allocator); + } } } if (Which.which( &path_buf, path_str.slice(), + cwd_str.slice(), bin_str.slice(), )) |bin_path| { return ZigString.init(bin_path).withEncoding().toValueGC(globalThis); diff --git a/src/bun.js/api/bun/subprocess.zig b/src/bun.js/api/bun/subprocess.zig index ea7f9ec8087381..f4aa21aae93639 100644 --- a/src/bun.js/api/bun/subprocess.zig +++ b/src/bun.js/api/bun/subprocess.zig @@ -1642,7 +1642,7 @@ pub const Subprocess = struct { if (argv0 == null) { var path_buf: [bun.MAX_PATH_BYTES]u8 = undefined; - const resolved = Which.which(&path_buf, PATH, arg0.slice()) orelse { + const resolved = Which.which(&path_buf, PATH, cwd, arg0.slice()) orelse { globalThis.throwInvalidArguments("Executable not found in $PATH: \"{s}\"", .{arg0.slice()}); return .zero; }; @@ -1652,7 +1652,7 @@ pub const Subprocess = struct { }; } else { var path_buf: [bun.MAX_PATH_BYTES]u8 = undefined; - const resolved = Which.which(&path_buf, PATH, bun.sliceTo(argv0.?, 0)) orelse { + const resolved = Which.which(&path_buf, PATH, cwd, bun.sliceTo(argv0.?, 0)) orelse { globalThis.throwInvalidArguments("Executable not found in $PATH: \"{s}\"", .{arg0.slice()}); return .zero; }; diff --git a/src/cli/bunx_command.zig b/src/cli/bunx_command.zig index c77495c8eceb19..cc277bd8915cde 100644 --- a/src/cli/bunx_command.zig +++ b/src/cli/bunx_command.zig @@ -449,6 +449,7 @@ pub const BunxCommand = struct { destination_ = bun.which( &path_buf, PATH_FOR_BIN_DIRS, + if (ignore_cwd.len > 0) "" else this_bundler.fs.top_level_dir, initial_bin_name, ); } @@ -459,6 +460,7 @@ pub const BunxCommand = struct { if (destination_ orelse bun.which( &path_buf, bunx_cache_dir, + if (ignore_cwd.len > 0) "" else this_bundler.fs.top_level_dir, absolute_in_cache_dir, )) |destination| { const out = bun.asByteSlice(destination); @@ -529,6 +531,7 @@ pub const BunxCommand = struct { destination_ = bun.which( &path_buf, bunx_cache_dir, + if (ignore_cwd.len > 0) "" else this_bundler.fs.top_level_dir, package_name_for_bin, ); } @@ -536,6 +539,7 @@ pub const BunxCommand = struct { if (destination_ orelse bun.which( &path_buf, bunx_cache_dir, + if (ignore_cwd.len > 0) "" else this_bundler.fs.top_level_dir, absolute_in_cache_dir, )) |destination| { const out = bun.asByteSlice(destination); @@ -651,6 +655,7 @@ pub const BunxCommand = struct { if (bun.which( &path_buf, bunx_cache_dir, + if (ignore_cwd.len > 0) "" else this_bundler.fs.top_level_dir, absolute_in_cache_dir, )) |destination| { const out = bun.asByteSlice(destination); @@ -675,6 +680,7 @@ pub const BunxCommand = struct { if (bun.which( &path_buf, bunx_cache_dir, + if (ignore_cwd.len > 0) "" else this_bundler.fs.top_level_dir, absolute_in_cache_dir, )) |destination| { const out = bun.asByteSlice(destination); diff --git a/src/cli/create_command.zig b/src/cli/create_command.zig index 98ebac6a2eb745..40d0cada3af16a 100644 --- a/src/cli/create_command.zig +++ b/src/cli/create_command.zig @@ -1564,7 +1564,7 @@ pub const CreateCommand = struct { Output.flush(); if (create_options.open) { - if (which(&bun_path_buf, PATH, "bun")) |bin| { + if (which(&bun_path_buf, PATH, destination, "bun")) |bin| { var argv = [_]string{bun.asByteSlice(bin)}; var child = std.ChildProcess.init(&argv, ctx.allocator); child.cwd = destination; @@ -2289,7 +2289,7 @@ const GitHandler = struct { // Time (mean ± σ): 306.7 ms ± 6.1 ms [User: 31.7 ms, System: 269.8 ms] // Range (min … max): 299.5 ms … 318.8 ms 10 runs - if (which(&bun_path_buf, PATH, "git")) |git| { + if (which(&bun_path_buf, PATH, destination, "git")) |git| { const git_commands = .{ &[_]string{ git, "init", "--quiet" }, &[_]string{ git, "add", destination, "--ignore-errors" }, diff --git a/src/cli/install_completions_command.zig b/src/cli/install_completions_command.zig index d10d2f0874f6db..3611ad32a21dda 100644 --- a/src/cli/install_completions_command.zig +++ b/src/cli/install_completions_command.zig @@ -50,7 +50,7 @@ pub const InstallCompletionsCommand = struct { var buf: [bun.MAX_PATH_BYTES]u8 = undefined; // don't install it if it's already there - if (bun.which(&buf, bun.getenvZ("PATH") orelse cwd, bunx_name) != null) + if (bun.which(&buf, bun.getenvZ("PATH") orelse cwd, cwd, bunx_name) != null) return; // first try installing the symlink into the same directory as the bun executable diff --git a/src/cli/run_command.zig b/src/cli/run_command.zig index 62ca3fb15703d9..8e6984d6df45d7 100644 --- a/src/cli/run_command.zig +++ b/src/cli/run_command.zig @@ -64,13 +64,13 @@ pub const RunCommand = struct { "zsh", }; - fn findShellImpl(PATH: string) ?stringZ { + fn findShellImpl(PATH: string, cwd: string) ?stringZ { if (comptime Environment.isWindows) { return "C:\\Windows\\System32\\cmd.exe"; } inline for (shells_to_search) |shell| { - if (which(&path_buf, PATH, shell)) |shell_| { + if (which(&path_buf, PATH, cwd, shell)) |shell_| { return shell_; } } @@ -101,7 +101,7 @@ pub const RunCommand = struct { /// Find the "best" shell to use /// Cached to only run once - pub fn findShell(PATH: string) ?stringZ { + pub fn findShell(PATH: string, cwd: string) ?stringZ { const bufs = struct { pub var shell_buf_once: [bun.MAX_PATH_BYTES]u8 = undefined; pub var found_shell: [:0]const u8 = ""; @@ -110,7 +110,7 @@ pub const RunCommand = struct { return bufs.found_shell; } - if (findShellImpl(PATH)) |found| { + if (findShellImpl(PATH, cwd)) |found| { if (found.len < bufs.shell_buf_once.len) { @memcpy(bufs.shell_buf_once[0..found.len], found); bufs.shell_buf_once[found.len] = 0; @@ -275,7 +275,7 @@ pub const RunCommand = struct { silent: bool, use_system_shell: bool, ) !bool { - const shell_bin = findShell(env.get("PATH") orelse "") orelse return error.MissingShell; + const shell_bin = findShell(env.get("PATH") orelse "", cwd) orelse return error.MissingShell; const script = original_script; var copy_script = try std.ArrayList(u8).initCapacity(allocator, script.len); @@ -1587,7 +1587,7 @@ pub const RunCommand = struct { } if (path_for_which.len > 0) { - if (which(&path_buf, path_for_which, script_name_to_search)) |destination| { + if (which(&path_buf, path_for_which, this_bundler.fs.top_level_dir, script_name_to_search)) |destination| { const out = bun.asByteSlice(destination); return try runBinaryWithoutBunxPath( ctx, diff --git a/src/cli/upgrade_command.zig b/src/cli/upgrade_command.zig index ee56728ea2c9d3..6ed111acab6c58 100644 --- a/src/cli/upgrade_command.zig +++ b/src/cli/upgrade_command.zig @@ -612,7 +612,7 @@ pub const UpgradeCommand = struct { } if (comptime Environment.isPosix) { - const unzip_exe = which(&unzip_path_buf, env_loader.map.get("PATH") orelse "", "unzip") orelse { + const unzip_exe = which(&unzip_path_buf, env_loader.map.get("PATH") orelse "", filesystem.top_level_dir, "unzip") orelse { save_dir.deleteFileZ(tmpname) catch {}; Output.prettyErrorln("error: Failed to locate \"unzip\" in PATH. bun upgrade needs \"unzip\" to work.", .{}); Global.exit(1); diff --git a/src/env_loader.zig b/src/env_loader.zig index 443581b887ca89..31ae661f4a2f7f 100644 --- a/src/env_loader.zig +++ b/src/env_loader.zig @@ -66,14 +66,14 @@ pub const Loader = struct { return strings.eqlComptime(env, "test"); } - pub fn getNodePath(this: *Loader, buf: *bun.PathBuffer) ?[:0]const u8 { + pub fn getNodePath(this: *Loader, fs: *Fs.FileSystem, buf: *bun.PathBuffer) ?[:0]const u8 { 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 (which(buf, this.get("PATH") orelse return null, "node")) |node| { + if (which(buf, this.get("PATH") orelse return null, fs.top_level_dir, "node")) |node| { return node; } @@ -180,15 +180,15 @@ pub const Loader = struct { var did_load_ccache_path: bool = false; - pub fn loadCCachePath(this: *Loader) void { + pub fn loadCCachePath(this: *Loader, fs: *Fs.FileSystem) void { if (did_load_ccache_path) { return; } did_load_ccache_path = true; - loadCCachePathImpl(this) catch {}; + loadCCachePathImpl(this, fs) catch {}; } - fn loadCCachePathImpl(this: *Loader) !void { + fn loadCCachePathImpl(this: *Loader, fs: *Fs.FileSystem) !void { // if they have ccache installed, put it in env variable `CMAKE_CXX_COMPILER_LAUNCHER` so // cmake can use it to hopefully speed things up @@ -196,6 +196,7 @@ pub const Loader = struct { const ccache_path = bun.which( &buf, this.get("PATH") orelse return, + fs.top_level_dir, "ccache", ) orelse ""; @@ -228,7 +229,7 @@ pub const Loader = struct { if (node_path_to_use_set_once.len > 0) { node_path_to_use = node_path_to_use_set_once; } else { - const node = this.getNodePath(&buf) orelse return false; + const node = this.getNodePath(fs, &buf) orelse return false; node_path_to_use = try fs.dirname_store.append([]const u8, bun.asByteSlice(node)); } } diff --git a/src/install/install.zig b/src/install/install.zig index 49d0dfa7804542..71a7fbf556f367 100644 --- a/src/install/install.zig +++ b/src/install/install.zig @@ -2121,11 +2121,11 @@ pub const PackageManager = struct { }; } - this.env.loadCCachePath(); + this.env.loadCCachePath(this_bundler.fs); { var node_path: [bun.MAX_PATH_BYTES]u8 = undefined; - if (this.env.getNodePath(&node_path)) |node_pathZ| { + if (this.env.getNodePath(this_bundler.fs, &node_path)) |node_pathZ| { _ = try this.env.loadNodeJSConfig(this_bundler.fs, bun.default_allocator.dupe(u8, node_pathZ) catch bun.outOfMemory()); } else brk: { const current_path = this.env.get("PATH") orelse ""; diff --git a/src/install/lifecycle_script_runner.zig b/src/install/lifecycle_script_runner.zig index 8a9183ea8e5a78..933e2d96498dc3 100644 --- a/src/install/lifecycle_script_runner.zig +++ b/src/install/lifecycle_script_runner.zig @@ -125,7 +125,7 @@ pub const LifecycleScriptSubprocess = struct { this.current_script_index = next_script_index; this.has_called_process_exit = false; - const shell_bin = bun.CLI.RunCommand.findShell(env.get("PATH") orelse "") orelse return error.MissingShell; + const shell_bin = bun.CLI.RunCommand.findShell(env.get("PATH") orelse "", cwd) orelse return error.MissingShell; var copy_script = try std.ArrayList(u8).initCapacity(manager.allocator, original_script.script.len + 1); defer copy_script.deinit(); diff --git a/src/open.zig b/src/open.zig index b4ee5f6a72d933..ea2bd6d64ca688 100644 --- a/src/open.zig +++ b/src/open.zig @@ -106,12 +106,12 @@ pub const Editor = enum(u8) { } const which = @import("./which.zig").which; - pub fn byPATH(env: *DotEnv.Loader, buf: *[bun.MAX_PATH_BYTES]u8, out: *[]const u8) ?Editor { + pub fn byPATH(env: *DotEnv.Loader, buf: *[bun.MAX_PATH_BYTES]u8, cwd: string, out: *[]const u8) ?Editor { const PATH = env.get("PATH") orelse return null; inline for (default_preference_list) |editor| { if (bin_name.get(editor)) |path| { - if (which(buf, PATH, path)) |bin| { + if (which(buf, PATH, cwd, path)) |bin| { out.* = bun.asByteSlice(bin); return editor; } @@ -121,12 +121,12 @@ pub const Editor = enum(u8) { return null; } - pub fn byPATHForEditor(env: *DotEnv.Loader, editor: Editor, buf: *[bun.MAX_PATH_BYTES]u8, out: *[]const u8) bool { + pub fn byPATHForEditor(env: *DotEnv.Loader, editor: Editor, buf: *[bun.MAX_PATH_BYTES]u8, cwd: string, out: *[]const u8) bool { const PATH = env.get("PATH") orelse return false; if (bin_name.get(editor)) |path| { if (path.len > 0) { - if (which(buf, PATH, path)) |bin| { + if (which(buf, PATH, cwd, path)) |bin| { out.* = bun.asByteSlice(bin); return true; } @@ -152,9 +152,9 @@ pub const Editor = enum(u8) { return false; } - pub fn byFallback(env: *DotEnv.Loader, buf: *[bun.MAX_PATH_BYTES]u8, out: *[]const u8) ?Editor { + pub fn byFallback(env: *DotEnv.Loader, buf: *[bun.MAX_PATH_BYTES]u8, cwd: string, out: *[]const u8) ?Editor { inline for (default_preference_list) |editor| { - if (byPATHForEditor(env, editor, buf, out)) { + if (byPATHForEditor(env, editor, buf, cwd, out)) { return editor; } @@ -405,7 +405,7 @@ pub const EditorContext = struct { // "vscode" if (Editor.byName(std.fs.path.basename(this.name))) |editor_| { - if (Editor.byPATHForEditor(env, editor_, &buf, &out)) { + if (Editor.byPATHForEditor(env, editor_, &buf, Fs.FileSystem.instance.top_level_dir, &out)) { this.editor = editor_; this.path = Fs.FileSystem.instance.dirname_store.append(string, out) catch unreachable; return; @@ -422,7 +422,7 @@ pub const EditorContext = struct { // EDITOR=code if (Editor.detect(env)) |editor_| { - if (Editor.byPATHForEditor(env, editor_, &buf, &out)) { + if (Editor.byPATHForEditor(env, editor_, &buf, Fs.FileSystem.instance.top_level_dir, &out)) { this.editor = editor_; this.path = Fs.FileSystem.instance.dirname_store.append(string, out) catch unreachable; return; @@ -437,7 +437,7 @@ pub const EditorContext = struct { } // Don't know, so we will just guess based on what exists - if (Editor.byFallback(env, &buf, &out)) |editor_| { + if (Editor.byFallback(env, &buf, Fs.FileSystem.instance.top_level_dir, &out)) |editor_| { this.editor = editor_; this.path = Fs.FileSystem.instance.dirname_store.append(string, out) catch unreachable; return; diff --git a/src/shell/interpreter.zig b/src/shell/interpreter.zig index cd1da60423918b..08a5adefb3f840 100644 --- a/src/shell/interpreter.zig +++ b/src/shell/interpreter.zig @@ -3599,7 +3599,7 @@ pub const Interpreter = struct { } var path_buf: [bun.MAX_PATH_BYTES]u8 = undefined; - const resolved = which(&path_buf, spawn_args.PATH, first_arg[0..first_arg_len]) orelse { + const resolved = which(&path_buf, spawn_args.PATH, spawn_args.cwd, first_arg[0..first_arg_len]) orelse { this.writeFailingError("bun: command not found: {s}\n", .{first_arg}); return; }; @@ -5868,7 +5868,7 @@ pub const Interpreter = struct { var had_not_found = false; for (args) |arg_raw| { const arg = arg_raw[0..std.mem.len(arg_raw)]; - const resolved = which(&path_buf, PATH.slice(), arg) orelse { + const resolved = which(&path_buf, PATH.slice(), this.bltn.parentCmd().base.shell.cwdZ(), arg) orelse { had_not_found = true; const buf = this.bltn.fmtErrorArena(.which, "{s} not found\n", .{arg}); _ = this.bltn.writeNoIO(.stdout, buf); @@ -5906,7 +5906,7 @@ pub const Interpreter = struct { var path_buf: [bun.MAX_PATH_BYTES]u8 = undefined; const PATH = this.bltn.parentCmd().base.shell.export_env.get(EnvStr.initSlice("PATH")) orelse EnvStr.initSlice(""); - const resolved = which(&path_buf, PATH.slice(), arg) orelse { + const resolved = which(&path_buf, PATH.slice(), this.bltn.parentCmd().base.shell.cwdZ(), arg) orelse { multiargs.had_not_found = true; if (!this.bltn.stdout.needsIO()) { const buf = this.bltn.fmtErrorArena(null, "{s} not found\n", .{arg}); diff --git a/src/which.zig b/src/which.zig index a11e0b840562a3..32dffb8d59009e 100644 --- a/src/which.zig +++ b/src/which.zig @@ -2,8 +2,6 @@ const std = @import("std"); const bun = @import("root").bun; const PosixToWinNormalizer = bun.path.PosixToWinNormalizer; -const debug = bun.Output.scoped(.which, true); - fn isValid(buf: *bun.PathBuffer, segment: []const u8, bin: []const u8) ?u16 { bun.copy(u8, buf, segment); buf[segment.len] = std.fs.path.sep; @@ -16,17 +14,15 @@ fn isValid(buf: *bun.PathBuffer, segment: []const u8, bin: []const u8) ?u16 { // Like /usr/bin/which but without needing to exec a child process // Remember to resolve the symlink if necessary -pub fn which(buf: *bun.PathBuffer, path: []const u8, bin: []const u8) ?[:0]const u8 { - debug("which({s} in {s})", .{ bin, path }); +pub fn which(buf: *bun.PathBuffer, path: []const u8, cwd: []const u8, bin: []const u8) ?[:0]const u8 { if (bun.Environment.os == .windows) { var convert_buf: bun.WPathBuffer = undefined; - const result = whichWin(&convert_buf, path, bin) orelse return null; + const result = whichWin(&convert_buf, path, cwd, bin) orelse return null; const result_converted = bun.strings.convertUTF16toUTF8InBuffer(buf, result) catch unreachable; buf[result_converted.len] = 0; std.debug.assert(result_converted.ptr == buf.ptr); return buf[0..result_converted.len :0]; } - if (bin.len == 0) return null; // handle absolute paths @@ -41,7 +37,11 @@ pub fn which(buf: *bun.PathBuffer, path: []const u8, bin: []const u8) ?[:0]const // /foo/bar/baz as a path and you're in /home/jarred? } - if (path.len == 0) return null; + if (cwd.len > 0) { + if (isValid(buf, std.mem.trimRight(u8, cwd, std.fs.path.sep_str), bin)) |len| { + return buf[0..len :0]; + } + } var path_iter = std.mem.tokenizeScalar(u8, path, std.fs.path.delimiter); while (path_iter.next()) |segment| { @@ -117,9 +117,9 @@ fn searchBinInPath(buf: *bun.WPathBuffer, path_buf: *[bun.MAX_PATH_BYTES]u8, pat } /// This is the windows version of `which`. -/// It returns a wide string. +/// It operates on wide strings. /// It is similar to Get-Command in powershell. -fn whichWin(buf: *bun.WPathBuffer, path: []const u8, bin: []const u8) ?[:0]const u16 { +pub fn whichWin(buf: *bun.WPathBuffer, path: []const u8, cwd: []const u8, bin: []const u8) ?[:0]const u16 { if (bin.len == 0) return null; var path_buf: [bun.MAX_PATH_BYTES]u8 = undefined; @@ -133,7 +133,10 @@ fn whichWin(buf: *bun.WPathBuffer, path: []const u8, bin: []const u8) ?[:0]const return searchBin(buf, bin_utf16.len, check_windows_extensions); } - if (path.len == 0) return null; + // check if bin is in cwd + if (searchBinInPath(buf, &path_buf, cwd, bin, check_windows_extensions)) |bin_path| { + return bin_path; + } // iterate over system path delimiter var path_iter = std.mem.tokenizeScalar(u8, path, ';'); @@ -145,3 +148,14 @@ fn whichWin(buf: *bun.WPathBuffer, path: []const u8, bin: []const u8) ?[:0]const return null; } + +test "which" { + var buf: bun.fs.PathBuffer = undefined; + const realpath = bun.getenvZ("PATH") orelse unreachable; + const whichbin = which(&buf, realpath, try bun.getcwdAlloc(std.heap.c_allocator), "which"); + try std.testing.expectEqualStrings(whichbin orelse return std.debug.assert(false), "/usr/bin/which"); + try std.testing.expect(null == which(&buf, realpath, try bun.getcwdAlloc(std.heap.c_allocator), "baconnnnnn")); + try std.testing.expect(null != which(&buf, realpath, try bun.getcwdAlloc(std.heap.c_allocator), "zig")); + try std.testing.expect(null == which(&buf, realpath, try bun.getcwdAlloc(std.heap.c_allocator), "bin")); + try std.testing.expect(null == which(&buf, realpath, try bun.getcwdAlloc(std.heap.c_allocator), "usr")); +} diff --git a/test/js/bun/util/which.test.ts b/test/js/bun/util/which.test.ts index c5ec4e13b2897b..f1d4b675bb3cf6 100644 --- a/test/js/bun/util/which.test.ts +++ b/test/js/bun/util/which.test.ts @@ -1,18 +1,11 @@ import { test, expect } from "bun:test"; -import { $, which } from "bun"; +import { which } from "bun"; import { rmSync, chmodSync, mkdirSync, realpathSync } from "node:fs"; import { join, basename } from "node:path"; import { tmpdir } from "node:os"; -import { rmdirSync } from "js/node/fs/export-star-from"; -import { isIntelMacOS, isWindows, tempDirWithFiles } from "harness"; - -{ - const delim = isWindows ? ";" : ":"; - if (`${delim}${process.env.PATH}${delim}`.includes(`${delim}.${delim}`)) { - throw new Error("$PATH includes . which will break `Bun.which` tests. This is an environment configuration issue."); - } -} +import { cpSync, rmdirSync } from "js/node/fs/export-star-from"; +import { isIntelMacOS, isWindows } from "../../../harness"; function writeFixture(path: string) { var fs = require("fs"); @@ -59,8 +52,10 @@ if (isWindows) { basedir = realpathSync(basedir); process.chdir(basedir); - expect(which("myscript.sh")).toBe(null); + // Our cwd is not /tmp + expect(which("myscript.sh")).toBe(abs); + const orig = process.cwd(); process.chdir(tmpdir()); try { rmdirSync("myscript.sh"); @@ -101,28 +96,16 @@ if (isWindows) { PATH: "/not-tmp", }), ).toBe(null); + + expect( + // cwd is checked first + which("myscript.sh", { + cwd: basedir, + }), + ).toBe(abs); } finally { process.chdir(origDir); rmSync(basedir, { recursive: true, force: true }); } }); } - -test("Bun.which does not look in the current directory", async () => { - const cwd = process.cwd(); - const dir = tempDirWithFiles("which", { - "some_program_name": "#!/usr/bin/env sh\necho FAIL\nexit 0\n", - "some_program_name.cmd": "@echo FAIL\n@exit 0\n", - }); - process.chdir(dir); - try { - if (!isWindows) { - await $`chmod +x ./some_program_name`; - } - - expect(which("some_program_name")).toBe(null); - expect((await $`some_program_name`).exitCode).not.toBe(0); - } finally { - process.chdir(cwd); - } -}); From 8985a29b0afed62dae2f2f2e19fe8962f504281f Mon Sep 17 00:00:00 2001 From: dave caruso Date: Sun, 31 Mar 2024 13:16:33 -0700 Subject: [PATCH 2/5] fix which implementation to be more accurate --- src/bun.js/bindings/ZigGlobalObject.cpp | 21 ----- src/js/builtins.d.ts | 1 - src/js/builtins/BunBuiltinNames.h | 1 - src/js/builtins/ReadableStream.ts | 113 ------------------------ src/string_immutable.zig | 7 ++ src/which.zig | 35 ++++++-- test/js/bun/util/which.test.ts | 72 ++++++++++++--- 7 files changed, 92 insertions(+), 158 deletions(-) diff --git a/src/bun.js/bindings/ZigGlobalObject.cpp b/src/bun.js/bindings/ZigGlobalObject.cpp index c1afa3ca866108..cea0f4cb4026f3 100644 --- a/src/bun.js/bindings/ZigGlobalObject.cpp +++ b/src/bun.js/bindings/ZigGlobalObject.cpp @@ -2070,26 +2070,6 @@ extern "C" int32_t ReadableStreamTag__tagged(Zig::GlobalObject* globalObject, JS return 0; } -extern "C" JSC__JSValue ReadableStream__consume(Zig::GlobalObject* globalObject, JSC__JSValue stream, JSC__JSValue nativeType, JSC__JSValue nativePtr); -extern "C" JSC__JSValue ReadableStream__consume(Zig::GlobalObject* globalObject, JSC__JSValue stream, JSC__JSValue nativeType, JSC__JSValue nativePtr) -{ - ASSERT(globalObject); - - auto& vm = globalObject->vm(); - auto scope = DECLARE_CATCH_SCOPE(vm); - - auto& builtinNames = WebCore::builtinNames(vm); - - auto function = globalObject->getDirect(vm, builtinNames.consumeReadableStreamPrivateName()).getObject(); - JSC::MarkedArgumentBuffer arguments = JSC::MarkedArgumentBuffer(); - arguments.append(JSValue::decode(nativePtr)); - arguments.append(JSValue::decode(nativeType)); - arguments.append(JSValue::decode(stream)); - - auto callData = JSC::getCallData(function); - return JSC::JSValue::encode(call(globalObject, function, callData, JSC::jsUndefined(), arguments)); -} - extern "C" JSC__JSValue ZigGlobalObject__createNativeReadableStream(Zig::GlobalObject* globalObject, JSC__JSValue nativePtr) { auto& vm = globalObject->vm(); @@ -3390,7 +3370,6 @@ void GlobalObject::addBuiltinGlobals(JSC::VM& vm) putDirectBuiltinFunction(vm, this, builtinNames.createFIFOPrivateName(), streamInternalsCreateFIFOCodeGenerator(vm), PropertyAttribute::Builtin | PropertyAttribute::DontDelete | PropertyAttribute::ReadOnly); putDirectBuiltinFunction(vm, this, builtinNames.createEmptyReadableStreamPrivateName(), readableStreamCreateEmptyReadableStreamCodeGenerator(vm), PropertyAttribute::Builtin | PropertyAttribute::DontDelete | PropertyAttribute::ReadOnly); putDirectBuiltinFunction(vm, this, builtinNames.createUsedReadableStreamPrivateName(), readableStreamCreateUsedReadableStreamCodeGenerator(vm), PropertyAttribute::Builtin | PropertyAttribute::DontDelete | PropertyAttribute::ReadOnly); - putDirectBuiltinFunction(vm, this, builtinNames.consumeReadableStreamPrivateName(), readableStreamConsumeReadableStreamCodeGenerator(vm), PropertyAttribute::Builtin | PropertyAttribute::DontDelete | PropertyAttribute::ReadOnly); putDirectBuiltinFunction(vm, this, builtinNames.createNativeReadableStreamPrivateName(), readableStreamCreateNativeReadableStreamCodeGenerator(vm), PropertyAttribute::Builtin | PropertyAttribute::DontDelete | PropertyAttribute::ReadOnly); putDirectBuiltinFunction(vm, this, builtinNames.requireESMPrivateName(), importMetaObjectRequireESMCodeGenerator(vm), PropertyAttribute::Builtin | PropertyAttribute::DontDelete | PropertyAttribute::ReadOnly); putDirectBuiltinFunction(vm, this, builtinNames.loadCJS2ESMPrivateName(), importMetaObjectLoadCJS2ESMCodeGenerator(vm), PropertyAttribute::Builtin | PropertyAttribute::DontDelete | PropertyAttribute::ReadOnly); diff --git a/src/js/builtins.d.ts b/src/js/builtins.d.ts index 68c84808b98fb9..386e32f583e8fd 100644 --- a/src/js/builtins.d.ts +++ b/src/js/builtins.d.ts @@ -254,7 +254,6 @@ declare function $closedPromise(): TODO; declare function $closedPromiseCapability(): TODO; declare function $code(): TODO; declare function $connect(): TODO; -declare function $consumeReadableStream(): TODO; declare function $controlledReadableStream(): TODO; declare function $controller(): TODO; declare function $cork(): TODO; diff --git a/src/js/builtins/BunBuiltinNames.h b/src/js/builtins/BunBuiltinNames.h index 127524b40cc043..3a56f99811a051 100644 --- a/src/js/builtins/BunBuiltinNames.h +++ b/src/js/builtins/BunBuiltinNames.h @@ -53,7 +53,6 @@ using namespace JSC; macro(closeRequested) \ macro(code) \ macro(connect) \ - macro(consumeReadableStream) \ macro(controlledReadableStream) \ macro(controller) \ macro(cork) \ diff --git a/src/js/builtins/ReadableStream.ts b/src/js/builtins/ReadableStream.ts index 0108c08154503f..7b077617156e5d 100644 --- a/src/js/builtins/ReadableStream.ts +++ b/src/js/builtins/ReadableStream.ts @@ -166,119 +166,6 @@ export function readableStreamToBlob(stream: ReadableStream): Promise { return Promise.resolve(Bun.readableStreamToArray(stream)).then(array => new Blob(array)); } -$linkTimeConstant; -export function consumeReadableStream(nativePtr, nativeType, inputStream) { - const symbol = globalThis.Symbol.for("Bun.consumeReadableStreamPrototype"); - var cached = globalThis[symbol]; - if (!cached) { - cached = globalThis[symbol] = []; - } - var Prototype = cached[nativeType]; - if (Prototype === undefined) { - var [doRead, doError, doReadMany, doClose, onClose, deinit] = $lazy(nativeType); - - Prototype = class NativeReadableStreamSink { - handleError: any; - handleClosed: any; - processResult: any; - - constructor(reader, ptr) { - this.#ptr = ptr; - this.#reader = reader; - this.#didClose = false; - - this.handleError = this._handleError.bind(this); - this.handleClosed = this._handleClosed.bind(this); - this.processResult = this._processResult.bind(this); - - reader.closed.then(this.handleClosed, this.handleError); - } - - _handleClosed() { - if (this.#didClose) return; - this.#didClose = true; - var ptr = this.#ptr; - this.#ptr = 0; - doClose(ptr); - deinit(ptr); - } - - _handleError(error) { - if (this.#didClose) return; - this.#didClose = true; - var ptr = this.#ptr; - this.#ptr = 0; - doError(ptr, error); - deinit(ptr); - } - - #ptr; - #didClose = false; - #reader; - - _handleReadMany({ value, done, size }) { - if (done) { - this.handleClosed(); - return; - } - - if (this.#didClose) return; - - doReadMany(this.#ptr, value, done, size); - } - - read() { - if (!this.#ptr) return $throwTypeError("ReadableStreamSink is already closed"); - - return this.processResult(this.#reader.read()); - } - - _processResult(result) { - if (result && $isPromise(result)) { - const flags = $getPromiseInternalField(result, $promiseFieldFlags); - if (flags & $promiseStateFulfilled) { - const fulfilledValue = $getPromiseInternalField(result, $promiseFieldReactionsOrResult); - if (fulfilledValue) { - result = fulfilledValue; - } - } - } - - if (result && $isPromise(result)) { - result.then(this.processResult, this.handleError); - return null; - } - - if (result.done) { - this.handleClosed(); - return 0; - } else if (result.value) { - return result.value; - } else { - return -1; - } - } - - readMany() { - if (!this.#ptr) return $throwTypeError("ReadableStreamSink is already closed"); - return this.processResult(this.#reader.readMany()); - } - }; - - const minlength = nativeType + 1; - if (cached.length < minlength) { - cached.length = minlength; - } - $putByValDirect(cached, nativeType, Prototype); - } - - if ($isReadableStreamLocked(inputStream)) { - throw new TypeError("Cannot start reading from a locked stream"); - } - - return new Prototype(inputStream.getReader(), nativePtr); -} - $linkTimeConstant; export function createEmptyReadableStream() { var stream = new ReadableStream({ diff --git a/src/string_immutable.zig b/src/string_immutable.zig index 9fd5f5792e2c0a..1dda0e6de38507 100644 --- a/src/string_immutable.zig +++ b/src/string_immutable.zig @@ -6295,5 +6295,12 @@ pub fn withoutSuffixComptime(input: []const u8, comptime suffix: []const u8) []c return input; } +pub fn withoutPrefixComptime(input: []const u8, comptime prefix: []const u8) []const u8 { + if (hasPrefixComptime(input, prefix)) { + return input[prefix.len..]; + } + return input; +} + // extern "C" bool icu_hasBinaryProperty(UChar32 cp, unsigned int prop) extern fn icu_hasBinaryProperty(c: u32, which: c_uint) bool; diff --git a/src/which.zig b/src/which.zig index 32dffb8d59009e..fca6571d15a648 100644 --- a/src/which.zig +++ b/src/which.zig @@ -23,6 +23,7 @@ pub fn which(buf: *bun.PathBuffer, path: []const u8, cwd: []const u8, bin: []con std.debug.assert(result_converted.ptr == buf.ptr); return buf[0..result_converted.len :0]; } + if (bin.len == 0) return null; // handle absolute paths @@ -31,16 +32,22 @@ pub fn which(buf: *bun.PathBuffer, path: []const u8, cwd: []const u8, bin: []con buf[bin.len] = 0; const binZ: [:0]u8 = buf[0..bin.len :0]; if (bun.sys.isExecutableFilePath(binZ)) return binZ; - - // note that directories are often executable - // TODO: should we return null here? What about the case where ytou have - // /foo/bar/baz as a path and you're in /home/jarred? + // Do not look absolute paths in $PATH + return null; } - if (cwd.len > 0) { - if (isValid(buf, std.mem.trimRight(u8, cwd, std.fs.path.sep_str), bin)) |len| { - return buf[0..len :0]; + if (bun.strings.containsChar(bin, '/')) { + if (cwd.len > 0) { + if (isValid( + buf, + std.mem.trimRight(u8, cwd, std.fs.path.sep_str), + bun.strings.withoutPrefixComptime(bin, "./"), + )) |len| { + return buf[0..len :0]; + } } + // Do not lookup paths with slashes in $PATH + return null; } var path_iter = std.mem.tokenizeScalar(u8, path, std.fs.path.delimiter); @@ -134,8 +141,18 @@ pub fn whichWin(buf: *bun.WPathBuffer, path: []const u8, cwd: []const u8, bin: [ } // check if bin is in cwd - if (searchBinInPath(buf, &path_buf, cwd, bin, check_windows_extensions)) |bin_path| { - return bin_path; + if (bun.strings.containsChar(bin, '/')) { + if (searchBinInPath( + buf, + &path_buf, + cwd, + bun.strings.withoutPrefixComptime(bin, "./"), + check_windows_extensions, + )) |bin_path| { + return bin_path; + } + // Do not lookup paths with slashes in $PATH + return null; } // iterate over system path delimiter diff --git a/test/js/bun/util/which.test.ts b/test/js/bun/util/which.test.ts index f1d4b675bb3cf6..f16f6d95c0814c 100644 --- a/test/js/bun/util/which.test.ts +++ b/test/js/bun/util/which.test.ts @@ -1,11 +1,19 @@ import { test, expect } from "bun:test"; -import { which } from "bun"; +import { $, which } from "bun"; import { rmSync, chmodSync, mkdirSync, realpathSync } from "node:fs"; import { join, basename } from "node:path"; import { tmpdir } from "node:os"; -import { cpSync, rmdirSync } from "js/node/fs/export-star-from"; -import { isIntelMacOS, isWindows } from "../../../harness"; +import { rmdirSync } from "js/node/fs/export-star-from"; +import { isIntelMacOS, isWindows, tempDirWithFiles } from "harness"; +import { w } from "vitest/dist/types-2b1c412e.js"; + +{ + const delim = isWindows ? ";" : ":"; + if (`${delim}${process.env.PATH}${delim}`.includes(`${delim}.${delim}`)) { + throw new Error("$PATH includes . which will break `Bun.which` tests. This is an environment configuration issue."); + } +} function writeFixture(path: string) { var fs = require("fs"); @@ -52,10 +60,8 @@ if (isWindows) { basedir = realpathSync(basedir); process.chdir(basedir); - // Our cwd is not /tmp - expect(which("myscript.sh")).toBe(abs); + expect(which("myscript.sh")).toBe(null); - const orig = process.cwd(); process.chdir(tmpdir()); try { rmdirSync("myscript.sh"); @@ -96,16 +102,56 @@ if (isWindows) { PATH: "/not-tmp", }), ).toBe(null); - - expect( - // cwd is checked first - which("myscript.sh", { - cwd: basedir, - }), - ).toBe(abs); } finally { process.chdir(origDir); rmSync(basedir, { recursive: true, force: true }); } }); } + +test("Bun.which does not look in the current directory for bins", async () => { + const cwd = process.cwd(); + const dir = tempDirWithFiles("which", { + "some_program_name": "#!/usr/bin/env sh\necho FAIL\nexit 0\n", + "some_program_name.cmd": "@echo FAIL\n@exit 0\n", + }); + process.chdir(dir); + try { + if (!isWindows) { + await $`chmod +x ./some_program_name`; + } + + expect(which("some_program_name")).toBe(null); + expect((await $`some_program_name`).exitCode).not.toBe(0); + } finally { + process.chdir(cwd); + } +}); + +test("Bun.which does look in the current directory when given a path with a slash", async () => { + const cwd = process.cwd(); + const dir = tempDirWithFiles("which", { + "some_program_name": "#!/usr/bin/env sh\necho posix\nexit 0\n", + "some_program_name.cmd": "@echo win32\n@exit 0\n", + "folder/other_app": "#!/usr/bin/env sh\necho posix\nexit 0\n", + "folder/other_app.cmd": "@echo win32\n@exit 0\n", + }); + process.chdir(dir); + try { + if (!isWindows) { + await $`chmod +x ./some_program_name`; + await $`chmod +x ./folder/other_app`; + } + + const suffix = isWindows ? ".cmd" : ""; + + expect(which("./some_program_name")).toBe(dir + "/some_program_name" + suffix); + expect((await $`./some_program_name`.text()).trim()).toBe(isWindows ? "win32" : "posix"); + expect(which("./folder/other_app")).toBe(dir + "/folder/other_app" + suffix); + expect((await $`./folder/other_app`.text()).trim()).toBe(isWindows ? "win32" : "posix"); + expect(which("folder/other_app")).toBe(dir + "/folder/other_app" + suffix); + expect((await $`folder/other_app`.text()).trim()).toBe(isWindows ? "win32" : "posix"); + } finally { + process.chdir(cwd); + } +}); From 9b01de2288f56978a62a2300ea6da9004d42ba30 Mon Sep 17 00:00:00 2001 From: dave caruso Date: Sun, 31 Mar 2024 13:17:58 -0700 Subject: [PATCH 3/5] t --- packages/bun-types/bun.d.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/bun-types/bun.d.ts b/packages/bun-types/bun.d.ts index 418c7f3b6cbeb8..0a1ba9f625aa0a 100644 --- a/packages/bun-types/bun.d.ts +++ b/packages/bun-types/bun.d.ts @@ -43,7 +43,7 @@ declare module "bun" { * * @param {string} command The name of the executable or script * @param {string} options.PATH Overrides the PATH environment variable - * @param {string} options.cwd Limits the search to a particular directory in which to searc + * @param {string} options.cwd When given a relative path, use this path to join it. */ function which(command: string, options?: { PATH?: string; cwd?: string }): string | null; From e01f6ad596c1abaea77a4392f7dcafa840bda7f9 Mon Sep 17 00:00:00 2001 From: dave caruso Date: Sun, 31 Mar 2024 16:04:35 -0700 Subject: [PATCH 4/5] which tests windows --- src/allocators.zig | 2 +- src/which.zig | 7 ++++--- test/js/bun/util/which.test.ts | 6 +++--- 3 files changed, 8 insertions(+), 7 deletions(-) diff --git a/src/allocators.zig b/src/allocators.zig index cea0ae9b41e511..5af6c1014cbe76 100644 --- a/src/allocators.zig +++ b/src/allocators.zig @@ -7,7 +7,7 @@ const bun = @import("root").bun; pub fn isSliceInBufferT(comptime T: type, slice: []const T, buffer: []const T) bool { return (@intFromPtr(buffer.ptr) <= @intFromPtr(slice.ptr) and - (@intFromPtr(slice.ptr) + slice.len) <= (@intFromPtr(buffer.ptr) + buffer.len)); + (@intFromPtr(slice.ptr) + slice.len * @sizeOf(T)) <= (@intFromPtr(buffer.ptr) + buffer.len * @sizeOf(T))); } /// Checks if a slice's pointer is contained within another slice. diff --git a/src/which.zig b/src/which.zig index fca6571d15a648..db63807da6a0c2 100644 --- a/src/which.zig +++ b/src/which.zig @@ -83,7 +83,7 @@ pub fn endsWithExtension(str: []const u8) bool { } /// Check if the WPathBuffer holds a existing file path, checking also for windows extensions variants like .exe, .cmd and .bat (internally used by whichWin) -fn searchBin(buf: *bun.WPathBuffer, path_size: usize, check_windows_extensions: bool) ?[:0]const u16 { +fn searchBin(buf: *bun.WPathBuffer, path_size: usize, check_windows_extensions: bool) ?[:0]u16 { if (!check_windows_extensions) // On Windows, files without extensions are not executable // Therefore, we should only care about this check when the file already has an extension. @@ -103,7 +103,7 @@ fn searchBin(buf: *bun.WPathBuffer, path_size: usize, check_windows_extensions: } /// Check if bin file exists in this path (internally used by whichWin) -fn searchBinInPath(buf: *bun.WPathBuffer, path_buf: *[bun.MAX_PATH_BYTES]u8, path: []const u8, bin: []const u8, check_windows_extensions: bool) ?[:0]const u16 { +fn searchBinInPath(buf: *bun.WPathBuffer, path_buf: *[bun.MAX_PATH_BYTES]u8, path: []const u8, bin: []const u8, check_windows_extensions: bool) ?[:0]u16 { if (path.len == 0) return null; const segment = if (std.fs.path.isAbsolute(path)) (PosixToWinNormalizer.resolveCWDWithExternalBuf(path_buf, path) catch return null) else path; const segment_utf16 = bun.strings.convertUTF8toUTF16InBuffer(buf, segment); @@ -141,7 +141,7 @@ pub fn whichWin(buf: *bun.WPathBuffer, path: []const u8, cwd: []const u8, bin: [ } // check if bin is in cwd - if (bun.strings.containsChar(bin, '/')) { + if (bun.strings.containsChar(bin, '/') or bun.strings.containsChar(bin, '\\')) { if (searchBinInPath( buf, &path_buf, @@ -149,6 +149,7 @@ pub fn whichWin(buf: *bun.WPathBuffer, path: []const u8, cwd: []const u8, bin: [ bun.strings.withoutPrefixComptime(bin, "./"), check_windows_extensions, )) |bin_path| { + bun.path.posixToPlatformInPlace(u16, bin_path); return bin_path; } // Do not lookup paths with slashes in $PATH diff --git a/test/js/bun/util/which.test.ts b/test/js/bun/util/which.test.ts index f16f6d95c0814c..1ad748754a416b 100644 --- a/test/js/bun/util/which.test.ts +++ b/test/js/bun/util/which.test.ts @@ -145,11 +145,11 @@ test("Bun.which does look in the current directory when given a path with a slas const suffix = isWindows ? ".cmd" : ""; - expect(which("./some_program_name")).toBe(dir + "/some_program_name" + suffix); + expect(which("./some_program_name")).toBe(join(dir , "some_program_name" + suffix)); expect((await $`./some_program_name`.text()).trim()).toBe(isWindows ? "win32" : "posix"); - expect(which("./folder/other_app")).toBe(dir + "/folder/other_app" + suffix); + expect(which("./folder/other_app")).toBe(join(dir , "folder/other_app" + suffix)); expect((await $`./folder/other_app`.text()).trim()).toBe(isWindows ? "win32" : "posix"); - expect(which("folder/other_app")).toBe(dir + "/folder/other_app" + suffix); + expect(which("folder/other_app")).toBe(join(dir , "folder/other_app" + suffix)); expect((await $`folder/other_app`.text()).trim()).toBe(isWindows ? "win32" : "posix"); } finally { process.chdir(cwd); From c41b2d7002d18d8b8bcbe6582eea38320be6d3b0 Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Sun, 31 Mar 2024 23:06:03 +0000 Subject: [PATCH 5/5] [autofix.ci] apply automated fixes --- test/js/bun/util/which.test.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/test/js/bun/util/which.test.ts b/test/js/bun/util/which.test.ts index 1ad748754a416b..7c20612e6f47fe 100644 --- a/test/js/bun/util/which.test.ts +++ b/test/js/bun/util/which.test.ts @@ -145,11 +145,11 @@ test("Bun.which does look in the current directory when given a path with a slas const suffix = isWindows ? ".cmd" : ""; - expect(which("./some_program_name")).toBe(join(dir , "some_program_name" + suffix)); + expect(which("./some_program_name")).toBe(join(dir, "some_program_name" + suffix)); expect((await $`./some_program_name`.text()).trim()).toBe(isWindows ? "win32" : "posix"); - expect(which("./folder/other_app")).toBe(join(dir , "folder/other_app" + suffix)); + expect(which("./folder/other_app")).toBe(join(dir, "folder/other_app" + suffix)); expect((await $`./folder/other_app`.text()).trim()).toBe(isWindows ? "win32" : "posix"); - expect(which("folder/other_app")).toBe(join(dir , "folder/other_app" + suffix)); + expect(which("folder/other_app")).toBe(join(dir, "folder/other_app" + suffix)); expect((await $`folder/other_app`.text()).trim()).toBe(isWindows ? "win32" : "posix"); } finally { process.chdir(cwd);