diff --git a/packages/bun-types/bun.d.ts b/packages/bun-types/bun.d.ts index 2bb70f39716e4c..0a1ba9f625aa0a 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 When given a relative path, use this path to join it. */ - 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/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/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/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/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/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/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/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 a11e0b840562a3..db63807da6a0c2 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,11 +14,10 @@ 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); @@ -35,13 +32,23 @@ pub fn which(buf: *bun.PathBuffer, path: []const u8, bin: []const u8) ?[:0]const 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 (path.len == 0) return null; + 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); while (path_iter.next()) |segment| { @@ -76,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. @@ -96,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); @@ -117,9 +124,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 +140,21 @@ 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 (bun.strings.containsChar(bin, '/') or bun.strings.containsChar(bin, '\\')) { + if (searchBinInPath( + buf, + &path_buf, + cwd, + 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 + return null; + } // iterate over system path delimiter var path_iter = std.mem.tokenizeScalar(u8, path, ';'); @@ -145,3 +166,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..7c20612e6f47fe 100644 --- a/test/js/bun/util/which.test.ts +++ b/test/js/bun/util/which.test.ts @@ -6,6 +6,7 @@ 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"; +import { w } from "vitest/dist/types-2b1c412e.js"; { const delim = isWindows ? ";" : ":"; @@ -108,7 +109,7 @@ if (isWindows) { }); } -test("Bun.which does not look in the current directory", async () => { +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", @@ -126,3 +127,31 @@ test("Bun.which does not look in the current directory", async () => { 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(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((await $`./folder/other_app`.text()).trim()).toBe(isWindows ? "win32" : "posix"); + 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); + } +});