diff --git a/src/StandaloneModuleGraph.zig b/src/StandaloneModuleGraph.zig index 8626a37eb5..f521325e4b 100644 --- a/src/StandaloneModuleGraph.zig +++ b/src/StandaloneModuleGraph.zig @@ -10,6 +10,8 @@ const Global = bun.Global; const Environment = bun.Environment; const Syscall = bun.sys; +const exe_suffix = bun.exe_suffix; + const w = std.os.windows; pub const StandaloneModuleGraph = struct { @@ -662,8 +664,6 @@ pub const StandaloneModuleGraph = struct { return try StandaloneModuleGraph.fromBytes(allocator, to_read, offsets); } - const exe_suffix = if (Environment.isWindows) ".exe" else ""; - fn isBuiltInExe(argv0: []const u8) bool { if (argv0.len == 0) return false; diff --git a/src/async/windows_event_loop.zig b/src/async/windows_event_loop.zig index 0dc855f8d7..6888e23b7a 100644 --- a/src/async/windows_event_loop.zig +++ b/src/async/windows_event_loop.zig @@ -195,7 +195,10 @@ pub const FilePoll = struct { pub fn unregister(this: *FilePoll, loop: *Loop) bool { _ = loop; - uv.uv_unref(@ptrFromInt(this.fd.int())); + // TODO(@paperdave): This cast is extremely suspicious. At best, `fd` is + // the wrong type (it should be a uv handle), at worst this code is a + // crash due to invalid memory access. + uv.uv_unref(@ptrFromInt(@intFromEnum(this.fd))); return true; } diff --git a/src/bun.js/ConsoleObject.zig b/src/bun.js/ConsoleObject.zig index a43dfc3091..b0d9324947 100644 --- a/src/bun.js/ConsoleObject.zig +++ b/src/bun.js/ConsoleObject.zig @@ -1633,8 +1633,11 @@ pub const Formatter = struct { } else if (Environment.isDebug and is_private_symbol) { this.addForNewLine(1 + "$:".len + key.len); writer.print( - comptime Output.prettyFmt("${any}: ", enable_ansi_colors), - .{key}, + comptime Output.prettyFmt("{s}{any}: ", enable_ansi_colors), + .{ + if (key.len > 0 and key.charAt(0) == '#') "" else "$", + key, + }, ); } else { this.addForNewLine(1 + "[Symbol()]:".len + key.len); diff --git a/src/bun.js/api/bun/socket.zig b/src/bun.js/api/bun/socket.zig index 1e71c4fa8f..ae54172199 100644 --- a/src/bun.js/api/bun/socket.zig +++ b/src/bun.js/api/bun/socket.zig @@ -1172,23 +1172,23 @@ fn NewSocket(comptime ssl: bool) type { pub fn doConnect(this: *This, connection: Listener.UnixOrHost, socket_ctx: *uws.SocketContext) !void { switch (connection) { .host => |c| { - _ = This.Socket.connectPtr( + _ = try This.Socket.connectPtr( normalizeHost(c.host), c.port, socket_ctx, This, this, "socket", - ) orelse return error.ConnectionFailed; + ); }, .unix => |u| { - _ = This.Socket.connectUnixPtr( + _ = try This.Socket.connectUnixPtr( u, socket_ctx, This, this, "socket", - ) orelse return error.ConnectionFailed; + ); }, .fd => |f| { const socket = This.Socket.fromFd(socket_ctx, f, This, this, "socket") orelse return error.ConnectionFailed; diff --git a/src/bun.js/api/bun/subprocess.zig b/src/bun.js/api/bun/subprocess.zig index 067310a793..b91ebb379a 100644 --- a/src/bun.js/api/bun/subprocess.zig +++ b/src/bun.js/api/bun/subprocess.zig @@ -593,7 +593,7 @@ pub const Subprocess = struct { .memfd => return this.toBufferedValue(globalThis), .fd => |fd| { - return JSValue.jsNumber(fd); + return fd.toJS(globalThis); }, .pipe => { return this.pipe.toJS(this, globalThis, exited); @@ -607,7 +607,7 @@ pub const Subprocess = struct { pub fn toBufferedValue(this: *Readable, globalThis: *JSC.JSGlobalObject) JSValue { switch (this.*) { .fd => |fd| { - return JSValue.jsNumber(fd); + return fd.toJS(globalThis); }, .memfd => |fd| { if (comptime !Environment.isPosix) { @@ -1739,7 +1739,7 @@ pub const Subprocess = struct { pub fn toJS(this: Writable, globalThis: *JSC.JSGlobalObject) JSValue { return switch (this) { .pipe => |pipe| pipe.toJS(globalThis), - .fd => |fd| JSValue.jsNumber(fd), + .fd => |fd| fd.toJS(globalThis), .memfd, .ignore => JSValue.jsUndefined(), .inherit => JSValue.jsUndefined(), .buffered_input => JSValue.jsUndefined(), @@ -3287,7 +3287,7 @@ pub const Subprocess = struct { return true; } else if (value.isNumber()) { const fd = value.asFileDescriptor(); - if (fd.int() < 0) { + if (bun.uvfdcast(fd) < 0) { globalThis.throwInvalidArguments("file descriptor must be a positive integer", .{}); return false; } diff --git a/src/bun.js/bindings/bindings.zig b/src/bun.js/bindings/bindings.zig index 2069325d18..5545c4f6c4 100644 --- a/src/bun.js/bindings/bindings.zig +++ b/src/bun.js/bindings/bindings.zig @@ -3958,14 +3958,6 @@ pub const JSValue = enum(JSValueReprInt) { else => jsNumberFromInt64(@as(i64, @intCast(number))), }, else => { - // windows - if (comptime Number == std.os.fd_t) { - return jsNumber(bun.toFD(number)); - } - if (Number == bun.FileDescriptor) { - return jsNumber(number.int()); - } - @compileError("Type transformation missing for number of type: " ++ @typeName(Number)); }, }; diff --git a/src/bun.js/module_loader.zig b/src/bun.js/module_loader.zig index e1f7f21d56..8700284127 100644 --- a/src/bun.js/module_loader.zig +++ b/src/bun.js/module_loader.zig @@ -1494,8 +1494,8 @@ pub const ModuleLoader = struct { var package_json: ?*PackageJSON = null; if (jsc_vm.bun_watcher.indexOf(hash)) |index| { - const _fd = jsc_vm.bun_watcher.watchlist().items(.fd)[index]; - fd = if (_fd.int() > 0) _fd else null; + const maybe_fd = jsc_vm.bun_watcher.watchlist().items(.fd)[index]; + fd = if (maybe_fd != .zero) maybe_fd else null; package_json = jsc_vm.bun_watcher.watchlist().items(.package_json)[index]; } diff --git a/src/bun.js/webcore/blob.zig b/src/bun.js/webcore/blob.zig index acc881b558..93ac701677 100644 --- a/src/bun.js/webcore/blob.zig +++ b/src/bun.js/webcore/blob.zig @@ -1890,7 +1890,10 @@ pub const Blob = struct { } } - if (is_allowed_to_close_fd and this.opened_fd.int() > 2 and this.opened_fd != invalid_fd) { + if (is_allowed_to_close_fd and + this.opened_fd != invalid_fd and + !this.opened_fd.isStdio()) + { if (comptime Environment.isWindows) { bun.Async.Closer.close(bun.uvfdcast(this.opened_fd), this.loop); } else { diff --git a/src/bun.js/webcore/blob/WriteFile.zig b/src/bun.js/webcore/blob/WriteFile.zig index e826ce7da6..7b511a9ce5 100644 --- a/src/bun.js/webcore/blob/WriteFile.zig +++ b/src/bun.js/webcore/blob/WriteFile.zig @@ -398,20 +398,18 @@ pub const WriteFileWindows = struct { }, .fd => { write_file.fd = brk: { - const rare = event_loop.virtual_machine.rareData(); - - if (file_blob.store == rare.stdout_store) { - break :brk 1; - } else if (file_blob.store == rare.stderr_store) { - break :brk 2; - } else if (file_blob.store == rare.stdin_store) { - break :brk 0; + if (event_loop.virtual_machine.rare_data) |rare| { + if (file_blob.store == rare.stdout_store) { + break :brk 1; + } else if (file_blob.store == rare.stderr_store) { + break :brk 2; + } else if (file_blob.store == rare.stdin_store) { + break :brk 0; + } } - // The file stored descriptor is not 0, 1, or 2. - const fd_ = file_blob.store.?.data.file.pathlike.fd; - - break :brk @intCast(fd_.int()); + // The file stored descriptor is not stdin, stdout, or stderr. + break :brk bun.uvfdcast(file_blob.store.?.data.file.pathlike.fd); }; write_file.doWriteLoop(write_file.loop()); diff --git a/src/bun.js/webcore/response.zig b/src/bun.js/webcore/response.zig index 725c7c8e66..917cdae722 100644 --- a/src/bun.js/webcore/response.zig +++ b/src/bun.js/webcore/response.zig @@ -1194,7 +1194,9 @@ pub const Fetch = struct { } return null; } + pub fn onReject(this: *FetchTasklet) JSValue { + std.debug.assert(this.result.fail != null); log("onReject", .{}); if (this.getAbortError()) |err| { @@ -1211,20 +1213,17 @@ pub const Fetch = struct { return JSC.WebCore.AbortSignal.createAbortError(JSC.ZigString.static("The user aborted a request"), &JSC.ZigString.Empty, this.global_this); } - var path: bun.String = undefined; - // some times we don't have metadata so we also check http.url - if (this.metadata) |metadata| { - path = bun.String.createUTF8(metadata.url); - } else if (this.http) |http_| { - path = bun.String.createUTF8(http_.url.href); - } else { - path = bun.String.empty; - } + const path = if (this.metadata) |metadata| + bun.String.createUTF8(metadata.url) + else if (this.http) |http_| + bun.String.createUTF8(http_.url.href) + else + bun.String.empty; const fetch_error = JSC.SystemError{ - .code = bun.String.static(@errorName(this.result.fail)), - .message = switch (this.result.fail) { + .code = bun.String.static(@errorName(this.result.fail.?)), + .message = switch (this.result.fail.?) { error.ConnectionClosed => bun.String.static("The socket connection was closed unexpectedly. For more information, pass `verbose: true` in the second argument to fetch()"), error.FailedToOpenSocket => bun.String.static("Was there a typo in the url or port?"), error.TooManyRedirects => bun.String.static("The response redirected too many times. For more information, pass `verbose: true` in the second argument to fetch()"), diff --git a/src/bun.zig b/src/bun.zig index 2a23afb166..0d22a12491 100644 --- a/src/bun.zig +++ b/src/bun.zig @@ -40,7 +40,7 @@ pub const meta = @import("./meta.zig"); pub const ComptimeStringMap = @import("./comptime_string_map.zig").ComptimeStringMap; pub const base64 = @import("./base64/base64.zig"); pub const path = @import("./resolver/resolve_path.zig"); -pub const resolver = @import("./resolver//resolver.zig"); +pub const resolver = @import("./resolver/resolver.zig"); pub const DirIterator = @import("./bun.js/node/dir_iterator.zig"); pub const PackageJSON = @import("./resolver/package_json.zig").PackageJSON; pub const fmt = @import("./fmt.zig"); @@ -66,16 +66,29 @@ else std.os.fd_t; pub const FileDescriptor = enum(FileDescriptorInt) { - zero, + /// Zero is used in old filesystem code to indicate "no file descriptor" + /// This is problematic because on POSIX, this is ambiguous with stdin being 0. + /// All code that uses this should migrate to invalid_fd to represent invalid states. + zero = 0, + // Represents an invalid file descriptor. This is used instead of null to + // avoid an extra bit. + // invalid = @intFromEnum(invalid_fd), _, - pub inline fn int(fd: FileDescriptor) FileDescriptorInt { - // TODO(@paperdave): make this a compile error to call on windows. every usage is incorrect. - return @intFromEnum(fd); + /// Do not use this function in new code. + /// + /// Interpreting a FD as an integer is almost certainly a mistake. + /// On Windows, it is always a mistake, as the integer is bitcast of a tagged packed struct. + /// + /// TODO(@paperdave): remove this API. + pub inline fn int(self: FileDescriptor) std.os.fd_t { + if (Environment.isWindows) + @compileError("FileDescriptor.int() is not allowed on Windows."); + return @intFromEnum(self); } pub inline fn writeTo(fd: FileDescriptor, writer: anytype, endian: std.builtin.Endian) !void { - try writer.writeInt(FileDescriptorInt, fd.int(), endian); + try writer.writeInt(FileDescriptorInt, @intFromEnum(fd), endian); } pub inline fn readFrom(reader: anytype, endian: std.builtin.Endian) !FileDescriptor { @@ -118,6 +131,24 @@ pub const FileDescriptor = enum(FileDescriptorInt) { pub fn assertKind(fd: FileDescriptor, kind: FDImpl.Kind) void { std.debug.assert(FDImpl.decode(fd).kind == kind); } + + pub fn isStdio(fd: FileDescriptor) bool { + // fd.assertValid(); + const decoded = FDImpl.decode(fd); + return switch (Environment.os) { + else => decoded.value.as_system < 3, + .windows => switch (decoded.kind) { + .system => fd == win32.STDIN_FD or + fd == win32.STDOUT_FD or + fd == win32.STDERR_FD, + .uv => decoded.value.as_uv < 3, + }, + }; + } + + pub fn toJS(value: FileDescriptor, global: *JSC.JSGlobalObject) JSC.JSValue { + return FDImpl.decode(value).toJS(global); + } }; pub const FDImpl = @import("./fd.zig").FDImpl; @@ -2687,3 +2718,5 @@ pub var buffered_stdin = std.io.BufferedReader(4096, std.fs.File.Reader){ }; pub const WindowsSpawnWorkaround = @import("./child_process_windows.zig"); + +pub const exe_suffix = if (Environment.isWindows) ".exe" else ""; diff --git a/src/bundler/bundle_v2.zig b/src/bundler/bundle_v2.zig index fcd9c0c748..0ed79fc240 100644 --- a/src/bundler/bundle_v2.zig +++ b/src/bundler/bundle_v2.zig @@ -2108,7 +2108,7 @@ pub const BundleV2 = struct { } if (this.bun_watcher != null) { - if (empty_result.watcher_data.fd.int() > 0 and empty_result.watcher_data.fd != bun.invalid_fd) { + if (empty_result.watcher_data.fd != .zero and empty_result.watcher_data.fd != bun.invalid_fd) { this.bun_watcher.?.addFile( empty_result.watcher_data.fd, input_files.items(.source)[empty_result.source_index.get()].path.text, @@ -2127,7 +2127,7 @@ pub const BundleV2 = struct { { // to minimize contention, we add watcher here if (this.bun_watcher != null) { - if (result.watcher_data.fd.int() > 0 and result.watcher_data.fd != bun.invalid_fd) { + if (result.watcher_data.fd != .zero and result.watcher_data.fd != bun.invalid_fd) { this.bun_watcher.?.addFile( result.watcher_data.fd, result.source.path.text, @@ -2784,7 +2784,7 @@ pub const ParseTask = struct { file_path.text, task.contents_or_fd.fd.dir, false, - if (task.contents_or_fd.fd.file.int() > 0) + if (task.contents_or_fd.fd.file != .zero) task.contents_or_fd.fd.file else null, @@ -2821,12 +2821,12 @@ pub const ParseTask = struct { errdefer if (task.contents_or_fd == .fd) entry.deinit(allocator); - const will_close_file_descriptor = task.contents_or_fd == .fd and entry.fd.int() > 2 and this.ctx.bun_watcher == null; + const will_close_file_descriptor = task.contents_or_fd == .fd and !entry.fd.isStdio() and this.ctx.bun_watcher == null; if (will_close_file_descriptor) { _ = entry.closeFD(); } - if (!will_close_file_descriptor and entry.fd.int() > 2) task.contents_or_fd = .{ + if (!will_close_file_descriptor and !entry.fd.isStdio()) task.contents_or_fd = .{ .fd = .{ .file = entry.fd, .dir = bun.invalid_fd, diff --git a/src/cache.zig b/src/cache.zig index 81a6c5b5af..08b94e3f77 100644 --- a/src/cache.zig +++ b/src/cache.zig @@ -169,7 +169,7 @@ pub const Fs = struct { var file_handle: std.fs.File = if (_file_handle) |__file| __file.asFile() else undefined; if (_file_handle == null) { - if (FeatureFlags.store_file_descriptors and dirname_fd != bun.invalid_fd and dirname_fd.int() > 0) { + if (FeatureFlags.store_file_descriptors and dirname_fd != bun.invalid_fd and dirname_fd != .zero) { file_handle = (bun.sys.openatA(dirname_fd, std.fs.path.basename(path), std.os.O.RDONLY, 0).unwrap() catch |err| brk: { switch (err) { error.ENOENT => { diff --git a/src/cli.zig b/src/cli.zig index 3a00c082a7..b120458329 100644 --- a/src/cli.zig +++ b/src/cli.zig @@ -776,7 +776,6 @@ pub const Arguments = struct { if (cmd == .AutoCommand or cmd == .RunCommand) { ctx.debug.silent = args.flag("--silent"); - ctx.debug.run_in_bun = args.flag("--bun") or ctx.debug.run_in_bun; if (opts.define) |define| { if (define.keys.len > 0) @@ -784,6 +783,10 @@ pub const Arguments = struct { } } + if (cmd == .RunCommand or cmd == .AutoCommand or cmd == .BunxCommand) { + ctx.debug.run_in_bun = args.flag("--bun") or ctx.debug.run_in_bun; + } + opts.resolve = Api.ResolveMode.lazy; if (jsx_factory != null or @@ -1033,6 +1036,9 @@ const AddCompletions = @import("./cli/add_completions.zig"); /// - `node scripts/postinstall` -> `bun run ./scripts/postinstall` pub var pretend_to_be_node = false; +/// This is set `true` during `Command.which()` if argv0 is "bunx" +pub var is_bunx_exe = false; + pub const Command = struct { var script_name_buf: [bun.MAX_PATH_BYTES]u8 = undefined; @@ -1195,7 +1201,20 @@ pub const Command = struct { argv0; // symlink is argv[0] - if (isBunX(without_exe)) return .BunxCommand; + if (isBunX(without_exe)) { + // if we are bunx, but NOT a symlink to bun. when we run ` install`, we dont + // want to recursively run bunx. so this check lets us peek back into bun install. + if (args_iter.next()) |next| { + if (bun.strings.eqlComptime(next, "add") and + bun.getenvZ("BUN_INTERNAL_BUNX_INSTALL") != null) + { + return .AddCommand; + } + } + + is_bunx_exe = true; + return .BunxCommand; + } if (isNode(without_exe)) { @import("./deps/zig-clap/clap/streaming.zig").warn_on_unrecognized_flag = false; @@ -1375,7 +1394,7 @@ pub const Command = struct { if (comptime bun.fast_debug_build_mode and bun.fast_debug_build_cmd != .BunxCommand) unreachable; const ctx = try Command.Context.create(allocator, log, .BunxCommand); - try BunxCommand.exec(ctx, bun.argv()[1..]); + try BunxCommand.exec(ctx, bun.argv()[if (is_bunx_exe) 0 else 1..]); return; }, .ReplCommand => { @@ -1383,8 +1402,8 @@ pub const Command = struct { if (comptime bun.fast_debug_build_mode and bun.fast_debug_build_cmd != .BunxCommand) unreachable; var ctx = try Command.Context.create(allocator, log, .BunxCommand); ctx.debug.run_in_bun = true; // force the same version of bun used. fixes bun-debug for example - var args = bun.argv()[1..]; - args[0] = @constCast("bun-repl"); + var args = bun.argv()[0..]; + args[1] = @constCast("bun-repl"); try BunxCommand.exec(ctx, args); return; }, @@ -1535,7 +1554,7 @@ pub const Command = struct { // if --help, print help and exit const print_help = brk: { for (bun.argv()) |arg| { - if (strings.eqlComptime(arg, "--help")) { + if (strings.eqlComptime(arg, "--help") or strings.eqlComptime(arg, "-h")) { break :brk true; } } @@ -1565,7 +1584,8 @@ pub const Command = struct { if (print_help or // "bun create --" // "bun create -abc --" - positional_i == 0) + positional_i == 0 or + positionals[0].len == 0) { Command.Tag.printHelp(.CreateCommand, true); Global.exit(0); @@ -1618,9 +1638,10 @@ pub const Command = struct { example_tag != CreateCommandExample.Tag.local_folder; if (use_bunx) { - const bunx_args = try allocator.alloc([:0]const u8, args.len - template_name_start); - bunx_args[0] = try BunxCommand.addCreatePrefix(allocator, template_name); - for (bunx_args[1..], args[template_name_start + 1 ..]) |*dest, src| { + const bunx_args = try allocator.alloc([:0]const u8, 1 + args.len - template_name_start); + bunx_args[0] = "bunx"; + bunx_args[1] = try BunxCommand.addCreatePrefix(allocator, template_name); + for (bunx_args[2..], args[template_name_start + 1 ..]) |*dest, src| { dest.* = src; } diff --git a/src/cli/bunx_command.zig b/src/cli/bunx_command.zig index f8106d6217..d6706c85d9 100644 --- a/src/cli/bunx_command.zig +++ b/src/cli/bunx_command.zig @@ -12,10 +12,13 @@ const std = @import("std"); const Command = @import("../cli.zig").Command; const Run = @import("./run_command.zig").RunCommand; +const debug = Output.scoped(.bunx, false); + pub const BunxCommand = struct { var path_buf: [bun.MAX_PATH_BYTES]u8 = undefined; - /// clones the string + /// Adds `create-` to the string, but also handles scoped packages correctly. + /// Always clones the string in the process. pub fn addCreatePrefix(allocator: std.mem.Allocator, input: []const u8) ![:0]const u8 { const prefixLength = "create-".len; @@ -54,7 +57,9 @@ pub const BunxCommand = struct { return new_str; } - const seconds_cache_valid = 60 * 60 * 24; // 1 day + /// 1 day + const seconds_cache_valid = 60 * 60 * 24; + /// 1 day const nanoseconds_cache_valid = seconds_cache_valid * 1000000000; fn getBinNameFromSubpath(bundler: *bun.Bundler, dir_fd: bun.FileDescriptor, subpath_z: [:0]const u8) ![]const u8 { @@ -200,50 +205,49 @@ pub const BunxCommand = struct { pub fn exec(ctx_: bun.CLI.Command.Context, argv: [][:0]const u8) !void { var ctx = ctx_; - var requests_buf = bun.PackageManager.UpdateRequest.Array.init(0) catch unreachable; - var run_in_bun = ctx.debug.run_in_bun; + // Don't log stuff + ctx.debug.silent = true; var passthrough_list = try std.ArrayList(string).initCapacity(ctx.allocator, argv.len); - var package_name_for_update_request = [1]string{""}; + var maybe_package_name: ?string = null; + var verbose_install = false; { var found_subcommand_name = false; for (argv) |positional| { - if (positional.len == 0) continue; - - if (positional[0] != '-') { - if (!found_subcommand_name) { - found_subcommand_name = true; - if (positional.len == 1 and positional[0] == 'x') - continue; - } - } - - if (!run_in_bun and !found_subcommand_name) { - if (strings.eqlComptime(positional, "--bun")) { - run_in_bun = true; - continue; - } - } - - if (package_name_for_update_request[0].len == 0 and positional.len > 0 and positional[0] != '-') { - package_name_for_update_request[0] = positional; + if (maybe_package_name != null) { + passthrough_list.appendAssumeCapacity(positional); continue; } - try passthrough_list.append(positional); + if (positional.len > 0 and positional[0] == '-') { + if (strings.eqlComptime(positional, "--verbose")) { + verbose_install = true; + } else if (strings.eqlComptime(positional, "--bun") or strings.eqlComptime(positional, "-b")) { + ctx.debug.run_in_bun = true; + } + } else { + if (!found_subcommand_name) { + found_subcommand_name = true; + } else { + maybe_package_name = positional; + } + } } } // check if package_name_for_update_request is empty string or " " - if (package_name_for_update_request[0].len == 0) { + if (maybe_package_name == null or maybe_package_name.?.len == 0) { exitWithUsage(); } + const package_name = maybe_package_name.?; + + var requests_buf = bun.PackageManager.UpdateRequest.Array{}; const update_requests = bun.PackageManager.UpdateRequest.parse( ctx.allocator, ctx.log, - &package_name_for_update_request, + &.{package_name}, &requests_buf, .add, ); @@ -252,15 +256,7 @@ pub const BunxCommand = struct { exitWithUsage(); } - // this shouldn't happen - if (update_requests.len > 1) { - Output.prettyErrorln("error: Only one package can be installed & run at a time right now", .{}); - Global.exit(1); - } - - // Don't log stuff - ctx.debug.silent = true; - + std.debug.assert(update_requests.len == 1); // One positional cannot parse to multiple requests var update_request = update_requests[0]; // if you type "tsc" and TypeScript is not installed: @@ -278,18 +274,19 @@ pub const BunxCommand = struct { update_request.name[index + 1 ..] else update_request.name; + debug("initial_bin_name: {s}", .{initial_bin_name}); // fast path: they're actually using this interchangeably with `bun run` // so we use Bun.which to check var this_bundler: bun.Bundler = undefined; var ORIGINAL_PATH: string = ""; - const force_using_bun = run_in_bun; const root_dir_info = try Run.configureEnvForRun( ctx, &this_bundler, null, true, + true, ); try Run.configurePathForRun( @@ -298,7 +295,7 @@ pub const BunxCommand = struct { &this_bundler, &ORIGINAL_PATH, root_dir_info.abs_path, - force_using_bun, + ctx.debug.run_in_bun, ); const ignore_cwd = this_bundler.env.get("BUN_WHICH_IGNORE_CWD") orelse ""; @@ -313,20 +310,59 @@ pub const BunxCommand = struct { else update_request.version.literal.slice(update_request.version_buf); + // package_fmt is used for the path to install in. const package_fmt = brk: { - if (update_request.version.tag == .github) { - break :brk update_request.version.literal.slice(update_request.version_buf); - } + // Includes the delimiters because we use this as a part of $PATH + const banned_path_chars = switch (Environment.os) { + .windows => ":*?<>|;", + else => ":", + }; - break :brk try std.fmt.allocPrint( - ctx.allocator, - "{s}@{s}", - .{ + const has_banned_char = std.mem.indexOfAny(u8, update_request.name, banned_path_chars) != null or + std.mem.indexOfAny(u8, display_version, banned_path_chars) != null; + + break :brk try if (has_banned_char) + // This branch gets hit usually when a URL is requested as the package + // See https://github.com/oven-sh/bun/issues/3675 + // + // But the requested version will contain the url. + // The colon will break all platforms. + std.fmt.allocPrint(ctx.allocator, "{s}@{s}@{d}", .{ + initial_bin_name, + @tagName(update_request.version.tag), + bun.hash(update_request.name) +% bun.hash(display_version), + }) + else + try std.fmt.allocPrint(ctx.allocator, "{s}@{s}", .{ update_request.name, display_version, - }, - ); + }); }; + debug("package_fmt: {s}", .{package_fmt}); + + // install_param -> used in command 'bun install {what}' + // result_package_name -> used for path 'node_modules/{what}/package.json' + const install_param, const result_package_name = if (update_request.name.len != 0) + .{ + try std.fmt.allocPrint(ctx.allocator, "{s}@{s}", .{ + update_request.name, + display_version, + }), + update_request.name, + } + else + // When there is not a clear package name (URL/GitHub/etc), we force the package name + // to be the same as the calculated initial bin name. This allows us to have a predictable + // node_modules folder structure. + .{ + try std.fmt.allocPrint(ctx.allocator, "{s}@{s}", .{ + initial_bin_name, + display_version, + }), + initial_bin_name, + }; + debug("install_param: {s}", .{install_param}); + debug("result_package_name: {s}", .{result_package_name}); const temp_dir = bun.fs.FileSystem.RealFS.platformTempDir(); @@ -350,16 +386,13 @@ pub const BunxCommand = struct { break :brk new_path.items; }; - - defer { - if (ignore_cwd.len > 0) { - ctx.allocator.free(PATH_FOR_BIN_DIRS); - } - } + defer if (ignore_cwd.len > 0) { + ctx.allocator.free(PATH_FOR_BIN_DIRS); + }; // The bunx cache path is at the following location // - // /bunx--/node_modules/.bin/ + // /bunx--/node_modules/.bin/ // // Reasoning: // - Prefix with "bunx" to identify the bunx cache, make it easier to "rm -r" @@ -370,7 +403,9 @@ pub const BunxCommand = struct { // across users, you run into permission conflicts // - If you set permission to 777, you run into a potential attack vector // where a user can replace the directory with malicious code. - const uid = if (bun.Environment.isPosix) bun.C.getuid() else windowsUserUniqueId(); + // + // If this format changes, please update cache clearing code in package_manager_command.zig + const uid = if (bun.Environment.isPosix) bun.C.getuid() else bun.windows.userUniqueId(); PATH = switch (PATH.len > 0) { inline else => |path_is_nonzero| try std.fmt.allocPrint( ctx.allocator, @@ -379,8 +414,8 @@ pub const BunxCommand = struct { temp_dir, uid, package_fmt, - if (path_is_nonzero) ":" else "", - PATH, + if (path_is_nonzero) &[1]u8{std.fs.path.delimiter} else "", + if (path_is_nonzero) PATH else "", }, ), }; @@ -389,17 +424,16 @@ pub const BunxCommand = struct { const bunx_cache_dir = PATH[0 .. temp_dir.len + "/bunx--".len + package_fmt.len + - if (Environment.isPosix) - std.fmt.count("{d}", .{uid}) - else - 0]; + std.fmt.count("{d}", .{uid})]; - var absolute_in_cache_dir_buf: [bun.MAX_PATH_BYTES]u8 = undefined; + debug("bunx_cache_dir: {s}", .{bunx_cache_dir}); + + var absolute_in_cache_dir_buf: bun.PathBuffer = undefined; var absolute_in_cache_dir = std.fmt.bufPrint( &absolute_in_cache_dir_buf, bun.pathLiteral("{s}/node_modules/.bin/{s}"), .{ bunx_cache_dir, initial_bin_name }, - ) catch unreachable; + ) catch return error.PathTooLong; const passthrough = passthrough_list.items; @@ -482,49 +516,47 @@ pub const BunxCommand = struct { } // 2. The "bin" is possibly not the same as the package name, so we load the package.json to figure out what "bin" to use - // TODO: root_dir_fd was observed on Windows to be zero, which is incorrect. figure out why const root_dir_fd = root_dir_info.getFileDescriptor(); - if (root_dir_fd != .zero) { - if (getBinName(&this_bundler, root_dir_fd, bunx_cache_dir, initial_bin_name)) |package_name_for_bin| { - // if we check the bin name and its actually the same, we don't need to check $PATH here again - if (!strings.eqlLong(package_name_for_bin, initial_bin_name, true)) { - absolute_in_cache_dir = std.fmt.bufPrint(&absolute_in_cache_dir_buf, "{s}/node_modules/.bin/{s}", .{ bunx_cache_dir, package_name_for_bin }) catch unreachable; + std.debug.assert(root_dir_fd != .zero); + if (getBinName(&this_bundler, root_dir_fd, bunx_cache_dir, initial_bin_name)) |package_name_for_bin| { + // if we check the bin name and its actually the same, we don't need to check $PATH here again + if (!strings.eqlLong(package_name_for_bin, initial_bin_name, true)) { + absolute_in_cache_dir = std.fmt.bufPrint(&absolute_in_cache_dir_buf, "{s}/node_modules/.bin/{s}", .{ bunx_cache_dir, package_name_for_bin }) catch unreachable; - // Only use the system-installed version if there is no version specified - if (update_request.version.literal.isEmpty()) { - destination_ = bun.which( - &path_buf, - PATH_FOR_BIN_DIRS, - if (ignore_cwd.len > 0) "" else this_bundler.fs.top_level_dir, - package_name_for_bin, - ); - } - - if (destination_ orelse bun.which( + // Only use the system-installed version if there is no version specified + if (update_request.version.literal.isEmpty()) { + destination_ = 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); - try Run.runBinary( - ctx, - try this_bundler.fs.dirname_store.append(@TypeOf(out), out), - this_bundler.fs.top_level_dir, - this_bundler.env, - passthrough, - null, - ); - // runBinary is noreturn - comptime unreachable; - } + package_name_for_bin, + ); } - } else |err| { - if (err == error.NoBinFound) { - Output.prettyErrorln("error: could not determine executable to run for package {s}", .{update_request.name}); - Global.exit(1); + + 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); + try Run.runBinary( + ctx, + try this_bundler.fs.dirname_store.append(@TypeOf(out), out), + this_bundler.fs.top_level_dir, + this_bundler.env, + passthrough, + null, + ); + // runBinary is noreturn + comptime unreachable; } } + } else |err| { + if (err == error.NoBinFound) { + Output.errGeneric("could not determine executable to run for package {s}", .{update_request.name}); + Global.exit(1); + } } } @@ -537,31 +569,44 @@ pub const BunxCommand = struct { package_json.writeAll("{}\n") catch {}; } - var args_buf = [_]string{ + var args = std.BoundedArray([]const u8, 7).fromSlice(&.{ try std.fs.selfExePathAlloc(ctx.allocator), "add", "--no-summary", - package_fmt, - - // the following two args are stripped off if `do_cache_bust` is false + install_param, + }) catch + unreachable; // upper bound is known + if (do_cache_bust) { // disable the manifest cache when a tag is specified // so that @latest is fetched from the registry - "--no-cache", + args.append("--no-cache") catch + unreachable; // upper bound is known + // forcefully re-install packages in this mode too - "--force", - }; + args.append("--force") catch + unreachable; // upper bound is known + } - const argv_to_use: []const string = args_buf[0 .. args_buf.len - 2 * @as(usize, @intFromBool(!do_cache_bust))]; + if (verbose_install) { + args.append("--verbose") catch + unreachable; // upper bound is known + } + const argv_to_use = args.slice(); + + debug("installing package: {s}", .{bun.fmt.fmtSlice(argv_to_use, " ")}); var child_process = std.ChildProcess.init(argv_to_use, default_allocator); child_process.cwd_dir = bunx_install_dir; + debug("cwd: {}", .{bun.toFD(bunx_install_dir.fd)}); // https://github.com/ziglang/zig/issues/5190 if (Environment.isWindows) { child_process.cwd = bunx_cache_dir; } - const env_map = try this_bundler.env.map.cloneToEnvMap(ctx.allocator); - child_process.env_map = &env_map; + this_bundler.env.map.put("BUN_INTERNAL_BUNX_INSTALL", "true") catch bun.outOfMemory(); + var env_map = try this_bundler.env.map.stdEnvMap(ctx.allocator); + defer env_map.deinit(); + child_process.env_map = env_map.get(); child_process.stderr_behavior = .Inherit; child_process.stdin_behavior = .Inherit; child_process.stdout_behavior = .Inherit; @@ -614,7 +659,7 @@ pub const BunxCommand = struct { } // 2. The "bin" is possibly not the same as the package name, so we load the package.json to figure out what "bin" to use - if (getBinNameFromTempDirectory(&this_bundler, bunx_cache_dir, update_request.name)) |package_name_for_bin| { + if (getBinNameFromTempDirectory(&this_bundler, bunx_cache_dir, result_package_name)) |package_name_for_bin| { if (!strings.eqlLong(package_name_for_bin, initial_bin_name, true)) { absolute_in_cache_dir = std.fmt.bufPrint(&absolute_in_cache_dir_buf, "{s}/node_modules/.bin/{s}", .{ bunx_cache_dir, package_name_for_bin }) catch unreachable; @@ -639,29 +684,7 @@ pub const BunxCommand = struct { } } else |_| {} - Output.prettyErrorln("error: could not determine executable to run for package {s}", .{update_request.name}); + Output.errGeneric("could not determine executable to run for package {s}", .{update_request.name}); Global.exit(1); } }; - -extern fn GetUserNameW( - lpBuffer: bun.windows.LPWSTR, - pcbBuffer: bun.windows.LPDWORD, -) bun.windows.BOOL; - -/// Is not the actual UID of the user, but just a hash of username. -fn windowsUserUniqueId() u32 { - // https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-tsch/165836c1-89d7-4abb-840d-80cf2510aa3e - // UNLEN + 1 - var buf: [257]u16 = undefined; - var size: u32 = buf.len; - if (GetUserNameW(@ptrCast(&buf), &size) == 0) { - if (Environment.isDebug) std.debug.panic("GetUserNameW failed: {}", .{bun.windows.GetLastError()}); - return 0; - } - const name = buf[0..size]; - if (Environment.isWindows) { - Output.scoped(.windowsUserUniqueId, false)("username: {}", .{bun.fmt.utf16(name)}); - } - return bun.hash32(std.mem.sliceAsBytes(name)); -} diff --git a/src/cli/create_command.zig b/src/cli/create_command.zig index bd209664ed..7bb4f0fe31 100644 --- a/src/cli/create_command.zig +++ b/src/cli/create_command.zig @@ -1586,6 +1586,10 @@ pub const CreateCommand = struct { const create_options = try CreateOptions.parse(ctx); const positionals = create_options.positionals; + if (positionals.len == 0) { + bun.CLI.Command.Tag.printHelp(.CreateCommand, false); + Global.crash(); + } var env_loader: DotEnv.Loader = brk: { const map = try ctx.allocator.create(DotEnv.Map); diff --git a/src/cli/install.ps1 b/src/cli/install.ps1 index 285a1b073b..10f2ac6007 100644 --- a/src/cli/install.ps1 +++ b/src/cli/install.ps1 @@ -201,10 +201,10 @@ function Install-Bun { Install-Bun -Version $Version -ForceBaseline $True exit 1 } - # '1073741515' was spotted in the wild, but not clearly documented as a status code: + # '-1073741515' was spotted in the wild, but not clearly documented as a status code: # https://discord.com/channels/876711213126520882/1149339379446325248/1205194965383250081 # http://community.sqlbackupandftp.com/t/error-1073741515-solved/1305 - if (($LASTEXITCODE -eq 3221225781) -or ($LASTEXITCODE -eq 1073741515)) # STATUS_DLL_NOT_FOUND + if (($LASTEXITCODE -eq 3221225781) -or ($LASTEXITCODE -eq -1073741515)) # STATUS_DLL_NOT_FOUND { Write-Output "Install Failed - You are missing a DLL required to run bun.exe" Write-Output "This can be solved by installing the Visual C++ Redistributable from Microsoft:`nSee https://learn.microsoft.com/cpp/windows/latest-supported-vc-redist`nDirect Download -> https://aka.ms/vs/17/release/vc_redist.x64.exe`n`n" diff --git a/src/cli/package_manager_command.zig b/src/cli/package_manager_command.zig index 0afed3a0ab..2924822cbf 100644 --- a/src/cli/package_manager_command.zig +++ b/src/cli/package_manager_command.zig @@ -185,17 +185,54 @@ pub const PackageManagerCommand = struct { Global.crash(); }; - // outpath = Path.normalizeString(outpath, true, .auto); - if (pm.options.positionals.len > 1 and strings.eqlComptime(pm.options.positionals[1], "rm")) { fd.close(); + + var had_err = false; + std.fs.deleteTreeAbsolute(outpath) catch |err| { - Output.prettyErrorln("{s} deleting cache directory", .{@errorName(err)}); - Global.crash(); + Output.err(err, "Could not delete {s}", .{outpath}); + had_err = true; }; - Output.prettyln("Cache directory deleted:\n {s}", .{outpath}); - Global.exit(0); + Output.prettyln("Cleared 'bun install' cache", .{}); + + bunx: { + const tmp = bun.fs.FileSystem.RealFS.platformTempDir(); + const tmp_dir = std.fs.openDirAbsolute(tmp, .{ .iterate = true }) catch |err| { + Output.err(err, "Could not open {s}", .{tmp}); + had_err = true; + break :bunx; + }; + var iter = tmp_dir.iterate(); + + // This is to match 'bunx_command.BunxCommand.exec's logic + const prefix = try std.fmt.allocPrint(ctx.allocator, "bunx-{d}-", .{ + if (bun.Environment.isPosix) bun.C.getuid() else bun.windows.userUniqueId(), + }); + + var deleted: usize = 0; + while (iter.next() catch |err| { + Output.err(err, "Could not read {s}", .{tmp}); + had_err = true; + break :bunx; + }) |entry| { + if (std.mem.startsWith(u8, entry.name, prefix)) { + tmp_dir.deleteTree(entry.name) catch |err| { + Output.err(err, "Could not delete {s}", .{entry.name}); + had_err = true; + continue; + }; + + deleted += 1; + } + } + + Output.prettyln("Cleared {d} cached 'bunx' packages", .{deleted}); + } + + Global.exit(if (had_err) 1 else 0); } + Output.writer().writeAll(outpath) catch {}; Global.exit(0); } else if (strings.eqlComptime(subcommand, "ls")) { diff --git a/src/cli/run_command.zig b/src/cli/run_command.zig index d4eb7d590c..b267056280 100644 --- a/src/cli/run_command.zig +++ b/src/cli/run_command.zig @@ -55,6 +55,8 @@ const Lockfile = @import("../install/lockfile.zig"); const LifecycleScriptSubprocess = bun.install.LifecycleScriptSubprocess; +const windows = std.os.windows; + pub const RunCommand = struct { const shells_to_search = &[_]string{ "bash", @@ -339,9 +341,9 @@ pub const RunCommand = struct { var child_process = std.ChildProcess.init(&argv, allocator); - var buf_map = try env.map.cloneToEnvMap(allocator); - - child_process.env_map = &buf_map; + var buf_map = try env.map.stdEnvMap(allocator); + defer buf_map.deinit(); + child_process.env_map = buf_map.get(); child_process.cwd = cwd; child_process.stderr_behavior = .Inherit; child_process.stdin_behavior = .Inherit; @@ -400,12 +402,20 @@ pub const RunCommand = struct { /// This prevents '"node" exited with ...' when it was actually bun. /// As of writing this is only used for 'runBinary' fn basenameOrBun(str: []const u8) []const u8 { - if (strings.eqlComptime(str, bun_node_dir ++ "/node")) { + // The full path is not used here, because on windows it is dependant on the + // username. Before windows we checked bun_node_dir, but this is not allowed on Windows. + if (strings.hasSuffixComptime(str, "/bun-node/node" ++ bun.exe_suffix)) { return "bun"; } return std.fs.path.basename(str); } + /// On windows, this checks for a `.bunx` file in the same directory as the + /// script If it exists, it will be run instead of the script which is + /// assumed to `bun_shim_impl.exe` + /// + /// This function only returns if an error starting the process is + /// encountered, most other errors are handled by printing and exiting. pub fn runBinary( ctx: Command.Context, executable: []const u8, @@ -413,6 +423,42 @@ pub const RunCommand = struct { env: *DotEnv.Loader, passthrough: []const string, original_script_for_bun_run: ?[]const u8, + ) !noreturn { + // Attempt to find a ".bunx" file on disk, and run it, skipping the + // wrapper exe. we build the full exe path even though we could do + // a relative lookup, because in the case we do find it, we have to + // generate this full path anyways. + if (Environment.isWindows and bun.strings.hasSuffixComptime(executable, ".exe")) { + std.debug.assert(std.fs.path.isAbsolute(executable)); + + // Using @constCast is safe because we know that `direct_launch_buffer` is the data destination + var wpath = @constCast(bun.strings.toNTPath(&BunXFastPath.direct_launch_buffer, executable)); + std.debug.assert(bun.isSliceInBuffer(u16, wpath, &BunXFastPath.direct_launch_buffer)); + + std.debug.assert(wpath.len > bun.windows.nt_object_prefix.len + ".exe".len); + wpath.len += ".bunx".len - ".exe".len; + @memcpy(wpath[wpath.len - "bunx".len ..], comptime bun.strings.w("bunx")); + + BunXFastPath.tryLaunch(ctx, wpath, env, passthrough); + } + + try runBinaryWithoutBunxPath( + ctx, + executable, + cwd, + env, + passthrough, + original_script_for_bun_run, + ); + } + + pub fn runBinaryWithoutBunxPath( + ctx: Command.Context, + executable: []const u8, + cwd: string, + env: *DotEnv.Loader, + passthrough: []const string, + original_script_for_bun_run: ?[]const u8, ) !noreturn { var argv_ = [_]string{executable}; var argv: []const string = &argv_; @@ -426,9 +472,10 @@ pub const RunCommand = struct { var child_process = std.ChildProcess.init(argv, ctx.allocator); - var buf_map = try env.map.cloneToEnvMap(ctx.allocator); + var buf_map = try env.map.stdEnvMap(ctx.allocator); + defer buf_map.deinit(); child_process.cwd = cwd; - child_process.env_map = &buf_map; + child_process.env_map = buf_map.get(); child_process.stderr_behavior = .Inherit; child_process.stdin_behavior = .Inherit; child_process.stdout_behavior = .Inherit; @@ -524,8 +571,11 @@ pub const RunCommand = struct { } pub const bun_node_dir = switch (Environment.os) { - // TODO: - .windows => "TMPDIR", + // This path is almost always a path to a user directory. So it cannot be inlined like + // our uses of /tmp. You can use one of these functions instead: + // - bun.windows.GetTempPathW (native) + // - bun.fs.FileSystem.RealFS.platformTempDir (any platform) + .windows => @compileError("Do not use RunCommand.bun_node_dir on Windows"), .mac => "/private/tmp", else => "/tmp", @@ -534,6 +584,32 @@ pub const RunCommand = struct { else "/bun-debug-node"; + pub fn bunNodeFileUtf8(allocator: std.mem.Allocator) ![:0]const u8 { + if (!Environment.isWindows) return bun_node_dir; + var temp_path_buffer: bun.WPathBuffer = undefined; + var target_path_buffer: bun.PathBuffer = undefined; + const len = bun.windows.GetTempPathW( + temp_path_buffer.len, + @ptrCast(&temp_path_buffer), + ); + if (len == 0) { + return error.FailedToGetTempPath; + } + + const converted = try bun.strings.convertUTF16toUTF8InBuffer( + &target_path_buffer, + temp_path_buffer[0..len], + ); + + const dir_name = "bun-node" ++ if (Environment.git_sha_short.len > 0) "-" ++ Environment.git_sha_short else ""; + const file_name = dir_name ++ "\\node.exe"; + @memcpy(target_path_buffer[converted.len..][0..file_name.len], file_name); + + target_path_buffer[converted.len + file_name.len] = 0; + + return try allocator.dupeZ(u8, target_path_buffer[0 .. converted.len + file_name.len :0]); + } + var self_exe_bin_path_buf: [bun.MAX_PATH_BYTES + 1]u8 = undefined; pub fn createFakeTemporaryNodeExecutable(PATH: *std.ArrayList(u8), optional_bun_path: *string) !void { @@ -562,21 +638,30 @@ pub const RunCommand = struct { argv0 = bun.argv()[0]; } - var retried = false; - while (true) { - inner: { - std.os.symlinkZ(argv0, bun_node_dir ++ "/node") catch |err| { - if (err == error.PathAlreadyExists) break :inner; - if (retried) - return; + if (Environment.isDebug) { + std.fs.deleteTreeAbsolute(bun_node_dir) catch {}; + } + const paths = if (Environment.isDebug) + .{ bun_node_dir ++ "/node", bun_node_dir ++ "/bun" } + else + .{bun_node_dir ++ "/node"}; + inline for (paths) |path| { + var retried = false; + while (true) { + inner: { + std.os.symlinkZ(argv0, path) catch |err| { + if (err == error.PathAlreadyExists) break :inner; + if (retried) + return; - std.fs.makeDirAbsoluteZ(bun_node_dir) catch {}; + std.fs.makeDirAbsoluteZ(bun_node_dir) catch {}; - retried = true; - continue; - }; + retried = true; + continue; + }; + } + break; } - break; } if (PATH.items.len > 0 and PATH.items[PATH.items.len - 1] != std.fs.path.delimiter) { @@ -603,35 +688,37 @@ pub const RunCommand = struct { @memcpy(target_path_buffer[0..prefix.len], prefix); - const dir_name = "bun-node" ++ if (Environment.git_sha_short.len > 0) "-" ++ Environment.git_sha_short else ""; - const file_name = dir_name ++ "\\node.exe\x00"; - @memcpy(target_path_buffer[len + prefix.len ..][0..file_name.len], comptime bun.strings.w(file_name)); - - const file_slice = target_path_buffer[0 .. prefix.len + len + file_name.len - "\x00".len]; + const dir_name = "bun-node" ++ if (Environment.isDebug) + "-debug" + else if (Environment.git_sha_short.len > 0) + "-" ++ Environment.git_sha_short + else + ""; const dir_slice = target_path_buffer[0 .. prefix.len + len + dir_name.len]; const image_path = bun.windows.exePathW(); + inline for (.{ "node.exe", "bun.exe" }) |name| { + const file_name = dir_name ++ "\\" ++ name ++ "\x00"; + @memcpy(target_path_buffer[len + prefix.len ..][0..file_name.len], comptime bun.strings.w(file_name)); - if (Environment.isDebug) { - // the link becomes out of date on rebuild - std.os.unlinkW(file_slice) catch {}; - } + const file_slice = target_path_buffer[0 .. prefix.len + len + file_name.len - "\x00".len]; - if (bun.windows.CreateHardLinkW(@ptrCast(file_slice.ptr), image_path.ptr, null) == 0) { - switch (std.os.windows.kernel32.GetLastError()) { - .ALREADY_EXISTS => {}, - else => { - { - std.debug.assert(target_path_buffer[dir_slice.len] == '\\'); - target_path_buffer[dir_slice.len] = 0; - std.os.mkdirW(target_path_buffer[0..dir_slice.len :0], 0) catch {}; - target_path_buffer[dir_slice.len] = '\\'; - } + if (bun.windows.CreateHardLinkW(@ptrCast(file_slice.ptr), image_path.ptr, null) == 0) { + switch (std.os.windows.kernel32.GetLastError()) { + .ALREADY_EXISTS => {}, + else => { + { + std.debug.assert(target_path_buffer[dir_slice.len] == '\\'); + target_path_buffer[dir_slice.len] = 0; + std.os.mkdirW(target_path_buffer[0..dir_slice.len :0], 0) catch {}; + target_path_buffer[dir_slice.len] = '\\'; + } - if (bun.windows.CreateHardLinkW(@ptrCast(file_slice.ptr), image_path.ptr, null) == 0) { - return; - } - }, + if (bun.windows.CreateHardLinkW(@ptrCast(file_slice.ptr), image_path.ptr, null) == 0) { + return; + } + }, + } } } @@ -650,6 +737,7 @@ pub const RunCommand = struct { this_bundler: *bundler.Bundler, env: ?*DotEnv.Loader, log_errors: bool, + store_root_fd: bool, ) !*DirInfo { const args = ctx.args; this_bundler.* = try bundler.Bundler.init(ctx.allocator, ctx.log, args, env); @@ -659,6 +747,7 @@ pub const RunCommand = struct { this_bundler.resolver.care_about_bin_folder = true; this_bundler.resolver.care_about_scripts = true; + this_bundler.resolver.store_fd = store_root_fd; this_bundler.resolver.opts.load_tsconfig_json = false; this_bundler.options.load_tsconfig_json = false; @@ -686,6 +775,8 @@ pub const RunCommand = struct { return error.CouldntReadCurrentDirectory; }; + this_bundler.resolver.store_fd = false; + if (env == null) { this_bundler.env.loadProcess(); @@ -772,9 +863,11 @@ pub const RunCommand = struct { original_path.* = PATH; } + const bun_node_exe = try bunNodeFileUtf8(ctx.allocator); + const bun_node_dir_win = bun.Dirname.dirname(u8, bun_node_exe) orelse return error.FailedToGetTempPath; const found_node = this_bundler.env.loadNodeJSConfig( this_bundler.fs, - if (force_using_bun) bun_node_dir ++ "/node" else "", + if (force_using_bun) bun_node_exe else "", ) catch false; var needs_to_force_bun = force_using_bun or !found_node; @@ -797,17 +890,17 @@ pub const RunCommand = struct { } if (needs_to_force_bun) { - new_path_len += bun_node_dir.len + 1; + new_path_len += bun_node_dir_win.len + 1; } var new_path = try std.ArrayList(u8).initCapacity(ctx.allocator, new_path_len); if (needs_to_force_bun) { - createFakeTemporaryNodeExecutable(&new_path, &optional_bun_self_path) catch unreachable; + createFakeTemporaryNodeExecutable(&new_path, &optional_bun_self_path) catch bun.outOfMemory(); if (!force_using_bun) { - this_bundler.env.map.put("NODE", bun_node_dir ++ "/node") catch unreachable; - this_bundler.env.map.put("npm_node_execpath", bun_node_dir ++ "/node") catch unreachable; - this_bundler.env.map.put("npm_execpath", optional_bun_self_path) catch unreachable; + this_bundler.env.map.put("NODE", bun_node_exe) catch bun.outOfMemory(); + this_bundler.env.map.put("npm_node_execpath", bun_node_exe) catch bun.outOfMemory(); + this_bundler.env.map.put("npm_execpath", optional_bun_self_path) catch bun.outOfMemory(); } needs_to_force_bun = false; @@ -834,7 +927,7 @@ pub const RunCommand = struct { try new_path.appendSlice(PATH); } - this_bundler.env.map.put("PATH", new_path.items) catch unreachable; + this_bundler.env.map.put("PATH", new_path.items) catch bun.outOfMemory(); } pub fn completions(ctx: Command.Context, default_completions: ?[]const string, reject_list: []const string, comptime filter: Filter) !ShellCompletions { @@ -1253,7 +1346,7 @@ pub const RunCommand = struct { var ORIGINAL_PATH: string = ""; var this_bundler: bundler.Bundler = undefined; - const root_dir_info = try configureEnvForRun(ctx, &this_bundler, null, log_errors); + const root_dir_info = try configureEnvForRun(ctx, &this_bundler, null, log_errors, false); try configurePathForRun(ctx, root_dir_info, &this_bundler, &ORIGINAL_PATH, root_dir_info.abs_path, force_using_bun); this_bundler.env.map.put("npm_lifecycle_event", script_name_to_search) catch unreachable; @@ -1349,18 +1442,16 @@ pub const RunCommand = struct { } if (Environment.isWindows) try_bunx_file: { - const WinBunShimImpl = @import("../install/windows-shim/bun_shim_impl.zig"); - const w = std.os.windows; - const debug = Output.scoped(.BunRunXFastPath, false); - - // Attempt to find a ".bunx" file on disk, and run it, skipping the wrapper exe. - // we build the full exe path even though we could do a relative lookup, because in the case we do find it, we have to generate this full path anyways - var ptr: []u16 = &DirectBinLaunch.direct_launch_buffer; + // Attempt to find a ".bunx" file on disk, and run it, skipping the + // wrapper exe. we build the full exe path even though we could do + // a relative lookup, because in the case we do find it, we have to + // generate this full path anyways. + var ptr: []u16 = &BunXFastPath.direct_launch_buffer; const root = comptime bun.strings.w("\\??\\"); @memcpy(ptr[0..root.len], root); ptr = ptr[4..]; - const cwd_len = w.kernel32.GetCurrentDirectoryW( - DirectBinLaunch.direct_launch_buffer.len - 4, + const cwd_len = windows.kernel32.GetCurrentDirectoryW( + BunXFastPath.direct_launch_buffer.len - 4, ptr.ptr, ); if (cwd_len == 0) break :try_bunx_file; @@ -1375,52 +1466,8 @@ pub const RunCommand = struct { ptr[ext.len] = 0; const l = root.len + cwd_len + prefix.len + script_name_to_search.len + ext.len; - const path_to_use = DirectBinLaunch.direct_launch_buffer[0..l]; - var command_line = DirectBinLaunch.direct_launch_buffer[l..]; - - debug("Attempting to find and load bunx file: '{}'", .{ - bun.fmt.utf16(path_to_use), - }); - if (Environment.allow_assert) { - std.debug.assert(std.fs.path.isAbsoluteWindowsWTF16(path_to_use)); - } - const handle = (bun.sys.openFileAtWindows( - bun.invalid_fd, // absolute path is given - path_to_use, - w.STANDARD_RIGHTS_READ | w.FILE_READ_DATA | w.FILE_READ_ATTRIBUTES | w.FILE_READ_EA | w.SYNCHRONIZE, - w.FILE_OPEN, - w.FILE_NON_DIRECTORY_FILE | w.FILE_SYNCHRONOUS_IO_NONALERT, - ).unwrap() catch |err| { - debug("Failed to open bunx file: '{}'", .{err}); - break :try_bunx_file; - }).cast(); - - var i: usize = 0; - for (ctx.passthrough) |str| { - command_line[i] = ' '; - const result = bun.strings.convertUTF8toUTF16InBuffer(command_line[1 + i ..], str); - i += result.len + 1; - } - - const run_ctx = WinBunShimImpl.FromBunRunContext{ - .handle = handle, - .base_path = path_to_use[4..], - .arguments = command_line[0..i], - .force_use_bun = ctx.debug.run_in_bun, - .direct_launch_with_bun_js = &DirectBinLaunch.directLaunchWithBunJSFromShim, - .cli_context = &ctx, - }; - - if (Environment.isDebug) { - debug("run_ctx.handle: '{}'", .{bun.FDImpl.fromSystem(handle)}); - debug("run_ctx.base_path: '{}'", .{bun.fmt.utf16(run_ctx.base_path)}); - debug("run_ctx.arguments: '{}'", .{bun.fmt.utf16(run_ctx.arguments)}); - debug("run_ctx.force_use_bun: '{}'", .{run_ctx.force_use_bun}); - } - - // this function does not return. spooky - WinBunShimImpl.startupFromBunJS(run_ctx); - comptime unreachable; + const path_to_use = BunXFastPath.direct_launch_buffer[0..l :0]; + BunXFastPath.tryLaunch(ctx, path_to_use, this_bundler.env, ctx.passthrough); } const PATH = this_bundler.env.get("PATH") orelse ""; @@ -1435,24 +1482,8 @@ pub const RunCommand = struct { if (path_for_which.len > 0) { if (which(&path_buf, path_for_which, this_bundler.fs.top_level_dir, script_name_to_search)) |destination| { - // var file = std.fs.openFileAbsoluteZ(destination, .{ .mode = .read_only }) catch |err| { - // if (!log_errors) return false; - - // Output.prettyErrorln("error: {s} opening file: \"{s}\"", .{ err, std.mem.span(destination) }); - // Output.flush(); - // return err; - // }; - // // var outbuf = bun.getFdPath(file.handle, &path_buf2) catch |err| { - // // if (!log_errors) return false; - // // Output.prettyErrorln("error: {s} resolving file: \"{s}\"", .{ err, std.mem.span(destination) }); - // // Output.flush(); - // // return err; - // // }; - - // // file.close(); - const out = bun.asByteSlice(destination); - return try runBinary( + return try runBinaryWithoutBunxPath( ctx, try this_bundler.fs.dirname_store.append(@TypeOf(out), out), this_bundler.fs.top_level_dir, @@ -1522,10 +1553,63 @@ pub const RunCommand = struct { } }; -pub const DirectBinLaunch = struct { - var direct_launch_buffer: bun.WPathBuffer = undefined; +pub const BunXFastPath = struct { + const shim_impl = @import("../install/windows-shim/bun_shim_impl.zig"); + const debug = Output.scoped(.BunXFastPath, false); - fn directLaunchWithBunJSFromShim(wpath: []u16, ctx: *Command.Context) void { + var direct_launch_buffer: bun.WPathBuffer = undefined; + var environment_buffer: bun.WPathBuffer = undefined; + + /// If this returns, it implies the fast path cannot be taken + fn tryLaunch(ctx: Command.Context, path_to_use: [:0]u16, env: *DotEnv.Loader, passthrough: []const []const u8) void { + std.debug.assert(bun.isSliceInBuffer(u16, path_to_use, &BunXFastPath.direct_launch_buffer)); + var command_line = BunXFastPath.direct_launch_buffer[path_to_use.len..]; + + debug("Attempting to find and load bunx file: '{}'", .{bun.fmt.utf16(path_to_use)}); + if (Environment.allow_assert) { + std.debug.assert(std.fs.path.isAbsoluteWindowsWTF16(path_to_use)); + } + const handle = (bun.sys.openFileAtWindows( + bun.invalid_fd, // absolute path is given + path_to_use, + windows.STANDARD_RIGHTS_READ | windows.FILE_READ_DATA | windows.FILE_READ_ATTRIBUTES | windows.FILE_READ_EA | windows.SYNCHRONIZE, + windows.FILE_OPEN, + windows.FILE_NON_DIRECTORY_FILE | windows.FILE_SYNCHRONOUS_IO_NONALERT, + ).unwrap() catch |err| { + debug("Failed to open bunx file: '{}'", .{err}); + return; + }).cast(); + + var i: usize = 0; + for (passthrough) |str| { + command_line[i] = ' '; + const result = bun.strings.convertUTF8toUTF16InBuffer(command_line[1 + i ..], str); + i += result.len + 1; + } + + const run_ctx = shim_impl.FromBunRunContext{ + .handle = handle, + .base_path = path_to_use[4..], + .arguments = command_line[0..i], + .force_use_bun = ctx.debug.run_in_bun, + .direct_launch_with_bun_js = &directLaunchCallback, + .cli_context = &ctx, + .environment = env.map.writeWindowsEnvBlock(&environment_buffer) catch return, + }; + + if (Environment.isDebug) { + debug("run_ctx.handle: '{}'", .{bun.FDImpl.fromSystem(handle)}); + debug("run_ctx.base_path: '{}'", .{bun.fmt.utf16(run_ctx.base_path)}); + debug("run_ctx.arguments: '{}'", .{bun.fmt.utf16(run_ctx.arguments)}); + debug("run_ctx.force_use_bun: '{}'", .{run_ctx.force_use_bun}); + } + + shim_impl.tryStartupFromBunJS(run_ctx); + + debug("did not start via shim", .{}); + } + + fn directLaunchCallback(wpath: []u16, ctx: *const Command.Context) void { const utf8 = bun.strings.convertUTF16toUTF8InBuffer( bun.reinterpretSlice(u8, &direct_launch_buffer), wpath, diff --git a/src/cli/upgrade_command.zig b/src/cli/upgrade_command.zig index 513f69fe1c..df8e24eec1 100644 --- a/src/cli/upgrade_command.zig +++ b/src/cli/upgrade_command.zig @@ -756,12 +756,12 @@ pub const UpgradeCommand = struct { } } - const destination_executable_ = std.fs.selfExePath(¤t_executable_buf) catch return error.UpgradeFailedMissingExecutable; - current_executable_buf[destination_executable_.len] = 0; + const destination_executable = std.fs.selfExePath(¤t_executable_buf) catch return error.UpgradeFailedMissingExecutable; + current_executable_buf[destination_executable.len] = 0; - const target_filename_ = std.fs.path.basename(destination_executable_); - const target_filename = current_executable_buf[destination_executable_.len - target_filename_.len ..][0..target_filename_.len :0]; - const target_dir_ = std.fs.path.dirname(destination_executable_) orelse return error.UpgradeFailedBecauseOfMissingExecutableDir; + const target_filename_ = std.fs.path.basename(destination_executable); + const target_filename = current_executable_buf[destination_executable.len - target_filename_.len ..][0..target_filename_.len :0]; + const target_dir_ = std.fs.path.dirname(destination_executable) orelse return error.UpgradeFailedBecauseOfMissingExecutableDir; // safe because the slash will no longer be in use current_executable_buf[target_dir_.len] = 0; const target_dirname = current_executable_buf[0..target_dir_.len :0]; @@ -829,7 +829,7 @@ pub const UpgradeCommand = struct { target_dirname, target_filename, }); - std.os.rename(destination_executable_, outdated_filename.?) catch |err| { + std.os.rename(destination_executable, outdated_filename.?) catch |err| { save_dir_.deleteTree(version_name) catch {}; Output.prettyErrorln("error: Failed to rename current executable {s}", .{@errorName(err)}); Global.exit(1); @@ -842,11 +842,14 @@ pub const UpgradeCommand = struct { if (comptime Environment.isWindows) { // Attempt to restore the old executable. If this fails, the user will be left without a working copy of bun. - std.os.rename(outdated_filename.?, destination_executable_) catch { + std.os.rename(outdated_filename.?, destination_executable) catch { Output.errGeneric( - \\Failed to move new version of Bun due to {s} + \\Failed to move new version of Bun to {s} due to {s} , - .{@errorName(err)}, + .{ + destination_executable, + @errorName(err), + }, ); Output.errGeneric( \\Failed to restore the working copy of Bun. The installation is now corrupt. @@ -862,13 +865,17 @@ pub const UpgradeCommand = struct { } Output.errGeneric( - \\Failed to move new version of Bun due to {s} + \\Failed to move new version of Bun to {s} to {s} \\ \\Please reinstall Bun manually with the following command: \\ {s} \\ , - .{ @errorName(err), manual_upgrade_command }, + .{ + destination_executable, + @errorName(err), + manual_upgrade_command, + }, ); Global.exit(1); }; @@ -882,13 +889,14 @@ pub const UpgradeCommand = struct { }; env_loader.map.put("IS_BUN_AUTO_UPDATE", "true") catch bun.outOfMemory(); - var buf_map = try env_loader.map.cloneToEnvMap(ctx.allocator); + var std_map = try env_loader.map.stdEnvMap(ctx.allocator); + defer std_map.deinit(); _ = std.ChildProcess.run(.{ .allocator = ctx.allocator, .argv = &completions_argv, .cwd = target_dirname, .max_output_bytes = 4096, - .env_map = &buf_map, + .env_map = std_map.get(), }) catch {}; } @@ -960,7 +968,7 @@ pub const UpgradeCommand = struct { \\Start-Process powershell.exe -WindowStyle Minimized -ArgumentList "-NoProfile","-ExecutionPolicy","Bypass","-Command",'&{{$ErrorActionPreference=''SilentlyContinue''; Get-Process|Where-Object{{ $_.Path -eq ''{s}'' }}|Wait-Process; Remove-Item -Path ''{s}'' -Force }};'; exit , .{ - destination_executable_, + destination_executable, to_remove, }, ); diff --git a/src/deps/uws.zig b/src/deps/uws.zig index 81a002cb07..bc84a9e371 100644 --- a/src/deps/uws.zig +++ b/src/deps/uws.zig @@ -444,8 +444,8 @@ pub fn NewSocketHandler(comptime is_ssl: bool) type { comptime Context: type, ctx: *Context, comptime socket_field_name: []const u8, - ) ?*Context { - const this_socket = connectAnon(host, port, socket_ctx, ctx) orelse return null; + ) !*Context { + const this_socket = try connectAnon(host, port, socket_ctx, ctx); @field(ctx, socket_field_name) = this_socket; return ctx; } @@ -478,8 +478,8 @@ pub fn NewSocketHandler(comptime is_ssl: bool) type { comptime Context: type, ctx: *Context, comptime socket_field_name: []const u8, - ) ?*Context { - const this_socket = connectUnixAnon(path, socket_ctx, ctx) orelse return null; + ) !*Context { + const this_socket = try connectUnixAnon(path, socket_ctx, ctx); @field(ctx, socket_field_name) = this_socket; return ctx; } @@ -488,56 +488,78 @@ pub fn NewSocketHandler(comptime is_ssl: bool) type { path: []const u8, socket_ctx: *SocketContext, ctx: *anyopaque, - ) ?ThisSocket { + ) !ThisSocket { debug("connect(unix:{s})", .{path}); var stack_fallback = std.heap.stackFallback(1024, bun.default_allocator); var allocator = stack_fallback.get(); - const path_ = allocator.dupeZ(u8, path) catch return null; + const path_ = allocator.dupeZ(u8, path) catch bun.outOfMemory(); defer allocator.free(path_); - const socket = us_socket_context_connect_unix(comptime ssl_int, socket_ctx, path_, 0, 8) orelse return null; + const socket = us_socket_context_connect_unix(comptime ssl_int, socket_ctx, path_, 0, 8) orelse + return error.FailedToOpenSocket; const socket_ = ThisSocket{ .socket = socket }; const holder = socket_.ext(*anyopaque) orelse { if (comptime bun.Environment.allow_assert) unreachable; _ = us_socket_close_connecting(comptime ssl_int, socket); - return null; + return error.FailedToOpenSocket; }; holder.* = ctx; return socket_; } pub fn connectAnon( - host: []const u8, + raw_host: []const u8, port: i32, socket_ctx: *SocketContext, ptr: *anyopaque, - ) ?ThisSocket { - debug("connect({s}, {d})", .{ host, port }); + ) !ThisSocket { + debug("connect({s}, {d})", .{ raw_host, port }); var stack_fallback = std.heap.stackFallback(1024, bun.default_allocator); var allocator = stack_fallback.get(); - const host_: ?[*:0]u8 = brk: { + const host: ?[*:0]u8 = brk: { // getaddrinfo expects `node` to be null if localhost - if (host.len < 6 and (bun.strings.eqlComptime(host, "[::1]") or bun.strings.eqlComptime(host, "[::]"))) { + if (raw_host.len < 6 and (bun.strings.eqlComptime(raw_host, "[::1]") or bun.strings.eqlComptime(raw_host, "[::]"))) { break :brk null; } - break :brk allocator.dupeZ(u8, host) catch return null; + break :brk allocator.dupeZ(u8, raw_host) catch bun.outOfMemory(); }; - defer if (host_) |host__| allocator.free(host__[0..host.len]); + defer if (host) |allocated_host| allocator.free(allocated_host[0..raw_host.len]); - const socket = us_socket_context_connect(comptime ssl_int, socket_ctx, host_, port, null, 0, @sizeOf(*anyopaque)) orelse return null; - const socket_ = ThisSocket{ .socket = socket }; + const us_socket = us_socket_context_connect( + comptime ssl_int, + socket_ctx, + host, + port, + null, + 0, + @sizeOf(*anyopaque), + ) orelse { + if (Environment.isWindows) { + try bun.windows.WSAGetLastError(); + } else { + // TODO(@paperdave): On POSIX, this will call getaddrinfo + socket + // the former of these does not set errno, and usockets does not have + // a way to propogate the error. + // + // This is caught in the wild: https://github.com/oven-sh/bun/issues/6381 + // and we can definitely report a better here. It just is tricky. + } + return error.FailedToOpenSocket; + }; - const holder = socket_.ext(*anyopaque) orelse { + const socket = ThisSocket{ .socket = us_socket }; + + const holder = socket.ext(*anyopaque) orelse { if (comptime bun.Environment.allow_assert) unreachable; - _ = us_socket_close_connecting(comptime ssl_int, socket); - return null; + _ = us_socket_close_connecting(comptime ssl_int, us_socket); + return error.FailedToOpenSocket; }; holder.* = ptr; - return socket_; + return socket; } pub fn unsafeConfigure( diff --git a/src/env_loader.zig b/src/env_loader.zig index eef6834949..405595c9c6 100644 --- a/src/env_loader.zig +++ b/src/env_loader.zig @@ -273,7 +273,7 @@ pub const Loader = struct { prefix: string, allocator: std.mem.Allocator, ) !void { - var iter = this.map.iter(); + var iter = this.map.iterator(); var key_count: usize = 0; var string_map_hashes = try allocator.alloc(u64, framework_defaults.keys.len); defer allocator.free(string_map_hashes); @@ -1032,7 +1032,7 @@ const Parser = struct { }; } if (comptime !is_process) { - var it = map.iter(); + var it = map.iterator(); while (it.next()) |entry| { if (count > 0) { count -= 1; @@ -1095,34 +1095,75 @@ pub const Map = struct { return envp_buf; } - pub fn cloneToEnvMap(this: *Map, allocator: std.mem.Allocator) !std.process.EnvMap { + /// Returns a wrapper around the std.process.EnvMap that does not duplicate the memory of + /// the keys and values, but instead points into the memory of the bun env map. + /// + /// To prevent + pub fn stdEnvMap(this: *Map, allocator: std.mem.Allocator) !StdEnvMapWrapper { var env_map = std.process.EnvMap.init(allocator); - var iter_ = this.map.iterator(); - while (iter_.next()) |entry| { + var iter = this.map.iterator(); + while (iter.next()) |entry| { // Allow var from .env.development or .env.production to be loaded again if (!entry.value_ptr.conditional) { - // TODO(@paperdave): this crashes on windows. i remember there being a merge conflict with these two implementations. not sure what we should keep - if (Environment.isWindows) { - try env_map.put(@constCast(entry.key_ptr.*), @constCast(entry.value_ptr.value)); - } else { - try env_map.putMove(@constCast(entry.key_ptr.*), @constCast(entry.value_ptr.value)); - } + try env_map.hash_map.put(entry.key_ptr.*, entry.value_ptr.value); } } - return env_map; + return .{ .unsafe_map = env_map }; + } + + pub const StdEnvMapWrapper = struct { + unsafe_map: std.process.EnvMap, + + pub fn get(this: *const StdEnvMapWrapper) *const std.process.EnvMap { + return &this.unsafe_map; + } + + pub fn deinit(this: *StdEnvMapWrapper) void { + this.unsafe_map.hash_map.deinit(); + } + }; + + /// Write the Windows environment block into a buffer + /// This can be passed to CreateProcessW's lpEnvironment parameter + pub fn writeWindowsEnvBlock(this: *Map, result: *[32767]u16) ![*]const u16 { + var it = this.map.iterator(); + var i: usize = 0; + while (it.next()) |pair| { + i += bun.strings.convertUTF8toUTF16InBuffer(result[i..], pair.key_ptr.*).len; + if (i + 7 >= result.len) return error.TooManyEnvironmentVariables; + result[i] = '='; + i += 1; + i += bun.strings.convertUTF8toUTF16InBuffer(result[i..], pair.value_ptr.*.value).len; + if (i + 5 >= result.len) return error.TooManyEnvironmentVariables; + result[i] = 0; + i += 1; + } + result[i] = 0; + i += 1; + result[i] = 0; + i += 1; + result[i] = 0; + i += 1; + result[i] = 0; + i += 1; + + return result[0..].ptr; } pub inline fn init(allocator: std.mem.Allocator) Map { return Map{ .map = HashTable.init(allocator) }; } - pub inline fn iter(this: *Map) HashTable.Iterator { + pub inline fn iterator(this: *Map) HashTable.Iterator { return this.map.iterator(); } pub inline fn put(this: *Map, key: string, value: string) !void { + if (Environment.isWindows and Environment.allow_assert) { + std.debug.assert(bun.strings.indexOfChar(key, '\x00') == null); + } try this.map.put(key, .{ .value = value, .conditional = false, @@ -1163,10 +1204,10 @@ pub const Map = struct { } pub fn jsonStringify(self: *const @This(), writer: anytype) !void { - var iterator = self.map.iterator(); + var iter = self.map.iterator(); _ = try writer.write("{"); - while (iterator.next()) |entry| { + while (iter.next()) |entry| { _ = try writer.write("\n "); writer.write(entry.key_ptr.*) catch unreachable; @@ -1175,7 +1216,7 @@ pub const Map = struct { writer.write(entry.value_ptr.*) catch unreachable; - if (iterator.index <= self.map.count() - 1) { + if (iter.index <= self.map.count() - 1) { _ = try writer.write(", "); } } @@ -1203,6 +1244,10 @@ pub const Map = struct { .conditional = false, }); } + + pub fn remove(this: *Map, key: string) void { + this.map.remove(key); + } }; pub var instance: ?*Loader = null; diff --git a/src/fd.zig b/src/fd.zig index dfc8c360d1..febaa87998 100644 --- a/src/fd.zig +++ b/src/fd.zig @@ -162,7 +162,7 @@ pub const FDImpl = packed struct { } pub fn decode(fd: bun.FileDescriptor) FDImpl { - return @bitCast(fd.int()); + return @bitCast(@intFromEnum(fd)); } /// When calling this function, you should consider the FD struct to now be invalid. diff --git a/src/feature_flags.zig b/src/feature_flags.zig index 2a0d41ba97..df9db50a15 100644 --- a/src/feature_flags.zig +++ b/src/feature_flags.zig @@ -6,7 +6,8 @@ pub const keep_alive = false; pub const print_ast = false; pub const disable_printing_null = false; -// This was a ~5% performance improvement +/// Store and reuse file descriptors during module resolution +/// This was a ~5% performance improvement pub const store_file_descriptors = !env.isBrowser; pub const css_in_js_import_behavior = CSSInJSImportBehavior.facade; diff --git a/src/fmt.zig b/src/fmt.zig index d4291ab307..e285788deb 100644 --- a/src/fmt.zig +++ b/src/fmt.zig @@ -1176,3 +1176,25 @@ pub fn fmtDurationOneDecimal(ns: u64) std.fmt.Formatter(formatDurationOneDecimal return .{ .data = FormatDurationData{ .ns = ns } }; } // }; + +pub fn fmtSlice(data: anytype, comptime delim: []const u8) FormatSlice(@TypeOf(data), delim) { + return .{ .slice = data }; +} + +fn FormatSlice(comptime T: type, comptime delim: []const u8) type { + std.debug.assert(@typeInfo(T).Pointer.size == .Slice); + + return struct { + slice: T, + + pub fn format(self: @This(), comptime format_str: []const u8, _: fmt.FormatOptions, writer: anytype) !void { + if (self.slice.len == 0) return; + const f = "{" ++ format_str ++ "}"; + try writer.print(f, .{self.slice[0]}); + for (self.slice[1..]) |item| { + if (delim.len > 0) try writer.writeAll(delim); + try writer.print(f, .{item}); + } + } + }; +} diff --git a/src/fs.zig b/src/fs.zig index f38bd70de3..317865cb6b 100644 --- a/src/fs.zig +++ b/src/fs.zig @@ -7,7 +7,7 @@ const Environment = bun.Environment; const strings = bun.strings; const MutableString = bun.MutableString; const StoredFileDescriptorType = bun.StoredFileDescriptorType; -const FileDescriptorType = bun.FileDescriptor; +const FileDescriptor = bun.FileDescriptor; const FeatureFlags = bun.FeatureFlags; const stringZ = bun.stringZ; const default_allocator = bun.default_allocator; @@ -66,7 +66,7 @@ pub const FileSystem = struct { return tmpdir_handle.?; } - pub fn getFdPath(this: *const FileSystem, fd: FileDescriptorType) ![]const u8 { + pub fn getFdPath(this: *const FileSystem, fd: FileDescriptor) ![]const u8 { var buf: [bun.MAX_PATH_BYTES]u8 = undefined; const dir = try bun.getFdPath(fd, &buf); return try this.dirname_store.append([]u8, dir); @@ -83,18 +83,18 @@ pub const FileSystem = struct { }); } - pub var max_fd: FileDescriptorType = .zero; + pub var max_fd: std.os.fd_t = 0; + + pub inline fn setMaxFd(fd: std.os.fd_t) void { + if (Environment.isWindows) { + return; + } - pub inline fn setMaxFd(fd: anytype) void { if (!FeatureFlags.store_file_descriptors) { return; } - if (comptime @TypeOf(fd) == *anyopaque) { - max_fd = @enumFromInt(@max(@intFromPtr(fd), max_fd.int())); - } else { - max_fd = @enumFromInt(@max(fd, max_fd.int())); - } + max_fd = @max(fd, max_fd); } pub var instance_loaded: bool = false; pub var instance: FileSystem = undefined; @@ -747,8 +747,31 @@ pub const FileSystem = struct { return true; } + if (Environment.isWindows) { + // 'false' is okay here because windows gives you a seemingly unlimited number of open + // file handles, while posix has a lower limit. + // + // This limit does not extend to the C-Runtime which is only 512 to 8196 or so, + // but we know that all resolver-related handles are not C-Runtime handles because + // `setMaxFd` on Windows (besides being a no-op) only takes in `HANDLE`. + // + // Handles are automatically closed when the process exits as stated here: + // https://learn.microsoft.com/en-us/windows/win32/procthread/terminating-a-process + // But in a crazy experiment to find the upper-bound of the number of open handles, + // I found that opening upwards of 500k to a million handles in a single process + // would cause the process to hang while closing. This might just be Windows slowly + // closing the handles, not sure. This is likely not something to worry about. + // + // If it is decided that not closing files ever is a bad idea. This should be + // replaced with some form of intelligent count of how many files we opened. + // On POSIX we can get away with measuring how high `fd` gets because it typically + // assigns these descriptors in ascending order (1 2 3 ...). Windows does not + // guarantee this. + return false; + } + // If we're not near the max amount of open files, don't worry about it. - return !(rfs.file_limit > 254 and rfs.file_limit > (FileSystem.max_fd.int() + 1) * 2); + return !(rfs.file_limit > 254 and rfs.file_limit > (FileSystem.max_fd + 1) * 2); } /// Returns `true` if an entry was removed diff --git a/src/http.zig b/src/http.zig index 5b54751c84..644c2ed391 100644 --- a/src/http.zig +++ b/src/http.zig @@ -634,18 +634,13 @@ fn NewHTTPContext(comptime ssl: bool) type { pub fn connectSocket(this: *@This(), client: *HTTPClient, socket_path: []const u8) !HTTPSocket { client.connected_url = if (client.http_proxy) |proxy| proxy else client.url; - - if (HTTPSocket.connectUnixAnon( + const socket = try HTTPSocket.connectUnixAnon( socket_path, this.us_socket_context, - undefined, - )) |socket| { - client.allow_retry = false; - socket.ext(**anyopaque).?.* = bun.cast(**anyopaque, ActiveSocket.init(client).ptr()); - return socket; - } - - return error.FailedToOpenSocket; + ActiveSocket.init(client).ptr(), + ); + client.allow_retry = false; + return socket; } pub fn connect(this: *@This(), client: *HTTPClient, hostname_: []const u8, port: u16) !HTTPSocket { @@ -669,18 +664,14 @@ fn NewHTTPContext(comptime ssl: bool) type { } } - if (HTTPSocket.connectAnon( + const socket = try HTTPSocket.connectAnon( hostname, port, this.us_socket_context, - undefined, - )) |socket| { - client.allow_retry = false; - socket.ext(**anyopaque).?.* = bun.cast(**anyopaque, ActiveSocket.init(client).ptr()); - return socket; - } - - return error.FailedToOpenSocket; + ActiveSocket.init(client).ptr(), + ); + client.allow_retry = false; + return socket; } }; } @@ -711,10 +702,6 @@ pub const HTTPThread = struct { const threadlog = Output.scoped(.HTTPThread, true); - const FakeStruct = struct { - trash: i64 = 0, - }; - pub fn init() !void { if (http_thread_loaded.swap(true, .SeqCst)) { return; @@ -735,18 +722,17 @@ pub const HTTPThread = struct { .{ .stack_size = bun.default_thread_stack_size, }, - comptime onStart, - .{ - FakeStruct{}, - }, + onStart, + .{}, ); thread.detach(); } - pub fn onStart(_: FakeStruct) void { + pub fn onStart() void { Output.Source.configureNamedThread("HTTP Client"); default_arena = Arena.init() catch unreachable; default_allocator = default_arena.allocator(); + const loop = bun.uws.Loop.create(struct { pub fn wakeup(_: *uws.Loop) callconv(.C) void { http_thread.drainEvents(); @@ -755,6 +741,12 @@ pub const HTTPThread = struct { pub fn post(_: *uws.Loop) callconv(.C) void {} }); + if (Environment.isWindows) { + _ = std.os.getenvW(comptime bun.strings.w("SystemRoot")) orelse { + std.debug.panic("The %SystemRoot% environment variable is not set. Bun needs this set in order for network requests to work.", .{}); + }; + } + http_thread.loop = loop; http_thread.http_context.init() catch @panic("Failed to init http context"); http_thread.https_context.init() catch @panic("Failed to init https context"); @@ -1367,7 +1359,7 @@ pub const InternalState = struct { request_body: []const u8 = "", original_request_body: HTTPRequestBody = .{ .bytes = "" }, request_sent_len: usize = 0, - fail: anyerror = error.NoError, + fail: ?anyerror = null, request_stage: HTTPStage = .pending, response_stage: HTTPStage = .pending, certificate_info: ?CertificateInfo = null, @@ -1995,13 +1987,9 @@ pub const AsyncHTTP = struct { http_thread.schedule(batch); while (true) { const result: HTTPClientResult = ctx.channel.readItem() catch unreachable; - if (!result.isSuccess()) { - return result.fail; - } + if (result.fail) |e| return e; std.debug.assert(result.metadata != null); - if (result.metadata) |metadata| { - return metadata.response; - } + return result.metadata.?.response; } unreachable; @@ -2263,13 +2251,18 @@ fn start_(this: *HTTPClient, comptime is_ssl: bool) void { } var socket = http_thread.connect(this, is_ssl) catch |err| { + if (Environment.isDebug) { + if (@errorReturnTrace()) |trace| { + std.debug.dumpStackTrace(trace.*); + } + } this.fail(err); return; }; if (socket.isClosed() and (this.state.response_stage != .done and this.state.response_stage != .fail)) { this.fail(error.ConnectionClosed); - std.debug.assert(this.state.fail != error.NoError); + std.debug.assert(this.state.fail != null); return; } } @@ -2989,7 +2982,7 @@ pub fn progressUpdate(this: *HTTPClient, comptime is_ssl: bool, ctx: *NewHTTPCon const body = out_str.*; const result = this.toResult(); const is_done = !result.has_more; - if (this.state.is_redirect_pending and this.state.fail == error.NoError) { + if (this.state.is_redirect_pending and this.state.fail == null) { if (this.state.isDone()) { this.doRedirect(is_ssl, ctx, socket); } @@ -3041,7 +3034,8 @@ pub fn progressUpdate(this: *HTTPClient, comptime is_ssl: bool, ctx: *NewHTTPCon pub const HTTPClientResult = struct { body: ?*MutableString = null, has_more: bool = false, - fail: anyerror = error.NoError, + fail: ?anyerror = null, + /// Owns the response metadata aka headers, url and status code metadata: ?HTTPResponseMetadata = null, @@ -3060,15 +3054,15 @@ pub const HTTPClientResult = struct { }; pub fn isSuccess(this: *const HTTPClientResult) bool { - return this.fail == error.NoError; + return this.fail == null; } pub fn isTimeout(this: *const HTTPClientResult) bool { - return this.fail == error.Timeout; + return if (this.fail) |e| e == error.Timeout else false; } pub fn isAbort(this: *const HTTPClientResult) bool { - return this.fail == error.Aborted; + return if (this.fail) |e| e == error.Aborted else false; } pub const Callback = struct { @@ -3121,7 +3115,7 @@ pub fn toResult(this: *HTTPClient) HTTPClientResult { .redirected = this.remaining_redirect_count != default_redirect_count, .fail = this.state.fail, // check if we are reporting cert errors, do not have a fail state and we are not done - .has_more = this.state.fail == error.NoError and !this.state.isDone(), + .has_more = this.state.fail == null and !this.state.isDone(), .body_size = body_size, .certificate_info = null, }; @@ -3131,7 +3125,7 @@ pub fn toResult(this: *HTTPClient) HTTPClientResult { .metadata = null, .fail = this.state.fail, // check if we are reporting cert errors, do not have a fail state and we are not done - .has_more = certificate_info != null or (this.state.fail == error.NoError and !this.state.isDone()), + .has_more = certificate_info != null or (this.state.fail == null and !this.state.isDone()), .body_size = body_size, .certificate_info = certificate_info, }; diff --git a/src/http/websocket_http_client.zig b/src/http/websocket_http_client.zig index c04ed4ce70..187b6c4f4e 100644 --- a/src/http/websocket_http_client.zig +++ b/src/http/websocket_http_client.zig @@ -319,7 +319,8 @@ pub fn NewHTTPUpgradeClient(comptime ssl: bool) type { out.tcp.?.timeout(120); return out; - } else { + } else |_| { + // TODO: handle error better than just returning null client.clearData(); client.destroy(); } diff --git a/src/install/install.zig b/src/install/install.zig index 84ac58f690..523dfd02db 100644 --- a/src/install/install.zig +++ b/src/install/install.zig @@ -668,9 +668,7 @@ const Task = struct { ) catch |err| { if (comptime Environment.isDebug) { if (@errorReturnTrace()) |trace| { - _ = trace; // autofix - - // std.debug.dumpStackTrace(trace.*); + std.debug.dumpStackTrace(trace.*); } } @@ -2125,6 +2123,7 @@ pub const PackageManager = struct { this_bundler, this.env, log_level != .silent, + false, ); const init_cwd_gop = try this.env.map.getOrPutWithoutValue("INIT_CWD"); @@ -4692,7 +4691,7 @@ pub const PackageManager = struct { null, logger.Loc.Empty, manager.allocator, - "warn: {s} downloading package manifest {s}", + "{s} downloading package manifest {s}", .{ bun.span(@errorName(err)), name.slice() }, ) catch unreachable; } diff --git a/src/install/repository.zig b/src/install/repository.zig index 228d10c857..d37288b09e 100644 --- a/src/install/repository.zig +++ b/src/install/repository.zig @@ -110,7 +110,8 @@ pub const Repository = extern struct { cwd: if (Environment.isWindows) string else std.fs.Dir, argv: []const string, ) !string { - var buf_map = try env.map.cloneToEnvMap(allocator); + var std_map = try env.map.stdEnvMap(allocator); + defer std_map.deinit(); const result = if (comptime Environment.isWindows) try std.ChildProcess.run(.{ @@ -118,15 +119,14 @@ pub const Repository = extern struct { .argv = argv, // windows `std.ChildProcess.run` uses `cwd` instead of `cwd_dir` .cwd = cwd, - .env_map = &buf_map, + .env_map = std_map.get(), }) else try std.ChildProcess.run(.{ .allocator = allocator, .argv = argv, .cwd_dir = cwd, - - .env_map = &buf_map, + .env_map = std_map.get(), }); switch (result.term) { diff --git a/src/install/windows-shim/BinLinkingShim.zig b/src/install/windows-shim/BinLinkingShim.zig index ae8438d7e3..a229f79173 100644 --- a/src/install/windows-shim/BinLinkingShim.zig +++ b/src/install/windows-shim/BinLinkingShim.zig @@ -30,13 +30,17 @@ shebang: ?Shebang, /// These arbitrary numbers will probably not show up in the other fields. /// This will reveal off-by-one mistakes. pub const VersionFlag = enum(u13) { - pub const current = .v3; + pub const current = .v4; v1 = 5474, - // Fix bug where paths were not joined correctly + /// Fix bug where paths were not joined correctly v2 = 5475, - // Added an error message for when the process is not found + /// Added an error message for when the process is not found v3 = 5476, + /// Added a flag to tell if the shebang is exactly "node" This is used in an + /// automatic fallback path where if "node" is asked for, but not present, + /// it will retry the spawn with "bun". + v4 = 5477, _, }; @@ -44,7 +48,7 @@ pub const Flags = packed struct(u16) { // this is set if the shebang content is "node" or "bun" is_node_or_bun: bool, // this is for validation that the shim is not corrupt and to detect offset memory reads - is_valid: bool = true, + is_node: bool, // indicates if a shebang is present has_shebang: bool, @@ -53,13 +57,14 @@ pub const Flags = packed struct(u16) { pub fn isValid(flags: Flags) bool { const mask: u16 = @bitCast(Flags{ .is_node_or_bun = false, - .is_valid = true, + .is_node = false, .has_shebang = false, .version_tag = @enumFromInt(std.math.maxInt(u13)), }); const compare_to: u16 = @bitCast(Flags{ .is_node_or_bun = false, + .is_node = false, .has_shebang = false, }); @@ -231,12 +236,17 @@ pub fn encodeInto(options: @This(), buf: []u8) !void { wbuf = wbuf[2..]; const is_node_or_bun = if (options.shebang) |s| s.is_bun else false; - const flags = Flags{ + var flags = Flags{ .has_shebang = options.shebang != null, .is_node_or_bun = is_node_or_bun, + .is_node = false, }; if (options.shebang) |s| { + flags.is_node = bun.strings.hasPrefixComptime(s.launcher, "node") and + (s.launcher.len == 4 or s.launcher[4] == ' '); + if (flags.is_node) std.debug.assert(flags.is_node_or_bun); + const encoded = bun.strings.convertUTF8toUTF16InBuffer( wbuf[0..s.utf16_len], s.launcher, diff --git a/src/install/windows-shim/build.zig b/src/install/windows-shim/build.zig index 0f369f33bc..cc52098147 100644 --- a/src/install/windows-shim/build.zig +++ b/src/install/windows-shim/build.zig @@ -1,7 +1,8 @@ const std = @import("std"); +/// TODO(@paperdave): properly integrate this with the rest of Bun's build system. +/// There is no reason that this is a separate Zig project with build artifacts committed. pub fn build(b: *std.Build) void { - // TODO(@paperdave): arm support const target = b.standardTargetOptions(.{ .default_target = .{ .cpu_model = .{ .explicit = &std.Target.x86.cpu.nehalem }, diff --git a/src/install/windows-shim/bun_shim_impl.exe b/src/install/windows-shim/bun_shim_impl.exe index 5a81cbcafb..ce5a5eb726 100755 Binary files a/src/install/windows-shim/bun_shim_impl.exe and b/src/install/windows-shim/bun_shim_impl.exe differ diff --git a/src/install/windows-shim/bun_shim_impl.zig b/src/install/windows-shim/bun_shim_impl.zig index 428c65c101..b36f506cfc 100644 --- a/src/install/windows-shim/bun_shim_impl.zig +++ b/src/install/windows-shim/bun_shim_impl.zig @@ -34,11 +34,22 @@ //! Prior Art: //! - https://github.com/ScoopInstaller/Shim/blob/master/src/shim.cs //! -//! The compiled binary is 10752 bytes and is `@embedFile`d into Bun itself. +//! The compiled binary is 12800 bytes and is `@embedFile`d into Bun itself. //! When this file is updated, the new binary should be compiled and BinLinkingShim.VersionFlag.current should be updated. -const std = @import("std"); const builtin = @import("builtin"); -const bun = @import("root").bun; +const dbg = builtin.mode == .Debug; + +const std = @import("std"); +const w = std.os.windows; +const assert = std.debug.assert; +const fmt16 = std.unicode.fmtUtf16le; + +const is_standalone = !@hasDecl(@import("root"), "JavaScriptCore"); +const bun = if (!is_standalone) @import("root").bun else @compileError("cannot use 'bun' in standalone build of bun_shim_impl"); +const bunDebugMessage = bun.Output.scoped(.bun_shim_impl, true); +const callmod_inline = if (is_standalone) std.builtin.CallModifier.always_inline else bun.callmod_inline; + +const Flags = @import("./BinLinkingShim.zig").Flags; pub inline fn wliteral(comptime str: []const u8) []const u16 { if (!@inComptime()) @compileError("strings.w() must be called in a comptime context"); @@ -54,18 +65,6 @@ pub inline fn wliteral(comptime str: []const u8) []const u16 { return Static.literal; } -const is_standalone = !@hasDecl(@import("root"), "bun"); -const bunDebugMessage = bun.Output.scoped(.bun_shim_impl, true); - -const dbg = builtin.mode == .Debug; - -const Flags = @import("./BinLinkingShim.zig").Flags; - -const assert = std.debug.assert; -const fmt16 = std.unicode.fmtUtf16le; - -const w = std.os.windows; - /// A copy of all ntdll declarations this program uses const nt = struct { const Status = w.NTSTATUS; @@ -129,7 +128,7 @@ fn debug(comptime fmt: []const u8, args: anytype) void { if (!is_standalone) { bunDebugMessage(fmt, args); } else { - printError(fmt, args); + std.log.debug(if (fmt[fmt.len - 1] == '\n') fmt else fmt ++ "\n", args); } } @@ -146,14 +145,17 @@ const FailReason = enum { InvalidShimDataSize, ShimNotFound, CreateProcessFailed, + /// When encountering this outside of standalone mode, you should fallback + /// to running the '.exe' file, not printing this error. InvalidShimValidation, InvalidShimBounds, CouldNotDirectLaunch, BinNotFound, InterpreterNotFound, + InterpreterNotFoundBun, ElevationRequired, - pub fn render(reason: FailReason) []const u8 { + pub fn getFormatTemplate(reason: FailReason) []const u8 { return switch (reason) { .NoDirname => "could not find node_modules path", @@ -166,7 +168,8 @@ const FailReason = enum { // The difference between these two is that one is with a shebang (#!/usr/bin/env node) and // the other is without. This is a helpful distinction because it can detect if something // like node or bun is not in %path%, vs the actual executable was not installed in node_modules. - .InterpreterNotFound => "interpreter executable could not be found", + .InterpreterNotFound => "interpreter executable \"{s}\" not found in %PATH%", + .InterpreterNotFoundBun => "bun is not installed in %PATH%", .BinNotFound => "bin executable does not exist on disk", .ElevationRequired => "process requires elevation", .CreateProcessFailed => "could not create process", @@ -178,6 +181,56 @@ const FailReason = enum { unreachable, }; } + + pub fn format(reason: FailReason, comptime fmt: []const u8, _: std.fmt.FormatOptions, writer: anytype) !void { + if (fmt.len != 0) @compileError("FailReason.format() only takes empty format string"); + + if (!is_standalone and bun.Environment.allow_assert and reason == .InvalidShimValidation) { + @panic("Internal Assertion: When encountering FailReason.InvalidShimValidation, you must not print the error, but rather fallback to running the .exe file"); + } + + try writer.writeAll("error: "); + switch (reason) { + inline else => |r| { + if (is_standalone and r == .CouldNotDirectLaunch) unreachable; + + const template = comptime getFormatTemplate(r) ++ "\n\n"; + + if (comptime std.mem.indexOf(u8, template, "{s}") != null) { + try writer.print(template, .{failure_reason_argument.?}); + if (dbg) { + failure_reason_argument = null; + } + } else { + try writer.writeAll(template); + } + + const rest = switch (r) { + .InterpreterNotFoundBun => + \\Please run the following command, or double check %PATH% is right. + \\ + \\ powershell -c "irm bun.sh/install.ps1|iex" + \\ + \\ + , + else => + \\Bun failed to remap this bin to it's proper location within node_modules. + \\This is an indication of a corrupted node_modules directory. + \\ + \\Please run 'bun install --force' in the project root and try + \\it again. If this message persists, please open an issue: + \\https://github.com/oven-sh/bun/issues + \\ + \\ + }; + try writer.writeAll(rest); + }, + } + } + + pub inline fn write(reason: FailReason, writer: anytype) !void { + return reason.format("", undefined, writer); + } }; pub fn writeToHandle(handle: w.HANDLE, data: []const u8) error{}!usize { @@ -209,25 +262,10 @@ pub fn writeToHandle(handle: w.HANDLE, data: []const u8) error{}!usize { const NtWriter = std.io.Writer(w.HANDLE, error{}, writeToHandle); -inline fn printError(comptime fmt: []const u8, args: anytype) void { - std.fmt.format( - NtWriter{ - .context = @call(bun.callmod_inline, w.teb, .{}) - .ProcessEnvironmentBlock - .ProcessParameters - .hStdError, - }, - fmt, - args, - ) catch {}; -} +var failure_reason_data: [512]u8 = undefined; +var failure_reason_argument: ?[]const u8 = null; -noinline fn fail(comptime reason: FailReason) noreturn { - @setCold(true); - failWithReason(reason); -} - -noinline fn failWithReason(reason: FailReason) noreturn { +noinline fn failAndExitWithReason(reason: FailReason) noreturn { @setCold(true); const console_handle = w.teb().ProcessEnvironmentBlock.ProcessParameters.hStdError; @@ -237,26 +275,58 @@ noinline fn failWithReason(reason: FailReason) noreturn { _ = k32.SetConsoleMode(console_handle, mode); } - printError( - \\error: {s} - \\ - \\Bun failed to remap this bin to it's proper location within node_modules. - \\This is an indication of a corrupted node_modules directory. - \\ - \\Please run 'bun install --force' in the project root and try - \\it again. If this message persists, please open an issue: - \\https://github.com/oven-sh/bun/issues - \\ - \\ - , .{reason.render()}); + reason.write(NtWriter{ + .context = @call(callmod_inline, w.teb, .{}) + .ProcessEnvironmentBlock + .ProcessParameters + .hStdError, + }) catch |e| { + if (builtin.mode == .Debug) { + std.debug.panic("Failed to write to stderr: {s}", .{@errorName(e)}); + } + }; + nt.RtlExitUserProcess(255); } const nt_object_prefix = [4]u16{ '\\', '?', '?', '\\' }; -fn launcher(bun_ctx: anytype) noreturn { +// This is used for CreateProcessW's lpCommandLine +// "The maximum length of this string is 32,767 characters, including the Unicode terminating null character." +const buf2_u16_len = 32767 + 1; + +pub const LauncherMode = enum { + launch, + read_without_launch, + + /// Return type of `launcher` + fn RetType(comptime mode: LauncherMode) type { + return switch (mode) { + // See `tryStartupFromBunJS` for why this is `void` outside of standalone. + .launch => if (is_standalone) noreturn else void, + .read_without_launch => ReadWithoutLaunchResult, + }; + } + + fn FailRetType(comptime mode: LauncherMode) type { + return switch (mode) { + .launch => noreturn, + .read_without_launch => ReadWithoutLaunchResult, + }; + } + + noinline fn fail(comptime mode: LauncherMode, comptime reason: FailReason) mode.FailRetType() { + @setCold(true); + return switch (mode) { + .launch => failAndExitWithReason(reason), + .read_without_launch => ReadWithoutLaunchResult{ .err = reason }, + }; + } +}; + +fn launcher(comptime mode: LauncherMode, bun_ctx: anytype) mode.RetType() { // peb! w.teb is a couple instructions of inline asm - const teb: *w.TEB = @call(bun.callmod_inline, w.teb, .{}); + const teb: *w.TEB = @call(callmod_inline, w.teb, .{}); const peb = teb.ProcessEnvironmentBlock; const ProcessParameters = peb.ProcessParameters; const CommandLine = ProcessParameters.CommandLine; @@ -278,13 +348,8 @@ fn launcher(bun_ctx: anytype) noreturn { debug("ImagePathName: {}\n", .{fmt16(image_path_u16[0 .. image_path_b_len / 2])}); } - var buf1: [ - w.PATH_MAX_WIDE + "\"\" ".len - ]u16 = undefined; - - // This is used for CreateProcessW's lpCommandLine - // "The maximum length of this string is 32,767 characters, including the Unicode terminating null character." - var buf2: [32767 + 1]u16 = undefined; + var buf1: [w.PATH_MAX_WIDE + "\"\" ".len]u16 = undefined; + var buf2: [buf2_u16_len]u16 = undefined; const buf1_u8 = @as([*]u8, @ptrCast(&buf1[0]))[comptime buf1.len..]; const buf1_u16 = @as([*]u16, @ptrCast(&buf1[0]))[comptime buf1.len / 2..]; @@ -302,7 +367,12 @@ fn launcher(bun_ctx: anytype) noreturn { // BUF1: '\??\C:\Users\dave\project\node_modules\.bin\hello.!!!!!!!!!!!!!!!!!!!!!!!!!!' const suffix = comptime (if (is_standalone) wliteral("exe") else wliteral("bunx")); - std.debug.assert(std.mem.endsWith(u16, image_path_u16, suffix)); + if (dbg) if (!std.mem.endsWith(u16, image_path_u16, suffix)) { + std.debug.panic("assert failed: image path expected to end with {}, got {}", .{ + std.unicode.fmtUtf16le(suffix), + std.unicode.fmtUtf16le(image_path_u16), + }); + }; const image_path_to_copy_b_len = image_path_b_len - 2 * suffix.len; @memcpy( buf1_u8[2 * nt_object_prefix.len ..][0..image_path_to_copy_b_len], @@ -354,8 +424,8 @@ fn launcher(bun_ctx: anytype) noreturn { if (rc != .SUCCESS) { if (dbg) debug("error opening: {s}\n", .{@tagName(rc)}); if (rc == .OBJECT_NAME_NOT_FOUND) - fail(.ShimNotFound); - fail(.CouldNotOpenShim); + mode.fail(.ShimNotFound); + mode.fail(.CouldNotOpenShim); } } else { metadata_handle = bun_ctx.handle; @@ -429,7 +499,7 @@ fn launcher(bun_ctx: anytype) noreturn { } left -= 1; if (left == 0) { - fail(.NoDirname); + return mode.fail(.NoDirname); } ptr -= 1; std.debug.assert(@intFromPtr(ptr) >= @intFromPtr(buf1_u16)); @@ -439,12 +509,12 @@ fn launcher(bun_ctx: anytype) noreturn { while (true) { if (dbg) debug("2 - {}\n", .{std.unicode.fmtUtf16le(ptr[0..1])}); if (ptr[0] == '\\') { - // ptr is at the position marked s, so move forward one *character* + // ptr is at the position marked S, so move forward one *character* break :brk ptr + 1; } left -= 1; if (left == 0) { - fail(.NoDirname); + return mode.fail(.NoDirname); } ptr -= 1; std.debug.assert(@intFromPtr(ptr) >= @intFromPtr(buf1_u16)); @@ -480,7 +550,7 @@ fn launcher(bun_ctx: anytype) noreturn { read_max_len, else => |rc| { if (dbg) debug("error reading: {s}\n", .{@tagName(rc)}); - fail(.CouldNotReadShim); + return mode.fail(.CouldNotReadShim); }, }; @@ -501,10 +571,16 @@ fn launcher(bun_ctx: anytype) noreturn { } } - if (!flags.isValid()) - fail(.InvalidShimValidation); + if (!flags.isValid()) { + // We want to return control flow back into bun.exe's main code, so that it can fall + // back to the slow path. For more explanation, see the comment on top of `tryStartupFromBunJS`. + if (!is_standalone and mode == .launch) + return; - const spawn_command_line: [*:0]u16 = switch (flags.has_shebang) { + return mode.fail(.InvalidShimValidation); + } + + var spawn_command_line: [*:0]u16 = switch (flags.has_shebang) { false => spawn_command_line: { // no shebang, which means the command line is simply going to be the joined file exe // followed by the existing command line. @@ -574,7 +650,7 @@ fn launcher(bun_ctx: anytype) noreturn { if (dbg) debug("read_len: {}\n", .{read_len}); - fail(.InvalidShimBounds); + return mode.fail(.InvalidShimBounds); } if (!is_standalone and flags.is_node_or_bun and bun_ctx.force_use_bun) { @@ -589,13 +665,14 @@ fn launcher(bun_ctx: anytype) noreturn { // as we do not have to launch a second process. if (dbg) debug("direct_launch_with_bun_js\n", .{}); // BUF1: '\??\C:\Users\dave\project\node_modules\my-cli\src\app.js"#node #####!!!!!!!!!!' - // ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^ ^ read_ptr - const launch_slice = buf1_u16[nt_object_prefix.len..][0 .. (@intFromPtr(read_ptr) - @intFromPtr(buf1_u8)) / 2 - shebang_arg_len_u8 - "\"".len]; + // ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^ ^ read_ptr + const len = (@intFromPtr(read_ptr) - @intFromPtr(buf1_u8) - shebang_arg_len_u8) / 2 - nt_object_prefix.len - "\"\x00".len; + const launch_slice = buf1_u16[nt_object_prefix.len..][0..len :'"']; bun_ctx.direct_launch_with_bun_js( launch_slice, bun_ctx.cli_context, ); - fail(.CouldNotDirectLaunch); + return mode.fail(.CouldNotDirectLaunch); } // Copy the shebang bin path @@ -612,20 +689,20 @@ fn launcher(bun_ctx: anytype) noreturn { // BUF1: '\??\C:\Users\dave\project\node_modules\my-cli\src\app.js"#node #####!!!!!!!!!!' // ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^ ^ read_ptr // BUF2: 'node "C:\Users\dave\project\node_modules\my-cli\src\app.js"!!!!!!!!!!!!!!!!!!!!' - const length_of_filename_u8 = @intFromPtr(read_ptr) - @intFromPtr(buf1_u8) - nt_object_prefix.len - 6; + const length_of_filename_u8 = @intFromPtr(read_ptr) - @intFromPtr(buf1_u8) - nt_object_prefix.len - shebang_arg_len_u8 + "\"".len * 2; @memcpy( buf2_u8[shebang_arg_len_u8 + 2 * "\"".len ..][0..length_of_filename_u8], buf1_u8[2 * nt_object_prefix.len ..][0..length_of_filename_u8], ); + read_ptr = @ptrFromInt(@intFromPtr(buf2_u8) + length_of_filename_u8 + 2 * ("\"".len + nt_object_prefix.len)); - // Copy the user arguments in: - // BUF2: 'node "C:\Users\dave\project\node_modules\my-cli\src\app.js" --flags!!!!!!!!!!!' - // ^~~~~X^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^ - // | |filename_len where the user args go - // | the quote - // shebang_arg_len - read_ptr = @ptrFromInt(@intFromPtr(buf2_u8) + length_of_filename_u8 + 2 * "\"\"".len + 2 * nt_object_prefix.len); if (user_arguments_u8.len > 0) { + // Copy the user arguments in: + // BUF2: 'node "C:\Users\dave\project\node_modules\my-cli\src\app.js" --flags!!!!!!!!!!!' + // ^~~~~X^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^ + // | |filename_len where the user args go + // | the quote + // shebang_arg_len @memcpy(@as([*]u8, @ptrCast(read_ptr)), user_arguments_u8); read_ptr = @ptrFromInt(@intFromPtr(read_ptr) + user_arguments_u8.len); } @@ -638,9 +715,6 @@ fn launcher(bun_ctx: anytype) noreturn { }, }; - if (dbg) - debug("lpCommandLine: {}\n", .{fmt16(std.mem.span(spawn_command_line))}); - // I attempted to use lower level methods for this, but it really seems // too difficult and not worth the stability risks. // @@ -675,51 +749,114 @@ fn launcher(bun_ctx: anytype) noreturn { .hStdOutput = ProcessParameters.hStdOutput, .hStdError = ProcessParameters.hStdError, }; - const did_process_spawn = k32.CreateProcessW( - null, - spawn_command_line, - null, - null, - 1, // true - 0, - null, - null, - &startup_info, - &process, - ); - if (did_process_spawn == 0) { - const spawn_err = k32.GetLastError(); - if (dbg) { - printError("CreateProcessW failed: {s}\n", .{@tagName(spawn_err)}); - } - switch (spawn_err) { - .FILE_NOT_FOUND => if (flags.has_shebang) - fail(.InterpreterNotFound) - else - fail(.BinNotFound), - // TODO: ERROR_ELEVATION_REQUIRED must take a fallback path, this path is potentially slower: - // This likely will not be an issue anyone runs into for a while, because it implies - // the shebang depends on something that requires UAC, which .... why? - // - // https://learn.microsoft.com/en-us/windows/security/application-security/application-control/user-account-control/how-it-works#user - // https://learn.microsoft.com/en-us/windows/win32/api/shellapi/nf-shellapi-shellexecutew - .ELEVATION_REQUIRED => fail(.ElevationRequired), + inline for (.{ 0, 1 }) |attempt_number| iteration: { + if (dbg) + debug("lpCommandLine: {}\n", .{fmt16(std.mem.span(spawn_command_line))}); + const did_process_spawn = k32.CreateProcessW( + null, + spawn_command_line, + null, + null, + 1, // true + if (is_standalone) 0 else w.CREATE_UNICODE_ENVIRONMENT, + if (is_standalone) null else @constCast(bun_ctx.environment), + null, + &startup_info, + &process, + ); + if (did_process_spawn == 0) { + const spawn_err = k32.GetLastError(); + if (dbg) { + debug("CreateProcessW failed: {s}\n", .{@tagName(spawn_err)}); + debug("attempt number: {d}\n", .{attempt_number}); + } + return switch (spawn_err) { + .FILE_NOT_FOUND => if (flags.has_shebang) { + if (attempt_number == 0) { + if (flags.is_node) { + if (dbg) + debug("node is not found, changing to bun", .{}); - else => fail(.CreateProcessFailed), + if (!is_standalone) { + // TODO: this is another place that direct_launch_with_bun_js should be used + } + + // There are many packages that specifically call for node.exe, and Bun will respect that + // but if node installed, this means the binary is unlaunchable. So before we fail, + // we will try to launch it with bun.exe + // + // This is not an issue when using 'bunx' or 'bun run', because node.exe is already + // added to the path synthetically through 'createFakeTemporaryNodeExecutable'. The path + // here applies for when the binary is launched directly (user shell, double click, etc...) + assert(flags.has_shebang); + if (dbg) + assert(std.mem.startsWith(u16, std.mem.span(spawn_command_line), comptime wliteral("node "))); + + // To go from node -> bun, it is a matter of writing three chars, and incrementing a pointer. + // + // lpCommandLine: 'node "C:\Users\dave\project\node_modules\my-cli\src\app.js" --flags#!!!!!!!!!!' + // ^~~ replace these three bytes with 'bun' + @memcpy(spawn_command_line[1..][0..3], comptime wliteral("bun")); + + // lpCommandLine: 'nbun "C:\Users\dave\project\node_modules\my-cli\src\app.js" --flags#!!!!!!!!!!' + // ^ increment pointer by one char + spawn_command_line += 1; + + break :iteration; // loop back + } + + if (flags.is_node_or_bun) { + // This script calls for 'bun', but it was not found. + if (dbg) + assert(std.mem.startsWith(u16, std.mem.span(spawn_command_line), comptime wliteral("bun "))); + return mode.fail(.InterpreterNotFoundBun); + } + } + + // if attempt_number == 1, we already tried rewriting this to bun, and will now fail for real + if (attempt_number == 1) { + if (dbg) + assert(std.mem.startsWith(u16, std.mem.span(spawn_command_line), comptime wliteral("bun "))); + return mode.fail(.InterpreterNotFoundBun); + } + + // This UTF16 -> UTF-8 conversion is intentionally very lossy, and assuming that ascii text is provided. + // This trade off is made to reduce the binary size of the shim. + failure_reason_argument = brk: { + var i: u32 = 0; + while (spawn_command_line[i] != ' ' and i < 512) : (i += 1) { + failure_reason_data[i] = @as(u7, @truncate(spawn_command_line[i])); + } + break :brk failure_reason_data[0..i]; + }; + return mode.fail(.InterpreterNotFound); + } else return mode.fail(.BinNotFound), + + // TODO: ERROR_ELEVATION_REQUIRED must take a fallback path, this path is potentially slower: + // This likely will not be an issue anyone runs into for a while, because it implies + // the shebang depends on something that requires UAC, which .... why? + // + // https://learn.microsoft.com/en-us/windows/security/application-security/application-control/user-account-control/how-it-works#user + // https://learn.microsoft.com/en-us/windows/win32/api/shellapi/nf-shellapi-shellexecutew + .ELEVATION_REQUIRED => return mode.fail(.ElevationRequired), + + else => return mode.fail(.CreateProcessFailed), + }; } + + _ = k32.WaitForSingleObject(process.hProcess, w.INFINITE); + + var exit_code: w.DWORD = 255; + _ = k32.GetExitCodeProcess(process.hProcess, &exit_code); + + _ = nt.NtClose(process.hProcess); + _ = nt.NtClose(process.hThread); + + nt.RtlExitUserProcess(exit_code); comptime unreachable; } - - _ = k32.WaitForSingleObject(process.hProcess, w.INFINITE); - - var exit_code: w.DWORD = 255; - _ = k32.GetExitCodeProcess(process.hProcess, &exit_code); - - _ = nt.NtClose(process.hProcess); - _ = nt.NtClose(process.hThread); - - nt.RtlExitUserProcess(exit_code); + comptime unreachable; } pub const FromBunRunContext = struct { @@ -735,21 +872,59 @@ pub const FromBunRunContext = struct { /// Was --bun passed? force_use_bun: bool, /// A pointer to a function that can launch `Run.boot` - direct_launch_with_bun_js: *const fn (wpath: []u16, args: *CommandContext) void, + direct_launch_with_bun_js: *const fn (wpath: []u16, args: *const CommandContext) void, /// Command.Context - cli_context: *CommandContext, + cli_context: *const CommandContext, + /// Passed directly to CreateProcessW's lpEnvironment with CREATE_UNICODE_ENVIRONMENT + environment: ?[*]const u16, }; /// This is called from run_command.zig in bun.exe which allows us to skip the CreateProcessW /// call to create bun_shim_impl.exe. Instead we invoke the logic it has from an open file handle. /// -/// We pass in the context struct from above. -/// /// This saves ~5-12ms depending on the machine. -pub fn startupFromBunJS(context: FromBunRunContext) noreturn { +/// +/// If the launch is successful, this function does not return. If a validation error occurs, +/// this returns void, to which the caller should still try invoking the exe directly. This +/// is to handle version mismatches where bun.exe's decoder is too new than the .bunx file. +pub fn tryStartupFromBunJS(context: FromBunRunContext) void { std.debug.assert(!std.mem.startsWith(u16, context.base_path, &nt_object_prefix)); comptime std.debug.assert(!is_standalone); - launcher(context); + launcher(.launch, context); +} + +pub const FromBunShellContext = struct { + /// Path like 'C:\Users\dave\project\node_modules\.bin\foo.bunx' + base_path: []u16, + /// Command line arguments which does NOT include the bin name: + /// like '--port 3000 --config ./config.json' + arguments: []u16, + /// Handle to the successfully opened metadata file + handle: w.HANDLE, + /// Was --bun passed? + force_use_bun: bool, + /// A pointer to memory needed to store the command line + buf: *Buf, + + pub const Buf = [buf2_u16_len]u16; +}; + +pub const ReadWithoutLaunchResult = union { + err: FailReason, // enum which has a predefined custom formatter + command_line: []const u16, +}; + +/// Given the path and handle to a .bunx file, do everything needed to execute it, +/// *except* for spawning it. This is used by the Bun shell to skip spawning the +/// bun_shim_impl.exe executable. The returned command line is fed into the shell's +/// method for launching a process. +/// +/// The cost of spawning is about 5-12ms, and the unicode conversions are way +/// faster than that, so this is a huge win. +pub fn readWithoutLaunch(context: FromBunShellContext) ReadWithoutLaunchResult { + std.debug.assert(!std.mem.startsWith(u16, context.base_path, &nt_object_prefix)); + comptime std.debug.assert(!is_standalone); + return launcher(.read_without_launch, context); } /// Main function for `bun_shim_impl.exe` @@ -758,5 +933,5 @@ pub inline fn main() noreturn { comptime std.debug.assert(builtin.single_threaded); comptime std.debug.assert(!builtin.link_libc); comptime std.debug.assert(!builtin.link_libcpp); - launcher({}); + launcher(.launch, {}); } diff --git a/src/options.zig b/src/options.zig index f21f0124ef..309aad9141 100644 --- a/src/options.zig +++ b/src/options.zig @@ -2057,7 +2057,7 @@ pub const OutputFile = struct { .noop => JSC.JSValue.undefined, .copy => |copy| brk: { const file_blob = JSC.WebCore.Blob.Store.initFile( - if (copy.fd.int() != 0) + if (copy.fd != .zero) JSC.Node.PathOrFileDescriptor{ .fd = copy.fd, } diff --git a/src/output.zig b/src/output.zig index b30744414e..ea239ca223 100644 --- a/src/output.zig +++ b/src/output.zig @@ -404,9 +404,9 @@ pub noinline fn println(comptime fmt: string, args: anytype) void { /// Print to stdout, but only in debug builds. /// Text automatically buffers -pub inline fn debug(comptime fmt: string, args: anytype) void { +pub fn debug(comptime fmt: string, args: anytype) void { if (comptime Environment.isRelease) return; - prettyErrorln("\nDEBUG: " ++ fmt, args); + prettyErrorln("DEBUG: " ++ fmt, args); flush(); } diff --git a/src/resolver/resolver.zig b/src/resolver/resolver.zig index c721049bf4..bb5a242a3a 100644 --- a/src/resolver/resolver.zig +++ b/src/resolver/resolver.zig @@ -2142,7 +2142,6 @@ pub const Resolver = struct { dir_entries_ptr.* = new_entry; if (r.store_fd) { - Fs.FileSystem.setMaxFd(open_dir.fd); dir_entries_ptr.fd = bun.toFD(open_dir.fd); } diff --git a/src/shell/interpreter.zig b/src/shell/interpreter.zig index 46a9a2596e..f2a1bc8350 100644 --- a/src/shell/interpreter.zig +++ b/src/shell/interpreter.zig @@ -972,7 +972,7 @@ pub fn NewInterpreter(comptime EventLoopKind: JSC.EventLoopKind) type { break :env_loader global.env.?; }; - var iter = env_loader.map.iter(); + var iter = env_loader.map.iterator(); while (iter.next()) |entry| { const value = EnvStr.initSlice(entry.value_ptr.value); const key = EnvStr.initSlice(entry.key_ptr.*); diff --git a/src/string_immutable.zig b/src/string_immutable.zig index ddec5616c0..9f6237bc33 100644 --- a/src/string_immutable.zig +++ b/src/string_immutable.zig @@ -5388,7 +5388,7 @@ pub fn convertUTF8toUTF16InBuffer( // // the reason i didn't implement the fallback is purely because our // code in this file is too chaotic. it is left as a TODO - if (input.len == 0) return &[_]u16{}; + if (input.len == 0) return buf[0..0]; const result = bun.simdutf.convert.utf8.to.utf16.le(input, buf); return buf[0..result]; } diff --git a/src/sys.zig b/src/sys.zig index 3e268c9b10..ddfb1840c9 100644 --- a/src/sys.zig +++ b/src/sys.zig @@ -274,9 +274,7 @@ pub const Error = struct { } if (this.fd != bun.invalid_fd) { - if (this.fd.int() <= std.math.maxInt(i32)) { - err.fd = this.fd; - } + err.fd = this.fd; } return err; diff --git a/src/watcher.zig b/src/watcher.zig index 09423b9a06..cb320ff8ac 100644 --- a/src/watcher.zig +++ b/src/watcher.zig @@ -949,7 +949,7 @@ pub fn NewWatcher(comptime ContextType: type) type { } const fd = brk: { - if (stored_fd.int() > 0) break :brk stored_fd; + if (stored_fd != .zero) break :brk stored_fd; const dir = try std.fs.cwd().openDir(file_path, .{}); break :brk bun.toFD(dir.fd); }; @@ -1048,7 +1048,7 @@ pub fn NewWatcher(comptime ContextType: type) type { if (autowatch_parent_dir) { var watchlist_slice = this.watchlist.slice(); - if (dir_fd.int() > 0) { + if (dir_fd != .zero) { const fds = watchlist_slice.items(.fd); if (std.mem.indexOfScalar(bun.FileDescriptor, fds, dir_fd)) |i| { parent_watch_item = @as(WatchItemIndex, @truncate(i)); diff --git a/src/windows.zig b/src/windows.zig index 8c97645c87..c10710c2d2 100644 --- a/src/windows.zig +++ b/src/windows.zig @@ -3179,3 +3179,133 @@ comptime { @export(Bun__UVSignalHandle__close, .{ .name = "Bun__UVSignalHandle__close" }); } } + +extern fn GetUserNameW( + lpBuffer: bun.windows.LPWSTR, + pcbBuffer: bun.windows.LPDWORD, +) bun.windows.BOOL; + +/// Is not the actual UID of the user, but just a hash of username. +pub fn userUniqueId() u32 { + // https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-tsch/165836c1-89d7-4abb-840d-80cf2510aa3e + // UNLEN + 1 + var buf: [257]u16 = undefined; + var size: u32 = buf.len; + if (GetUserNameW(@ptrCast(&buf), &size) == 0) { + if (Environment.isDebug) std.debug.panic("GetUserNameW failed: {}", .{bun.windows.GetLastError()}); + return 0; + } + const name = buf[0..size]; + bun.Output.scoped(.windowsUserUniqueId, false)("username: {}", .{bun.fmt.utf16(name)}); + return bun.hash32(std.mem.sliceAsBytes(name)); +} + +pub fn winSockErrorToZigError(err: std.os.windows.ws2_32.WinsockError) !void { + return switch (err) { + // TODO: use `inline else` if https://github.com/ziglang/zig/issues/12250 is accepted + .WSA_INVALID_HANDLE => error.WSA_INVALID_HANDLE, + .WSA_NOT_ENOUGH_MEMORY => error.WSA_NOT_ENOUGH_MEMORY, + .WSA_INVALID_PARAMETER => error.WSA_INVALID_PARAMETER, + .WSA_OPERATION_ABORTED => error.WSA_OPERATION_ABORTED, + .WSA_IO_INCOMPLETE => error.WSA_IO_INCOMPLETE, + .WSA_IO_PENDING => error.WSA_IO_PENDING, + .WSAEINTR => error.WSAEINTR, + .WSAEBADF => error.WSAEBADF, + .WSAEACCES => error.WSAEACCES, + .WSAEFAULT => error.WSAEFAULT, + .WSAEINVAL => error.WSAEINVAL, + .WSAEMFILE => error.WSAEMFILE, + .WSAEWOULDBLOCK => error.WSAEWOULDBLOCK, + .WSAEINPROGRESS => error.WSAEINPROGRESS, + .WSAEALREADY => error.WSAEALREADY, + .WSAENOTSOCK => error.WSAENOTSOCK, + .WSAEDESTADDRREQ => error.WSAEDESTADDRREQ, + .WSAEMSGSIZE => error.WSAEMSGSIZE, + .WSAEPROTOTYPE => error.WSAEPROTOTYPE, + .WSAENOPROTOOPT => error.WSAENOPROTOOPT, + .WSAEPROTONOSUPPORT => error.WSAEPROTONOSUPPORT, + .WSAESOCKTNOSUPPORT => error.WSAESOCKTNOSUPPORT, + .WSAEOPNOTSUPP => error.WSAEOPNOTSUPP, + .WSAEPFNOSUPPORT => error.WSAEPFNOSUPPORT, + .WSAEAFNOSUPPORT => error.WSAEAFNOSUPPORT, + .WSAEADDRINUSE => error.WSAEADDRINUSE, + .WSAEADDRNOTAVAIL => error.WSAEADDRNOTAVAIL, + .WSAENETDOWN => error.WSAENETDOWN, + .WSAENETUNREACH => error.WSAENETUNREACH, + .WSAENETRESET => error.WSAENETRESET, + .WSAECONNABORTED => error.WSAECONNABORTED, + .WSAECONNRESET => error.WSAECONNRESET, + .WSAENOBUFS => error.WSAENOBUFS, + .WSAEISCONN => error.WSAEISCONN, + .WSAENOTCONN => error.WSAENOTCONN, + .WSAESHUTDOWN => error.WSAESHUTDOWN, + .WSAETOOMANYREFS => error.WSAETOOMANYREFS, + .WSAETIMEDOUT => error.WSAETIMEDOUT, + .WSAECONNREFUSED => error.WSAECONNREFUSED, + .WSAELOOP => error.WSAELOOP, + .WSAENAMETOOLONG => error.WSAENAMETOOLONG, + .WSAEHOSTDOWN => error.WSAEHOSTDOWN, + .WSAEHOSTUNREACH => error.WSAEHOSTUNREACH, + .WSAENOTEMPTY => error.WSAENOTEMPTY, + .WSAEPROCLIM => error.WSAEPROCLIM, + .WSAEUSERS => error.WSAEUSERS, + .WSAEDQUOT => error.WSAEDQUOT, + .WSAESTALE => error.WSAESTALE, + .WSAEREMOTE => error.WSAEREMOTE, + .WSASYSNOTREADY => error.WSASYSNOTREADY, + .WSAVERNOTSUPPORTED => error.WSAVERNOTSUPPORTED, + .WSANOTINITIALISED => error.WSANOTINITIALISED, + .WSAEDISCON => error.WSAEDISCON, + .WSAENOMORE => error.WSAENOMORE, + .WSAECANCELLED => error.WSAECANCELLED, + .WSAEINVALIDPROCTABLE => error.WSAEINVALIDPROCTABLE, + .WSAEINVALIDPROVIDER => error.WSAEINVALIDPROVIDER, + .WSAEPROVIDERFAILEDINIT => error.WSAEPROVIDERFAILEDINIT, + .WSASYSCALLFAILURE => error.WSASYSCALLFAILURE, + .WSASERVICE_NOT_FOUND => error.WSASERVICE_NOT_FOUND, + .WSATYPE_NOT_FOUND => error.WSATYPE_NOT_FOUND, + .WSA_E_NO_MORE => error.WSA_E_NO_MORE, + .WSA_E_CANCELLED => error.WSA_E_CANCELLED, + .WSAEREFUSED => error.WSAEREFUSED, + .WSAHOST_NOT_FOUND => error.WSAHOST_NOT_FOUND, + .WSATRY_AGAIN => error.WSATRY_AGAIN, + .WSANO_RECOVERY => error.WSANO_RECOVERY, + .WSANO_DATA => error.WSANO_DATA, + .WSA_QOS_RECEIVERS => error.WSA_QOS_RECEIVERS, + .WSA_QOS_SENDERS => error.WSA_QOS_SENDERS, + .WSA_QOS_NO_SENDERS => error.WSA_QOS_NO_SENDERS, + .WSA_QOS_NO_RECEIVERS => error.WSA_QOS_NO_RECEIVERS, + .WSA_QOS_REQUEST_CONFIRMED => error.WSA_QOS_REQUEST_CONFIRMED, + .WSA_QOS_ADMISSION_FAILURE => error.WSA_QOS_ADMISSION_FAILURE, + .WSA_QOS_POLICY_FAILURE => error.WSA_QOS_POLICY_FAILURE, + .WSA_QOS_BAD_STYLE => error.WSA_QOS_BAD_STYLE, + .WSA_QOS_BAD_OBJECT => error.WSA_QOS_BAD_OBJECT, + .WSA_QOS_TRAFFIC_CTRL_ERROR => error.WSA_QOS_TRAFFIC_CTRL_ERROR, + .WSA_QOS_GENERIC_ERROR => error.WSA_QOS_GENERIC_ERROR, + .WSA_QOS_ESERVICETYPE => error.WSA_QOS_ESERVICETYPE, + .WSA_QOS_EFLOWSPEC => error.WSA_QOS_EFLOWSPEC, + .WSA_QOS_EPROVSPECBUF => error.WSA_QOS_EPROVSPECBUF, + .WSA_QOS_EFILTERSTYLE => error.WSA_QOS_EFILTERSTYLE, + .WSA_QOS_EFILTERTYPE => error.WSA_QOS_EFILTERTYPE, + .WSA_QOS_EFILTERCOUNT => error.WSA_QOS_EFILTERCOUNT, + .WSA_QOS_EOBJLENGTH => error.WSA_QOS_EOBJLENGTH, + .WSA_QOS_EFLOWCOUNT => error.WSA_QOS_EFLOWCOUNT, + .WSA_QOS_EUNKOWNPSOBJ => error.WSA_QOS_EUNKOWNPSOBJ, + .WSA_QOS_EPOLICYOBJ => error.WSA_QOS_EPOLICYOBJ, + .WSA_QOS_EFLOWDESC => error.WSA_QOS_EFLOWDESC, + .WSA_QOS_EPSFLOWSPEC => error.WSA_QOS_EPSFLOWSPEC, + .WSA_QOS_EPSFILTERSPEC => error.WSA_QOS_EPSFILTERSPEC, + .WSA_QOS_ESDMODEOBJ => error.WSA_QOS_ESDMODEOBJ, + .WSA_QOS_ESHAPERATEOBJ => error.WSA_QOS_ESHAPERATEOBJ, + .WSA_QOS_RESERVED_PETYPE => error.WSA_QOS_RESERVED_PETYPE, + _ => |t| { + if (Environment.isDebug) { + bun.Output.debugWarn("Unknown WinSockError: {d}", .{@intFromEnum(t)}); + } + }, + }; +} + +pub fn WSAGetLastError() !void { + return winSockErrorToZigError(std.os.windows.ws2_32.WSAGetLastError()); +} diff --git a/test/bundler/bun-build-api.test.ts b/test/bundler/bun-build-api.test.ts index f61e94a267..5293d15183 100644 --- a/test/bundler/bun-build-api.test.ts +++ b/test/bundler/bun-build-api.test.ts @@ -3,9 +3,6 @@ import { readFileSync } from "fs"; import { bunEnv, bunExe } from "harness"; import { join } from "path"; -// TODO(@paperdave) -const todoOnWindows = process.platform === "win32" ? test.todo : test; - describe("Bun.build", () => { test("passing undefined doesnt segfault", () => { try { @@ -79,7 +76,7 @@ describe("Bun.build", () => { Bun.gc(true); }); - todoOnWindows("rebuilding busts the directory entries cache", () => { + test("rebuilding busts the directory entries cache", () => { Bun.gc(true); const { exitCode, stderr } = Bun.spawnSync({ cmd: [bunExe(), join(import.meta.dir, "bundler-reloader-script.ts")], diff --git a/test/cli/install/bun-pm.test.ts b/test/cli/install/bun-pm.test.ts index 8703f780c8..348fa46a55 100644 --- a/test/cli/install/bun-pm.test.ts +++ b/test/cli/install/bun-pm.test.ts @@ -330,7 +330,7 @@ it("should remove all cache", async () => { expect(stderr2).toBeDefined(); expect(await new Response(stderr2).text()).toBe(""); expect(stdout2).toBeDefined(); - expect(await new Response(stdout2).text()).toBe("Cache directory deleted:\n " + cache_dir + "\n"); + expect(await new Response(stdout2).text()).toInclude("Cleared 'bun install' cache\n"); expect(await exited2).toBe(0); expect(await exists(cache_dir)).toBeFalse(); });