From d0b759f784cd0f98875f109178fbc41ed80e8d51 Mon Sep 17 00:00:00 2001 From: Jarred Sumner <709451+Jarred-Sumner@users.noreply.github.com> Date: Mon, 29 Jan 2024 08:10:14 -0800 Subject: [PATCH 01/45] Remove references to `bun-types` --- docs/cli/init.md | 2 +- docs/quickstart.md | 17 ++++++++++++----- 2 files changed, 13 insertions(+), 6 deletions(-) diff --git a/docs/cli/init.md b/docs/cli/init.md index f17c8e1b79..fefbe99830 100644 --- a/docs/cli/init.md +++ b/docs/cli/init.md @@ -35,6 +35,6 @@ It creates: If you pass `-y` or `--yes`, it will assume you want to continue without asking questions. -At the end, it runs `bun install` to install `bun-types`. +At the end, it runs `bun install` to install `@types/bun`. {% /details %} diff --git a/docs/quickstart.md b/docs/quickstart.md index 2c818f15dc..73d487698e 100644 --- a/docs/quickstart.md +++ b/docs/quickstart.md @@ -45,18 +45,25 @@ console.log(`Listening on http://localhost:${server.port} ...`); {% details summary="Seeing TypeScript errors on `Bun`?" %} If you used `bun init`, Bun will have automatically installed Bun's TypeScript declarations and configured your `tsconfig.json`. If you're trying out Bun in an existing project, you may see a type error on the `Bun` global. -To fix this, first install `bun-types` as a dev dependency. +To fix this, first install `@types/bun` as a dev dependency. ```sh -$ bun add -d bun-types +$ bun add -d @types/bun ``` -Then add the following line to your `compilerOptions` in `tsconfig.json`. +Then add the following to your `compilerOptions` in `tsconfig.json`: -```json-diff#tsconfig.json +```json#tsconfig.json { "compilerOptions": { -+ "types": ["bun-types"] + "lib": ["ESNext"], + "target": "ESNext", + "module": "ESNext", + "moduleDetection": "force", + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "verbatimModuleSyntax": true, + "noEmit": true, } } ``` From eaea6dea54a15e0952e13817d88e54131181df36 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20V=2E=20Farias?= Date: Mon, 29 Jan 2024 14:41:58 -0300 Subject: [PATCH 02/45] chore(build): adjust arch/manjaro linux auto detect to include artix (#8510) --- CMakeLists.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 208ef75dde..147cec1fcf 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -233,7 +233,7 @@ endif() if(UNIX AND NOT APPLE) execute_process(COMMAND cat /etc/os-release COMMAND head -n1 OUTPUT_VARIABLE LINUX_DISTRO) - if(${LINUX_DISTRO} MATCHES "NAME=\"(Arch|Manjaro) Linux\"\n") + if(${LINUX_DISTRO} MATCHES "NAME=\"(Arch|Manjaro|Artix) Linux\"\n") set(DEFAULT_USE_STATIC_LIBATOMIC OFF) endif() endif() From bc7e7027e4c71d653a73ec4da84edfc5dd4b9d9c Mon Sep 17 00:00:00 2001 From: dave caruso Date: Mon, 29 Jan 2024 10:50:24 -0800 Subject: [PATCH 03/45] windows: some random things (#8521) * swaggin * fix(bun_shim_impl): support exe names that are shorter than four characters * hi * a --- packages/bun-usockets/src/eventing/libuv.c | 8 +- src/bun.js/api/BunObject.zig | 2 +- src/bun.js/api/glob.zig | 12 +- src/bun.js/bindings/bindings.zig | 2 +- src/bun.js/node/dir_iterator.zig | 12 +- src/bun.js/webcore/blob.zig | 9 +- src/bun.js/webcore/encoding.zig | 8 +- src/bun.zig | 8 + src/child_process_windows.zig | 743 ++++++++++++++++++++ src/cli.zig | 14 +- src/cli/bunx_command.zig | 7 +- src/cli/run_command.zig | 34 +- src/deps/uws.zig | 7 +- src/fd.zig | 20 +- src/http.zig | 8 +- src/http/websocket_http_client.zig | 2 +- src/install/extract_tarball.zig | 1 - src/install/windows-shim/BinLinkingShim.zig | 24 +- src/install/windows-shim/build.zig | 4 - src/install/windows-shim/bun_shim_impl.exe | Bin 10240 -> 10240 bytes src/install/windows-shim/bun_shim_impl.zig | 18 +- src/js_ast.zig | 2 +- src/libarchive/libarchive.zig | 25 +- src/shell/interpreter.zig | 47 ++ src/string.zig | 2 +- src/string_immutable.zig | 67 +- 26 files changed, 1000 insertions(+), 86 deletions(-) create mode 100644 src/child_process_windows.zig diff --git a/packages/bun-usockets/src/eventing/libuv.c b/packages/bun-usockets/src/eventing/libuv.c index c1db86c431..c7625114b6 100644 --- a/packages/bun-usockets/src/eventing/libuv.c +++ b/packages/bun-usockets/src/eventing/libuv.c @@ -198,7 +198,7 @@ void us_loop_free(struct us_loop_t *loop) { void us_loop_run(struct us_loop_t *loop) { us_loop_integrate(loop); - uv_run(loop->uv_loop, UV_RUN_NOWAIT); + uv_run(loop->uv_loop, UV_RUN_ONCE); } struct us_poll_t *us_create_poll(struct us_loop_t *loop, int fallthrough, @@ -327,11 +327,11 @@ void us_internal_async_wakeup(struct us_internal_async *a) { uv_async_send(uv_async); } -int us_socket_get_error(int ssl, struct us_socket_t* s) -{ +int us_socket_get_error(int ssl, struct us_socket_t *s) { int error = 0; socklen_t len = sizeof(error); - if (getsockopt(us_poll_fd((struct us_poll_t*)s), SOL_SOCKET, SO_ERROR, (char*)&error, &len) == -1) { + if (getsockopt(us_poll_fd((struct us_poll_t *)s), SOL_SOCKET, SO_ERROR, + (char *)&error, &len) == -1) { return errno; } return error; diff --git a/src/bun.js/api/BunObject.zig b/src/bun.js/api/BunObject.zig index a1c36bd840..1781799e79 100644 --- a/src/bun.js/api/BunObject.zig +++ b/src/bun.js/api/BunObject.zig @@ -3076,7 +3076,7 @@ pub export fn Bun__escapeHTML16(globalObject: *JSC.JSGlobalObject, input_value: std.debug.assert( std.mem.eql( u16, - (strings.toUTF16Alloc(bun.default_allocator, strings.toUTF8Alloc(bun.default_allocator, escaped_html) catch unreachable, false) catch unreachable).?, + (strings.toUTF16Alloc(bun.default_allocator, strings.toUTF8Alloc(bun.default_allocator, escaped_html) catch unreachable, false, false) catch unreachable).?, escaped_html, ), ); diff --git a/src/bun.js/api/glob.zig b/src/bun.js/api/glob.zig index 7985273b4f..d2be93a941 100644 --- a/src/bun.js/api/glob.zig +++ b/src/bun.js/api/glob.zig @@ -55,23 +55,15 @@ const ScanOpts = struct { } // Conver to utf-16 - const utf16 = (bun.strings.toUTF16Alloc( + const utf16 = bun.strings.toUTF16AllocForReal( allocator, cwd_zig_str.slice(), // Let windows APIs handle errors with invalid surrogate pairs, etc. false, + false, ) catch { globalThis.throwOutOfMemory(); return null; - }) orelse brk: { - // All ascii - const output = allocator.alloc(u16, cwd_zig_str.len) catch { - globalThis.throwOutOfMemory(); - return null; - }; - - bun.strings.copyU8IntoU16(output, cwd_zig_str.slice()); - break :brk output; }; const ptr: [*]u8 = @ptrCast(utf16.ptr); diff --git a/src/bun.js/bindings/bindings.zig b/src/bun.js/bindings/bindings.zig index c7fb8c0d07..369ab361de 100644 --- a/src/bun.js/bindings/bindings.zig +++ b/src/bun.js/bindings/bindings.zig @@ -121,7 +121,7 @@ pub const ZigString = extern struct { } pub fn dupeForJS(utf8: []const u8, allocator: std.mem.Allocator) !ZigString { - if (try strings.toUTF16Alloc(allocator, utf8, false)) |utf16| { + if (try strings.toUTF16Alloc(allocator, utf8, false, false)) |utf16| { var out = ZigString.init16(utf16); out.mark(); out.markUTF16(); diff --git a/src/bun.js/node/dir_iterator.zig b/src/bun.js/node/dir_iterator.zig index 761f426365..a5912b148a 100644 --- a/src/bun.js/node/dir_iterator.zig +++ b/src/bun.js/node/dir_iterator.zig @@ -225,16 +225,14 @@ pub fn NewIterator(comptime use_windows_ospath: bool) type { self.first = false; if (io.Information == 0) { - bun.sys.syslog("NtQueryDirectoryFile({d}) = 0", .{ - @intFromPtr(self.dir.fd), - }); + bun.sys.syslog("NtQueryDirectoryFile({}) = 0", .{bun.toFD(self.dir.fd)}); return .{ .result = null }; } self.index = 0; self.end_index = io.Information; // If the handle is not a directory, we'll get STATUS_INVALID_PARAMETER. if (rc == .INVALID_PARAMETER) { - bun.sys.syslog("NtQueryDirectoryFile({d}) = {s}", .{ @intFromPtr(self.dir.fd), @tagName(rc) }); + bun.sys.syslog("NtQueryDirectoryFile({}) = {s}", .{ bun.toFD(self.dir.fd), @tagName(rc) }); return .{ .err = .{ .errno = @intFromEnum(bun.C.SystemErrno.ENOTDIR), @@ -244,13 +242,13 @@ pub fn NewIterator(comptime use_windows_ospath: bool) type { } if (rc == .NO_MORE_FILES) { - bun.sys.syslog("NtQueryDirectoryFile({d}) = {s}", .{ @intFromPtr(self.dir.fd), @tagName(rc) }); + bun.sys.syslog("NtQueryDirectoryFile({}) = {s}", .{ bun.toFD(self.dir.fd), @tagName(rc) }); self.end_index = self.index; return .{ .result = null }; } if (rc != .SUCCESS) { - bun.sys.syslog("NtQueryDirectoryFile({d}) = {s}", .{ @intFromPtr(self.dir.fd), @tagName(rc) }); + bun.sys.syslog("NtQueryDirectoryFile({}) = {s}", .{ bun.toFD(self.dir.fd), @tagName(rc) }); if ((bun.windows.Win32Error.fromNTStatus(rc).toSystemErrno())) |errno| { return .{ @@ -269,7 +267,7 @@ pub fn NewIterator(comptime use_windows_ospath: bool) type { }; } - bun.sys.syslog("NtQueryDirectoryFile({d}) = {d}", .{ @intFromPtr(self.dir.fd), self.end_index }); + bun.sys.syslog("NtQueryDirectoryFile({}) = {d}", .{ bun.toFD(self.dir.fd), self.end_index }); } const dir_info: *w.FILE_DIRECTORY_INFORMATION = @ptrCast(@alignCast(&self.buf[self.index])); diff --git a/src/bun.js/webcore/blob.zig b/src/bun.js/webcore/blob.zig index 85a7dc2c87..394c544112 100644 --- a/src/bun.js/webcore/blob.zig +++ b/src/bun.js/webcore/blob.zig @@ -3635,7 +3635,10 @@ pub const Blob = struct { if (could_be_all_ascii == null or !could_be_all_ascii.?) { // if toUTF16Alloc returns null, it means there are no non-ASCII characters // instead of erroring, invalid characters will become a U+FFFD replacement character - if (strings.toUTF16Alloc(bun.default_allocator, buf, false) catch unreachable) |external| { + if (strings.toUTF16Alloc(bun.default_allocator, buf, false, false) catch { + global.throwOutOfMemory(); + return .zero; + }) |external| { if (lifetime != .temporary) this.setIsASCIIFlag(false); @@ -3735,7 +3738,7 @@ pub const Blob = struct { var stack_fallback = std.heap.stackFallback(4096, bun.default_allocator); const allocator = stack_fallback.get(); // if toUTF16Alloc returns null, it means there are no non-ASCII characters - if (strings.toUTF16Alloc(allocator, buf, false) catch null) |external| { + if (strings.toUTF16Alloc(allocator, buf, false, false) catch null) |external| { if (comptime lifetime != .temporary) this.setIsASCIIFlag(false); const result = ZigString.init16(external).toJSONObject(global); allocator.free(external); @@ -4393,7 +4396,7 @@ pub const InternalBlob = struct { pub fn toStringOwned(this: *@This(), globalThis: *JSC.JSGlobalObject) JSValue { const bytes_without_bom = strings.withoutUTF8BOM(this.bytes.items); - if (strings.toUTF16Alloc(globalThis.allocator(), bytes_without_bom, false) catch &[_]u16{}) |out| { + if (strings.toUTF16Alloc(globalThis.allocator(), bytes_without_bom, false, false) catch &[_]u16{}) |out| { const return_value = ZigString.toExternalU16(out.ptr, out.len, globalThis); return_value.ensureStillAlive(); this.deinit(); diff --git a/src/bun.js/webcore/encoding.zig b/src/bun.js/webcore/encoding.zig index 7c87810c06..50b1737953 100644 --- a/src/bun.js/webcore/encoding.zig +++ b/src/bun.js/webcore/encoding.zig @@ -641,7 +641,7 @@ pub const TextDecoder = struct { buffer_slice; if (this.fatal) { - if (toUTF16(default_allocator, moved_buffer_slice_8, true)) |result_| { + if (toUTF16(default_allocator, moved_buffer_slice_8, true, false)) |result_| { if (result_) |result| { return ZigString.toExternalU16(result.ptr, result.len, globalThis); } @@ -660,7 +660,7 @@ pub const TextDecoder = struct { } } } else { - if (toUTF16(default_allocator, moved_buffer_slice_8, false)) |result_| { + if (toUTF16(default_allocator, moved_buffer_slice_8, false, false)) |result_| { if (result_) |result| { return ZigString.toExternalU16(result.ptr, result.len, globalThis); } @@ -899,7 +899,7 @@ pub const Encoder = struct { return bun.String.createExternalGloballyAllocated(.latin1, input); }, .buffer, .utf8 => { - const converted = strings.toUTF16Alloc(bun.default_allocator, input, false) catch return bun.String.dead; + const converted = strings.toUTF16Alloc(bun.default_allocator, input, false, false) catch return bun.String.dead; if (converted) |utf16| { defer bun.default_allocator.free(input); return bun.String.createExternalGloballyAllocated(.utf16, utf16); @@ -978,7 +978,7 @@ pub const Encoder = struct { return str.toJS(global); }, .buffer, .utf8 => { - const converted = strings.toUTF16Alloc(allocator, input, false) catch return ZigString.init("Out of memory").toErrorInstance(global); + const converted = strings.toUTF16Alloc(allocator, input, false, false) catch return ZigString.init("Out of memory").toErrorInstance(global); if (converted) |utf16| { return ZigString.toExternalU16(utf16.ptr, utf16.len, global); } diff --git a/src/bun.zig b/src/bun.zig index 65c3495778..aebeb4f2ac 100644 --- a/src/bun.zig +++ b/src/bun.zig @@ -103,6 +103,14 @@ pub const FileDescriptor = enum(FileDescriptorInt) { try FDImpl.format(FDImpl.decode(fd), fmt_, options_, writer); } + pub fn assertValid(fd: FileDescriptor) void { + FDImpl.decode(fd).assertValid(); + } + + pub fn isValid(fd: FileDescriptor) bool { + return FDImpl.decode(fd).isValid(); + } + pub fn assertKind(fd: FileDescriptor, kind: FDImpl.Kind) void { std.debug.assert(FDImpl.decode(fd).kind == kind); } diff --git a/src/child_process_windows.zig b/src/child_process_windows.zig new file mode 100644 index 0000000000..452bd1ee37 --- /dev/null +++ b/src/child_process_windows.zig @@ -0,0 +1,743 @@ +//! TODO: Delete this entire file once https://github.com/ziglang/zig/issues/18694 is resolved. +const bun = @import("root").bun; +const std = @import("std"); + +const os = std.os; +const windows = os.windows; +const mem = std.mem; +const unicode = std.unicode; +const fs = std.fs; +const math = std.math; + +const File = fs.File; + +const ChildProcess = std.ChildProcess; +const SpawnError = ChildProcess.SpawnError; +const StdIo = ChildProcess.StdIo; +const EnvMap = std.process.EnvMap; + +pub fn toUTF16Alloc(alloc: mem.Allocator, bytes: []const u8) ![:0]u16 { + return bun.strings.toUTF16AllocForReal(alloc, bytes, false, true); +} +const utf8ToUtf16Le = bun.strings.convertUTF8toUTF16InBuffer; + +pub fn spawnWindows(self: *ChildProcess) SpawnError!void { + const saAttr = windows.SECURITY_ATTRIBUTES{ + .nLength = @sizeOf(windows.SECURITY_ATTRIBUTES), + .bInheritHandle = windows.TRUE, + .lpSecurityDescriptor = null, + }; + + const any_ignore = (self.stdin_behavior == StdIo.Ignore or self.stdout_behavior == StdIo.Ignore or self.stderr_behavior == StdIo.Ignore); + + const nul_handle = if (any_ignore) + // "\Device\Null" or "\??\NUL" + windows.OpenFile(&[_]u16{ '\\', 'D', 'e', 'v', 'i', 'c', 'e', '\\', 'N', 'u', 'l', 'l' }, .{ + .access_mask = windows.GENERIC_READ | windows.SYNCHRONIZE, + .share_access = windows.FILE_SHARE_READ, + .creation = windows.OPEN_EXISTING, + .io_mode = .blocking, + }) catch |err| switch (err) { + error.PathAlreadyExists => unreachable, // not possible for "NUL" + error.PipeBusy => unreachable, // not possible for "NUL" + error.FileNotFound => unreachable, // not possible for "NUL" + error.AccessDenied => unreachable, // not possible for "NUL" + error.NameTooLong => unreachable, // not possible for "NUL" + error.WouldBlock => unreachable, // not possible for "NUL" + error.NetworkNotFound => unreachable, // not possible for "NUL" + else => |e| return e, + } + else + undefined; + defer { + if (any_ignore) os.close(nul_handle); + } + if (any_ignore) { + try windows.SetHandleInformation(nul_handle, windows.HANDLE_FLAG_INHERIT, 0); + } + + var g_hChildStd_IN_Rd: ?windows.HANDLE = null; + var g_hChildStd_IN_Wr: ?windows.HANDLE = null; + switch (self.stdin_behavior) { + StdIo.Pipe => { + try windowsMakePipeIn(&g_hChildStd_IN_Rd, &g_hChildStd_IN_Wr, &saAttr); + }, + StdIo.Ignore => { + g_hChildStd_IN_Rd = nul_handle; + }, + StdIo.Inherit => { + g_hChildStd_IN_Rd = windows.GetStdHandle(windows.STD_INPUT_HANDLE) catch null; + }, + StdIo.Close => { + g_hChildStd_IN_Rd = null; + }, + } + errdefer if (self.stdin_behavior == StdIo.Pipe) { + windowsDestroyPipe(g_hChildStd_IN_Rd, g_hChildStd_IN_Wr); + }; + + var g_hChildStd_OUT_Rd: ?windows.HANDLE = null; + var g_hChildStd_OUT_Wr: ?windows.HANDLE = null; + switch (self.stdout_behavior) { + StdIo.Pipe => { + try windowsMakeAsyncPipe(&g_hChildStd_OUT_Rd, &g_hChildStd_OUT_Wr, &saAttr); + }, + StdIo.Ignore => { + g_hChildStd_OUT_Wr = nul_handle; + }, + StdIo.Inherit => { + g_hChildStd_OUT_Wr = windows.GetStdHandle(windows.STD_OUTPUT_HANDLE) catch null; + }, + StdIo.Close => { + g_hChildStd_OUT_Wr = null; + }, + } + errdefer if (self.stdin_behavior == StdIo.Pipe) { + windowsDestroyPipe(g_hChildStd_OUT_Rd, g_hChildStd_OUT_Wr); + }; + + var g_hChildStd_ERR_Rd: ?windows.HANDLE = null; + var g_hChildStd_ERR_Wr: ?windows.HANDLE = null; + switch (self.stderr_behavior) { + StdIo.Pipe => { + try windowsMakeAsyncPipe(&g_hChildStd_ERR_Rd, &g_hChildStd_ERR_Wr, &saAttr); + }, + StdIo.Ignore => { + g_hChildStd_ERR_Wr = nul_handle; + }, + StdIo.Inherit => { + g_hChildStd_ERR_Wr = windows.GetStdHandle(windows.STD_ERROR_HANDLE) catch null; + }, + StdIo.Close => { + g_hChildStd_ERR_Wr = null; + }, + } + errdefer if (self.stdin_behavior == StdIo.Pipe) { + windowsDestroyPipe(g_hChildStd_ERR_Rd, g_hChildStd_ERR_Wr); + }; + + const cmd_line = try windowsCreateCommandLine(self.allocator, self.argv); + defer self.allocator.free(cmd_line); + + var siStartInfo = windows.STARTUPINFOW{ + .cb = @sizeOf(windows.STARTUPINFOW), + .hStdError = g_hChildStd_ERR_Wr, + .hStdOutput = g_hChildStd_OUT_Wr, + .hStdInput = g_hChildStd_IN_Rd, + .dwFlags = windows.STARTF_USESTDHANDLES, + + .lpReserved = null, + .lpDesktop = null, + .lpTitle = null, + .dwX = 0, + .dwY = 0, + .dwXSize = 0, + .dwYSize = 0, + .dwXCountChars = 0, + .dwYCountChars = 0, + .dwFillAttribute = 0, + .wShowWindow = 0, + .cbReserved2 = 0, + .lpReserved2 = null, + }; + var piProcInfo: windows.PROCESS_INFORMATION = undefined; + + const cwd_w = if (self.cwd) |cwd| try toUTF16Alloc(self.allocator, cwd) else null; + defer if (cwd_w) |cwd| self.allocator.free(cwd); + const cwd_w_ptr = if (cwd_w) |cwd| cwd.ptr else null; + + const maybe_envp_buf = if (self.env_map) |env_map| try createWindowsEnvBlock(self.allocator, env_map) else null; + defer if (maybe_envp_buf) |envp_buf| self.allocator.free(envp_buf); + const envp_ptr = if (maybe_envp_buf) |envp_buf| envp_buf.ptr else null; + + const app_name_utf8 = self.argv[0]; + const app_name_is_absolute = fs.path.isAbsolute(app_name_utf8); + + // the cwd set in ChildProcess is in effect when choosing the executable path + // to match posix semantics + var cwd_path_w_needs_free = false; + const cwd_path_w = x: { + // If the app name is absolute, then we need to use its dirname as the cwd + if (app_name_is_absolute) { + cwd_path_w_needs_free = true; + const dir = fs.path.dirname(app_name_utf8).?; + break :x try toUTF16Alloc(self.allocator, dir); + } else if (self.cwd) |cwd| { + cwd_path_w_needs_free = true; + break :x try toUTF16Alloc(self.allocator, cwd); + } else { + break :x &[_:0]u16{}; // empty for cwd + } + }; + defer if (cwd_path_w_needs_free) self.allocator.free(cwd_path_w); + + // If the app name has more than just a filename, then we need to separate that + // into the basename and dirname and use the dirname as an addition to the cwd + // path. This is because NtQueryDirectoryFile cannot accept FileName params with + // path separators. + const app_basename_utf8 = fs.path.basename(app_name_utf8); + // If the app name is absolute, then the cwd will already have the app's dirname in it, + // so only populate app_dirname if app name is a relative path with > 0 path separators. + const maybe_app_dirname_utf8 = if (!app_name_is_absolute) fs.path.dirname(app_name_utf8) else null; + const app_dirname_w: ?[:0]u16 = x: { + if (maybe_app_dirname_utf8) |app_dirname_utf8| { + break :x try toUTF16Alloc(self.allocator, app_dirname_utf8); + } + break :x null; + }; + defer if (app_dirname_w != null) self.allocator.free(app_dirname_w.?); + + const app_name_w = try toUTF16Alloc(self.allocator, app_basename_utf8); + defer self.allocator.free(app_name_w); + + const cmd_line_w = try toUTF16Alloc(self.allocator, cmd_line); + defer self.allocator.free(cmd_line_w); + + run: { + const PATH: [:0]const u16 = std.os.getenvW(unicode.utf8ToUtf16LeStringLiteral("PATH")) orelse &[_:0]u16{}; + const PATHEXT: [:0]const u16 = std.os.getenvW(unicode.utf8ToUtf16LeStringLiteral("PATHEXT")) orelse &[_:0]u16{}; + + var app_buf = std.ArrayListUnmanaged(u16){}; + defer app_buf.deinit(self.allocator); + + try app_buf.appendSlice(self.allocator, app_name_w); + + var dir_buf = std.ArrayListUnmanaged(u16){}; + defer dir_buf.deinit(self.allocator); + + if (cwd_path_w.len > 0) { + try dir_buf.appendSlice(self.allocator, cwd_path_w); + } + if (app_dirname_w) |app_dir| { + if (dir_buf.items.len > 0) try dir_buf.append(self.allocator, fs.path.sep); + try dir_buf.appendSlice(self.allocator, app_dir); + } + if (dir_buf.items.len > 0) { + // Need to normalize the path, openDirW can't handle things like double backslashes + const normalized_len = windows.normalizePath(u16, dir_buf.items) catch return error.BadPathName; + dir_buf.shrinkRetainingCapacity(normalized_len); + } + + windowsCreateProcessPathExt(self.allocator, &dir_buf, &app_buf, PATHEXT, cmd_line_w.ptr, envp_ptr, cwd_w_ptr, &siStartInfo, &piProcInfo) catch |no_path_err| { + const original_err = switch (no_path_err) { + error.FileNotFound, error.InvalidExe, error.AccessDenied => |e| e, + error.UnrecoverableInvalidExe => return error.InvalidExe, + else => |e| return e, + }; + + // If the app name had path separators, that disallows PATH searching, + // and there's no need to search the PATH if the app name is absolute. + // We still search the path if the cwd is absolute because of the + // "cwd set in ChildProcess is in effect when choosing the executable path + // to match posix semantics" behavior--we don't want to skip searching + // the PATH just because we were trying to set the cwd of the child process. + if (app_dirname_w != null or app_name_is_absolute) { + return original_err; + } + + var it = mem.tokenizeScalar(u16, PATH, ';'); + while (it.next()) |search_path| { + dir_buf.clearRetainingCapacity(); + try dir_buf.appendSlice(self.allocator, search_path); + // Need to normalize the path, some PATH values can contain things like double + // backslashes which openDirW can't handle + const normalized_len = windows.normalizePath(u16, dir_buf.items) catch continue; + dir_buf.shrinkRetainingCapacity(normalized_len); + + if (windowsCreateProcessPathExt(self.allocator, &dir_buf, &app_buf, PATHEXT, cmd_line_w.ptr, envp_ptr, cwd_w_ptr, &siStartInfo, &piProcInfo)) { + break :run; + } else |err| switch (err) { + error.FileNotFound, error.AccessDenied, error.InvalidExe => continue, + error.UnrecoverableInvalidExe => return error.InvalidExe, + else => |e| return e, + } + } else { + return original_err; + } + }; + } + + if (g_hChildStd_IN_Wr) |h| { + self.stdin = File{ .handle = h }; + } else { + self.stdin = null; + } + if (g_hChildStd_OUT_Rd) |h| { + self.stdout = File{ .handle = h }; + } else { + self.stdout = null; + } + if (g_hChildStd_ERR_Rd) |h| { + self.stderr = File{ .handle = h }; + } else { + self.stderr = null; + } + + self.id = piProcInfo.hProcess; + self.thread_handle = piProcInfo.hThread; + self.term = null; + + if (self.stdin_behavior == StdIo.Pipe) { + os.close(g_hChildStd_IN_Rd.?); + } + if (self.stderr_behavior == StdIo.Pipe) { + os.close(g_hChildStd_ERR_Wr.?); + } + if (self.stdout_behavior == StdIo.Pipe) { + os.close(g_hChildStd_OUT_Wr.?); + } +} + +/// Caller must dealloc. +fn windowsCreateCommandLine(allocator: mem.Allocator, argv: []const []const u8) ![:0]u8 { + var buf = std.ArrayList(u8).init(allocator); + defer buf.deinit(); + + for (argv, 0..) |arg, arg_i| { + if (arg_i != 0) try buf.append(' '); + if (mem.indexOfAny(u8, arg, " \t\n\"") == null) { + try buf.appendSlice(arg); + continue; + } + try buf.append('"'); + var backslash_count: usize = 0; + for (arg) |byte| { + switch (byte) { + '\\' => backslash_count += 1, + '"' => { + try buf.appendNTimes('\\', backslash_count * 2 + 1); + try buf.append('"'); + backslash_count = 0; + }, + else => { + try buf.appendNTimes('\\', backslash_count); + try buf.append(byte); + backslash_count = 0; + }, + } + } + try buf.appendNTimes('\\', backslash_count * 2); + try buf.append('"'); + } + + return buf.toOwnedSliceSentinel(0); +} + +fn windowsDestroyPipe(rd: ?windows.HANDLE, wr: ?windows.HANDLE) void { + if (rd) |h| os.close(h); + if (wr) |h| os.close(h); +} + +fn windowsMakePipeIn(rd: *?windows.HANDLE, wr: *?windows.HANDLE, sattr: *const windows.SECURITY_ATTRIBUTES) !void { + var rd_h: windows.HANDLE = undefined; + var wr_h: windows.HANDLE = undefined; + try windows.CreatePipe(&rd_h, &wr_h, sattr); + errdefer windowsDestroyPipe(rd_h, wr_h); + try windows.SetHandleInformation(wr_h, windows.HANDLE_FLAG_INHERIT, 0); + rd.* = rd_h; + wr.* = wr_h; +} + +var pipe_name_counter = std.atomic.Value(u32).init(1); + +fn windowsMakeAsyncPipe(rd: *?windows.HANDLE, wr: *?windows.HANDLE, sattr: *const windows.SECURITY_ATTRIBUTES) !void { + var tmp_bufw: [128]u16 = undefined; + + // Anonymous pipes are built upon Named pipes. + // https://docs.microsoft.com/en-us/windows/win32/api/namedpipeapi/nf-namedpipeapi-createpipe + // Asynchronous (overlapped) read and write operations are not supported by anonymous pipes. + // https://docs.microsoft.com/en-us/windows/win32/ipc/anonymous-pipe-operations + const pipe_path = blk: { + var tmp_buf: [128]u8 = undefined; + // Forge a random path for the pipe. + const pipe_path = std.fmt.bufPrintZ( + &tmp_buf, + "\\\\.\\pipe\\zig-childprocess-{d}-{d}", + .{ windows.kernel32.GetCurrentProcessId(), pipe_name_counter.fetchAdd(1, .Monotonic) }, + ) catch unreachable; + const buf_2 = utf8ToUtf16Le(&tmp_bufw, pipe_path); + tmp_bufw[buf_2.len] = 0; + break :blk tmp_bufw[0..buf_2.len :0]; + }; + + // Create the read handle that can be used with overlapped IO ops. + const read_handle = windows.kernel32.CreateNamedPipeW( + pipe_path.ptr, + windows.PIPE_ACCESS_INBOUND | windows.FILE_FLAG_OVERLAPPED, + windows.PIPE_TYPE_BYTE, + 1, + 4096, + 4096, + 0, + sattr, + ); + if (read_handle == windows.INVALID_HANDLE_VALUE) { + switch (windows.kernel32.GetLastError()) { + else => |err| return windows.unexpectedError(err), + } + } + errdefer os.close(read_handle); + + var sattr_copy = sattr.*; + const write_handle = windows.kernel32.CreateFileW( + pipe_path.ptr, + windows.GENERIC_WRITE, + 0, + &sattr_copy, + windows.OPEN_EXISTING, + windows.FILE_ATTRIBUTE_NORMAL, + null, + ); + if (write_handle == windows.INVALID_HANDLE_VALUE) { + switch (windows.kernel32.GetLastError()) { + else => |err| return windows.unexpectedError(err), + } + } + errdefer os.close(write_handle); + + try windows.SetHandleInformation(read_handle, windows.HANDLE_FLAG_INHERIT, 0); + + rd.* = read_handle; + wr.* = write_handle; +} + +pub fn createWindowsEnvBlock(allocator: mem.Allocator, env_map: *const EnvMap) ![]u16 { + // count bytes needed + const max_chars_needed = x: { + var max_chars_needed: usize = 4; // 4 for the final 4 null bytes + var it = env_map.iterator(); + while (it.next()) |pair| { + // +1 for '=' + // +1 for null byte + max_chars_needed += pair.key_ptr.len + pair.value_ptr.len + 2; + } + break :x max_chars_needed; + }; + const result = try allocator.alloc(u16, max_chars_needed); + errdefer allocator.free(result); + + var it = env_map.iterator(); + var i: usize = 0; + while (it.next()) |pair| { + i += utf8ToUtf16Le(result[i..], pair.key_ptr.*).len; + result[i] = '='; + i += 1; + i += utf8ToUtf16Le(result[i..], pair.value_ptr.*).len; + 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 try allocator.realloc(result, i); +} + +/// Expects `app_buf` to contain exactly the app name, and `dir_buf` to contain exactly the dir path. +/// After return, `app_buf` will always contain exactly the app name and `dir_buf` will always contain exactly the dir path. +/// Note: `app_buf` should not contain any leading path separators. +/// Note: If the dir is the cwd, dir_buf should be empty (len = 0). +fn windowsCreateProcessPathExt( + allocator: mem.Allocator, + dir_buf: *std.ArrayListUnmanaged(u16), + app_buf: *std.ArrayListUnmanaged(u16), + pathext: [:0]const u16, + cmd_line: [*:0]u16, + envp_ptr: ?[*]u16, + cwd_ptr: ?[*:0]u16, + lpStartupInfo: *windows.STARTUPINFOW, + lpProcessInformation: *windows.PROCESS_INFORMATION, +) !void { + const app_name_len = app_buf.items.len; + const dir_path_len = dir_buf.items.len; + + if (app_name_len == 0) return error.FileNotFound; + + defer app_buf.shrinkRetainingCapacity(app_name_len); + defer dir_buf.shrinkRetainingCapacity(dir_path_len); + + // The name of the game here is to avoid CreateProcessW calls at all costs, + // and only ever try calling it when we have a real candidate for execution. + // Secondarily, we want to minimize the number of syscalls used when checking + // for each PATHEXT-appended version of the app name. + // + // An overview of the technique used: + // - Open the search directory for iteration (either cwd or a path from PATH) + // - Use NtQueryDirectoryFile with a wildcard filename of `*` to + // check if anything that could possibly match either the unappended version + // of the app name or any of the versions with a PATHEXT value appended exists. + // - If the wildcard NtQueryDirectoryFile call found nothing, we can exit early + // without needing to use PATHEXT at all. + // + // This allows us to use a sequence + // for any directory that doesn't contain any possible matches, instead of having + // to use a separate look up for each individual filename combination (unappended + + // each PATHEXT appended). For directories where the wildcard *does* match something, + // we iterate the matches and take note of any that are either the unappended version, + // or a version with a supported PATHEXT appended. We then try calling CreateProcessW + // with the found versions in the appropriate order. + + var dir = dir: { + // needs to be null-terminated + try dir_buf.append(allocator, 0); + defer dir_buf.shrinkRetainingCapacity(dir_path_len); + const dir_path_z = dir_buf.items[0 .. dir_buf.items.len - 1 :0]; + const prefixed_path = try windows.wToPrefixedFileW(null, dir_path_z); + break :dir fs.cwd().openDirW(prefixed_path.span().ptr, .{ .iterate = true }) catch + return error.FileNotFound; + }; + defer dir.close(); + + // Add wildcard and null-terminator + try app_buf.append(allocator, '*'); + try app_buf.append(allocator, 0); + const app_name_wildcard = app_buf.items[0 .. app_buf.items.len - 1 :0]; + + // This 2048 is arbitrary, we just want it to be large enough to get multiple FILE_DIRECTORY_INFORMATION entries + // returned per NtQueryDirectoryFile call. + var file_information_buf: [2048]u8 align(@alignOf(os.windows.FILE_DIRECTORY_INFORMATION)) = undefined; + const file_info_maximum_single_entry_size = @sizeOf(windows.FILE_DIRECTORY_INFORMATION) + (windows.NAME_MAX * 2); + if (file_information_buf.len < file_info_maximum_single_entry_size) { + @compileError("file_information_buf must be large enough to contain at least one maximum size FILE_DIRECTORY_INFORMATION entry"); + } + var io_status: windows.IO_STATUS_BLOCK = undefined; + + const num_supported_pathext = @typeInfo(CreateProcessSupportedExtension).Enum.fields.len; + var pathext_seen = [_]bool{false} ** num_supported_pathext; + var any_pathext_seen = false; + var unappended_exists = false; + + // Fully iterate the wildcard matches via NtQueryDirectoryFile and take note of all versions + // of the app_name we should try to spawn. + // Note: This is necessary because the order of the files returned is filesystem-dependent: + // On NTFS, `blah.exe*` will always return `blah.exe` first if it exists. + // On FAT32, it's possible for something like `blah.exe.obj` to be returned first. + while (true) { + const app_name_len_bytes = math.cast(u16, app_name_wildcard.len * 2) orelse return error.NameTooLong; + var app_name_unicode_string = windows.UNICODE_STRING{ + .Length = app_name_len_bytes, + .MaximumLength = app_name_len_bytes, + .Buffer = @constCast(app_name_wildcard.ptr), + }; + const rc = windows.ntdll.NtQueryDirectoryFile( + dir.fd, + null, + null, + null, + &io_status, + &file_information_buf, + file_information_buf.len, + .FileDirectoryInformation, + windows.FALSE, // single result + &app_name_unicode_string, + windows.FALSE, // restart iteration + ); + + // If we get nothing with the wildcard, then we can just bail out + // as we know appending PATHEXT will not yield anything. + switch (rc) { + .SUCCESS => {}, + .NO_SUCH_FILE => return error.FileNotFound, + .NO_MORE_FILES => break, + .ACCESS_DENIED => return error.AccessDenied, + else => return windows.unexpectedStatus(rc), + } + + // According to the docs, this can only happen if there is not enough room in the + // buffer to write at least one complete FILE_DIRECTORY_INFORMATION entry. + // Therefore, this condition should not be possible to hit with the buffer size we use. + std.debug.assert(io_status.Information != 0); + + var it = windows.FileInformationIterator(windows.FILE_DIRECTORY_INFORMATION){ .buf = &file_information_buf }; + while (it.next()) |info| { + // Skip directories + if (info.FileAttributes & windows.FILE_ATTRIBUTE_DIRECTORY != 0) continue; + const filename = @as([*]u16, @ptrCast(&info.FileName))[0 .. info.FileNameLength / 2]; + // Because all results start with the app_name since we're using the wildcard `app_name*`, + // if the length is equal to app_name then this is an exact match + if (filename.len == app_name_len) { + // Note: We can't break early here because it's possible that the unappended version + // fails to spawn, in which case we still want to try the PATHEXT appended versions. + unappended_exists = true; + } else if (windowsCreateProcessSupportsExtension(filename[app_name_len..])) |pathext_ext| { + pathext_seen[@intFromEnum(pathext_ext)] = true; + any_pathext_seen = true; + } + } + } + + const unappended_err = unappended: { + if (unappended_exists) { + if (dir_path_len != 0) switch (dir_buf.items[dir_buf.items.len - 1]) { + '/', '\\' => {}, + else => try dir_buf.append(allocator, fs.path.sep), + }; + try dir_buf.appendSlice(allocator, app_buf.items[0..app_name_len]); + try dir_buf.append(allocator, 0); + const full_app_name = dir_buf.items[0 .. dir_buf.items.len - 1 :0]; + + if (windowsCreateProcess(full_app_name.ptr, cmd_line, envp_ptr, cwd_ptr, lpStartupInfo, lpProcessInformation)) |_| { + return; + } else |err| switch (err) { + error.FileNotFound, + error.AccessDenied, + => break :unappended err, + error.InvalidExe => { + // On InvalidExe, if the extension of the app name is .exe then + // it's treated as an unrecoverable error. Otherwise, it'll be + // skipped as normal. + const app_name = app_buf.items[0..app_name_len]; + const ext_start = std.mem.lastIndexOfScalar(u16, app_name, '.') orelse break :unappended err; + const ext = app_name[ext_start..]; + if (windows.eqlIgnoreCaseWTF16(ext, unicode.utf8ToUtf16LeStringLiteral(".EXE"))) { + return error.UnrecoverableInvalidExe; + } + break :unappended err; + }, + else => return err, + } + } + break :unappended error.FileNotFound; + }; + + if (!any_pathext_seen) return unappended_err; + + // Now try any PATHEXT appended versions that we've seen + var ext_it = mem.tokenizeScalar(u16, pathext, ';'); + while (ext_it.next()) |ext| { + const ext_enum = windowsCreateProcessSupportsExtension(ext) orelse continue; + if (!pathext_seen[@intFromEnum(ext_enum)]) continue; + + dir_buf.shrinkRetainingCapacity(dir_path_len); + if (dir_path_len != 0) switch (dir_buf.items[dir_buf.items.len - 1]) { + '/', '\\' => {}, + else => try dir_buf.append(allocator, fs.path.sep), + }; + try dir_buf.appendSlice(allocator, app_buf.items[0..app_name_len]); + try dir_buf.appendSlice(allocator, ext); + try dir_buf.append(allocator, 0); + const full_app_name = dir_buf.items[0 .. dir_buf.items.len - 1 :0]; + + if (windowsCreateProcess(full_app_name.ptr, cmd_line, envp_ptr, cwd_ptr, lpStartupInfo, lpProcessInformation)) |_| { + return; + } else |err| switch (err) { + error.FileNotFound => continue, + error.AccessDenied => continue, + error.InvalidExe => { + // On InvalidExe, if the extension of the app name is .exe then + // it's treated as an unrecoverable error. Otherwise, it'll be + // skipped as normal. + if (windows.eqlIgnoreCaseWTF16(ext, unicode.utf8ToUtf16LeStringLiteral(".EXE"))) { + return error.UnrecoverableInvalidExe; + } + continue; + }, + else => return err, + } + } + + return unappended_err; +} + +// Should be kept in sync with `windowsCreateProcessSupportsExtension` +const CreateProcessSupportedExtension = enum { + bat, + cmd, + com, + exe, +}; + +/// Case-insensitive UTF-16 lookup +fn windowsCreateProcessSupportsExtension(ext: []const u16) ?CreateProcessSupportedExtension { + if (ext.len != 4) return null; + const State = enum { + start, + dot, + b, + ba, + c, + cm, + co, + e, + ex, + }; + var state: State = .start; + for (ext) |c| switch (state) { + .start => switch (c) { + '.' => state = .dot, + else => return null, + }, + .dot => switch (c) { + 'b', 'B' => state = .b, + 'c', 'C' => state = .c, + 'e', 'E' => state = .e, + else => return null, + }, + .b => switch (c) { + 'a', 'A' => state = .ba, + else => return null, + }, + .c => switch (c) { + 'm', 'M' => state = .cm, + 'o', 'O' => state = .co, + else => return null, + }, + .e => switch (c) { + 'x', 'X' => state = .ex, + else => return null, + }, + .ba => switch (c) { + 't', 'T' => return .bat, + else => return null, + }, + .cm => switch (c) { + 'd', 'D' => return .cmd, + else => return null, + }, + .co => switch (c) { + 'm', 'M' => return .com, + else => return null, + }, + .ex => switch (c) { + 'e', 'E' => return .exe, + else => return null, + }, + }; + return null; +} + +fn windowsCreateProcess(app_name: [*:0]u16, cmd_line: [*:0]u16, envp_ptr: ?[*]u16, cwd_ptr: ?[*:0]u16, lpStartupInfo: *windows.STARTUPINFOW, lpProcessInformation: *windows.PROCESS_INFORMATION) !void { + // TODO the docs for environment pointer say: + // > A pointer to the environment block for the new process. If this parameter + // > is NULL, the new process uses the environment of the calling process. + // > ... + // > An environment block can contain either Unicode or ANSI characters. If + // > the environment block pointed to by lpEnvironment contains Unicode + // > characters, be sure that dwCreationFlags includes CREATE_UNICODE_ENVIRONMENT. + // > If this parameter is NULL and the environment block of the parent process + // > contains Unicode characters, you must also ensure that dwCreationFlags + // > includes CREATE_UNICODE_ENVIRONMENT. + // This seems to imply that we have to somehow know whether our process parent passed + // CREATE_UNICODE_ENVIRONMENT if we want to pass NULL for the environment parameter. + // Since we do not know this information that would imply that we must not pass NULL + // for the parameter. + // However this would imply that programs compiled with -DUNICODE could not pass + // environment variables to programs that were not, which seems unlikely. + // More investigation is needed. + return windows.CreateProcessW( + app_name, + cmd_line, + null, + null, + windows.TRUE, + windows.CREATE_UNICODE_ENVIRONMENT, + @as(?*anyopaque, @ptrCast(envp_ptr)), + cwd_ptr, + lpStartupInfo, + lpProcessInformation, + ); +} diff --git a/src/cli.zig b/src/cli.zig index 3061942c3f..a6bd336d3f 100644 --- a/src/cli.zig +++ b/src/cli.zig @@ -196,7 +196,10 @@ pub const Arguments = struct { const run_only_params = [_]ParamType{ clap.parseParam("--silent Don't print the script command") catch unreachable, clap.parseParam("-b, --bun Force a script or package to use Bun's runtime instead of Node.js (via symlinking node)") catch unreachable, - }; + } ++ if (Environment.isWindows) [_]ParamType{ + // clap.parseParam("--native-shell Use cmd.exe to interpret package.json scripts") catch unreachable, + clap.parseParam("--no-native-shell Use Bun shell (TODO: flip this switch)") catch unreachable, + } else .{}; pub const run_params = run_only_params ++ runtime_params_ ++ transpiler_params_ ++ base_params_; const bunx_commands = [_]ParamType{ @@ -835,6 +838,13 @@ pub const Arguments = struct { if (output_file != null) ctx.debug.output_file = output_file.?; + if (cmd == .RunCommand) { + ctx.debug.use_native_shell = if (Environment.isWindows) + !args.flag("--no-native-shell") + else + true; + } + return opts; } }; @@ -1028,6 +1038,8 @@ pub const Command = struct { offline_mode_setting: ?Bunfig.OfflineMode = null, run_in_bun: bool = false, loaded_bunfig: bool = false, + /// Disables using bun.shell.Interpreter for `bun run`, instead spawning cmd.exe + use_native_shell: bool = false, // technical debt macros: MacroOptions = MacroOptions.unspecified, diff --git a/src/cli/bunx_command.zig b/src/cli/bunx_command.zig index 4bf74f71df..711277a8b2 100644 --- a/src/cli/bunx_command.zig +++ b/src/cli/bunx_command.zig @@ -142,6 +142,7 @@ pub const BunxCommand = struct { /// Check the enclosing package.json for a matching "bin" /// If not found, check bunx cache dir fn getBinName(bundler: *bun.Bundler, toplevel_fd: bun.FileDescriptor, tempdir_name: []const u8, package_name: []const u8) error{ NoBinFound, NeedToInstall }![]const u8 { + toplevel_fd.assertValid(); return getBinNameFromProjectDirectory(bundler, toplevel_fd, package_name) catch |err| { if (err == error.NoBinFound) { return error.NoBinFound; @@ -323,13 +324,13 @@ pub const BunxCommand = struct { if (PATH.len > 0) { PATH = try std.fmt.allocPrint( ctx.allocator, - "{s}/{s}--bunx/node_modules/.bin:{s}", + bun.pathLiteral("{s}/{s}--bunx/node_modules/.bin:{s}"), .{ temp_dir, package_fmt, PATH }, ); } else { PATH = try std.fmt.allocPrint( ctx.allocator, - "{s}/{s}--bunx/node_modules/.bin", + bun.pathLiteral("{s}/{s}--bunx/node_modules/.bin"), .{ temp_dir, package_fmt }, ); } @@ -337,7 +338,7 @@ pub const BunxCommand = struct { const bunx_cache_dir = PATH[0 .. temp_dir.len + "/--bunx".len + package_fmt.len]; var absolute_in_cache_dir_buf: [bun.MAX_PATH_BYTES]u8 = undefined; - var absolute_in_cache_dir = std.fmt.bufPrint(&absolute_in_cache_dir_buf, "{s}/node_modules/.bin/{s}", .{ bunx_cache_dir, initial_bin_name }) catch unreachable; + 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; const passthrough = passthrough_list.items; diff --git a/src/cli/run_command.zig b/src/cli/run_command.zig index d4d58d0c20..eabf004d4d 100644 --- a/src/cli/run_command.zig +++ b/src/cli/run_command.zig @@ -271,6 +271,7 @@ pub const RunCommand = struct { env: *DotEnv.Loader, passthrough: []const string, silent: bool, + use_native_shell: bool, ) !bool { const shell_bin = findShell(env.map.get("PATH") orelse "", cwd) orelse return error.MissingShell; @@ -303,6 +304,28 @@ pub const RunCommand = struct { combined_script = combined_script_buf; } + if (Environment.isWindows and !use_native_shell) { + if (!silent) { + if (Environment.isDebug) { + Output.prettyError("[bun shell] ", .{}); + } + Output.prettyErrorln("$ {s}", .{combined_script}); + Output.flush(); + } + + const mini = bun.JSC.MiniEventLoop.initGlobal(env); + bun.shell.InterpreterMini.initAndRunFromSource(mini, name, combined_script) catch |err| { + if (!silent) { + Output.prettyErrorln("error: Failed to run script {s} due to error {s}", .{ name, @errorName(err) }); + } + + Output.flush(); + Global.exit(1); + }; + + return true; + } + var argv = [_]string{ shell_bin, if (Environment.isWindows) "/c" else "-c", @@ -324,7 +347,13 @@ pub const RunCommand = struct { child_process.stdin_behavior = .Inherit; child_process.stdout_behavior = .Inherit; - const result = child_process.spawnAndWait() catch |err| { + if (Environment.isWindows) { + try @import("../child_process_windows.zig").spawnWindows(&child_process); + } else { + try child_process.spawn(); + } + + const result = child_process.wait() catch |err| { if (!silent) { Output.prettyErrorln("error: Failed to run script {s} due to error {s}", .{ name, @errorName(err) }); } @@ -1249,6 +1278,7 @@ pub const RunCommand = struct { this_bundler.env, &.{}, ctx.debug.silent, + ctx.debug.use_native_shell, )) { return false; } @@ -1262,6 +1292,7 @@ pub const RunCommand = struct { this_bundler.env, passthrough, ctx.debug.silent, + ctx.debug.use_native_shell, )) return false; temp_script_buffer[0.."post".len].* = "post".*; @@ -1275,6 +1306,7 @@ pub const RunCommand = struct { this_bundler.env, &.{}, ctx.debug.silent, + ctx.debug.use_native_shell, )) { return false; } diff --git a/src/deps/uws.zig b/src/deps/uws.zig index 281ad7b948..c9b31a8ba5 100644 --- a/src/deps/uws.zig +++ b/src/deps/uws.zig @@ -2540,11 +2540,10 @@ pub const UVLoop = extern struct { pub fn run(this: *UVLoop) void { us_loop_run(this); } - pub const tick = run; - pub fn wait(this: *UVLoop) void { - us_loop_run(this); - } + // TODO: remove these two aliases + pub const tick = run; + pub const wait = run; pub fn inc(this: *UVLoop) void { this.uv_loop.inc(); diff --git a/src/fd.zig b/src/fd.zig index aa18bcf160..162a08328b 100644 --- a/src/fd.zig +++ b/src/fd.zig @@ -20,8 +20,12 @@ fn handleToNumber(handle: FDImpl.System) FDImpl.SystemAsInt { return handle; } } + fn numberToHandle(handle: FDImpl.SystemAsInt) FDImpl.System { if (env.os == .windows) { + if (!@inComptime()) { + std.debug.assert(handle != FDImpl.invalid_value); + } return @ptrFromInt(handle); } else { return handle; @@ -122,7 +126,17 @@ pub const FDImpl = packed struct { } pub fn isValid(this: FDImpl) bool { - return this.value.as_system != invalid_value; + return switch (env.os) { + // the 'zero' value on posix is debatable. it can be standard in. + // TODO(@paperdave): steamroll away every use of bun.FileDescriptor.zero + else => this.value.as_system != invalid_value, + .windows => switch (this.kind) { + // zero is not allowed in addition to the invalid value (zero would be a null ptr) + .system => this.value.as_system != invalid_value and this.value.as_system != 0, + // the libuv tag is always fine + .uv => true, + }, + }; } /// When calling this function, you may not be able to close the returned fd. @@ -323,4 +337,8 @@ pub const FDImpl = packed struct { }, } } + + pub fn assertValid(this: FDImpl) void { + std.debug.assert(this.isValid()); + } }; diff --git a/src/http.zig b/src/http.zig index 42529d43b6..82be549922 100644 --- a/src/http.zig +++ b/src/http.zig @@ -799,8 +799,13 @@ pub const HTTPThread = struct { } fn processEvents(this: *@This()) noreturn { - if (comptime Environment.isPosix) + if (comptime Environment.isPosix) { this.loop.num_polls = @max(2, this.loop.num_polls); + } else if (comptime Environment.isWindows) { + this.loop.inc(); + } else { + @compileError("TODO:"); + } while (true) { this.drainEvents(); @@ -810,7 +815,6 @@ pub const HTTPThread = struct { start_time = std.time.nanoTimestamp(); } Output.flush(); - // TODO(@paperdave): this does not wait any time on windows this.loop.run(); if (comptime Environment.isDebug) { const end = std.time.nanoTimestamp(); diff --git a/src/http/websocket_http_client.zig b/src/http/websocket_http_client.zig index 06763e3388..03913d50d3 100644 --- a/src/http/websocket_http_client.zig +++ b/src/http/websocket_http_client.zig @@ -1062,7 +1062,7 @@ pub fn NewWebSocketClient(comptime ssl: bool) type { // this function encodes to UTF-16 if > 127 // so we don't need to worry about latin1 non-ascii code points // we avoid trim since we wanna keep the utf8 validation intact - const utf16_bytes_ = strings.toUTF16AllocNoTrim(bun.default_allocator, data_, true) catch { + const utf16_bytes_ = strings.toUTF16AllocNoTrim(bun.default_allocator, data_, true, false) catch { this.terminate(ErrorCode.invalid_utf8); return; }; diff --git a/src/install/extract_tarball.zig b/src/install/extract_tarball.zig index 7bb2212d0d..8f1931ebab 100644 --- a/src/install/extract_tarball.zig +++ b/src/install/extract_tarball.zig @@ -351,7 +351,6 @@ fn extract(this: *const ExtractTarball, tgz_bytes: []const u8) !Install.ExtractD ) catch unreachable; return error.InstallFailed; }; - defer final_dir.close(); // and get the fd path const final_path = bun.getFdPath( diff --git a/src/install/windows-shim/BinLinkingShim.zig b/src/install/windows-shim/BinLinkingShim.zig index a1a8465e98..8cb4c552e9 100644 --- a/src/install/windows-shim/BinLinkingShim.zig +++ b/src/install/windows-shim/BinLinkingShim.zig @@ -26,23 +26,33 @@ bin_path: []const u16, /// Information found within the target file's shebang shebang: ?Shebang, +/// Random numbers are chosen for validation purposes +/// 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 = .v2; + + v1 = 5474, + v2 = 5475, + _, +}; + pub const Flags = packed struct(u16) { - // the shim doesnt use this right now + // 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 - // if this format is ever modified, we will set this flag to false to indicate version 2+ - is_version_1: bool = true, + is_valid: bool = true, // indicates if a shebang is present has_shebang: bool, - // this is for validation that the shim is not corrupt and to detect offset memory reads - must_be_5474: u13 = 5474, + + version_tag: VersionFlag = VersionFlag.current, pub fn isValid(flags: Flags) bool { const mask: u16 = @bitCast(Flags{ .is_node_or_bun = false, - .is_version_1 = true, + .is_valid = true, .has_shebang = false, - .must_be_5474 = std.math.maxInt(u13), + .version_tag = @enumFromInt(std.math.maxInt(u13)), }); const compare_to: u16 = @bitCast(Flags{ diff --git a/src/install/windows-shim/build.zig b/src/install/windows-shim/build.zig index 48e93da2e9..0f369f33bc 100644 --- a/src/install/windows-shim/build.zig +++ b/src/install/windows-shim/build.zig @@ -34,11 +34,7 @@ pub fn build(b: *std.Build) void { .optimize = .Debug, .use_llvm = true, .use_lld = true, - .unwind_tables = false, - .omit_frame_pointer = true, - .strip = true, .linkage = .static, - .sanitize_thread = false, .single_threaded = true, .link_libc = false, }); diff --git a/src/install/windows-shim/bun_shim_impl.exe b/src/install/windows-shim/bun_shim_impl.exe index ded01c18a8fcc5db516313d7c6ff45d8fdf8cbe6..8ccadc89417958545460e9f70dc486d522335203 100755 GIT binary patch delta 871 zcmX|8Ur19?7(Zv;sg>>~M3`Y4cCD5rj_i-9wAo#q8`tzfNl0yw4l#oswvs>`JyWXk(j7VD~N;$f=cnhD;kv$P0`)Hvq=}ezvuUz`}=+;P#>rt3S5vNCvblBYdc;8(q(%2zI{5?>#dx-i_yU3lw zF0G3@hMP5wd(a<->FRQXVtnWeMkpNP!=E9#KCZh)vDc*-CSG|54Yyc@{Yvb!Bsj|<4E$xGgTEf*^F-kSp`vg?rar+&yM!{wg4+>uJLdJpOkkY{ ztbO|#u3EhxrLT@)CB&)AdI0+kGS3FCb&Kw3-8@p_N0D{qIfGc%m z0yoUE=io%TsogOjwfGu|5hE3Iyuu|7LwjT{>7VoRRuQ2wy!OViur{GzVl%DGN#KZj z;Id<$w<51nwbw>e$>jS(aEz$Fylu%I2h})sU1)H}FeLj2pwEFYwRCH>k98tf5)z$} zrq*q~9vZu7xEDgS~7#lsX1I?BAIrb!uC= zNv2DLff9NEK(%-!B@`ddhgH%#k%&+5Nc5L}lL6GK?$zy`D9IkgI6wMnPX%uImJKW2 z;(`Rv8H+?UL)$l8ELYeF<3vZZSyM*4v89 z>!I<2p!`rDqM*L$3PB|1A5zON`6&%Tsbr!^3E`j0*LhRAaL&EIbAI>S^E)@x5NhZP zr8Jfg4oF6fOJWpLLwR!=Lc}uBL1d=bir_3)dj+}E>|lplZ}4(sColU0E_SHHOO#h? z6>|dH)Gnq3cd07#!0m_I=PD42`%XsVeyii-s$&3)(r-hz#bul2l?b<0g^*YyjkKQ* zBh=y#@^o_fF}#yFlLF2Kj?SrhX+>FVWF5ovTAWl)bw5|@)5?M~B3s}@Xb`WA*=ASX z^3ucxTTqR7X%(}#dx0$|fgMpAaaO~*MWCDhZvwr*d7V_?nrf9uk6W)uSBPbtgvS|# zL7l*LvKQm5$;sIXb#kQw^>Jp}!dY#zE?eAXn@(Iy1x+T<6!c|B<80KX7m@q9C(w1h z^18>F#84-}0kei3VouRfX>mKeb?_xZZ1fNCc>LuB!$$2u6(=K{5JspF`hK?)>fxG@ zKU<(b30)UtC+AJ9F;HLWPm+WbALkVvlTetK)k~=lgM`lS_2n}nr1$e#pWa=loT$-V ztL_Lmq8xJDro}!{I=kOY6yD(bMR1%bKGPq40Lpm=Cd)>8|7sesd?3;|18R9XzZ`-H zFS|E+I>C>wntcHpuT`ApEK>Q&AMDawBVnv}zv;0o5jrQ(bq`%gJcGXzYe1cz1$CA+ zU7$y4KWY%@BB9$J`Xdnnk_Rs(WwX)yJDRpDoyZ`R2SS@pP%3UN`yrQj3fhQ#j|;&* ztig(Pr3WxBjeXx&iCfpwA$yq#6Fj9Y5XERZjt~el*6I6ju6|cBwk1LSF1WtONKXcH zc7r2HP`C@O?lJDfZXGy=bKW08Xy*UYipTb_`Y6M$j%8R?LuS5Om-$9*$jr^l-O#im Vure?^m= @intFromPtr(buf1_u16)); + std.debug.assert(ptr[1] == '.'); + inline for (0..1) |_| { while (true) { if (ptr[0] == '\\') { diff --git a/src/js_ast.zig b/src/js_ast.zig index 05b7aeb9ae..8672c28dc3 100644 --- a/src/js_ast.zig +++ b/src/js_ast.zig @@ -2266,7 +2266,7 @@ pub const E = struct { if (s.isUTF8()) { if (comptime !Environment.isNative) { - const allocated = (strings.toUTF16Alloc(bun.default_allocator, s.data, false) catch return 0) orelse return s.data.len; + const allocated = (strings.toUTF16Alloc(bun.default_allocator, s.data, false, false) catch return 0) orelse return s.data.len; defer bun.default_allocator.free(allocated); return @as(u32, @truncate(allocated.len)); } diff --git a/src/libarchive/libarchive.zig b/src/libarchive/libarchive.zig index a5e2f10db5..9226c817e5 100644 --- a/src/libarchive/libarchive.zig +++ b/src/libarchive/libarchive.zig @@ -490,6 +490,9 @@ pub const Archive = struct { const dir = dir_; const dir_fd = dir.fd; + const loop = if (Environment.isWindows) bun.Async.Loop.get() else {}; + var w_path: if (Environment.isWindows) bun.WPathBuffer else void = undefined; + loop: while (true) { const r = @as(Status, @enumFromInt(lib.archive_read_next_header(archive, &entry))); @@ -577,11 +580,13 @@ pub const Archive = struct { }, Kind.file => { const mode: bun.Mode = if (comptime Environment.isWindows) 0 else @intCast(lib.archive_entry_perm(entry)); - const file = dir.createFileZ(pathname, .{ .truncate = true, .mode = mode }) catch |err| brk: { + const os_path = if (Environment.isWindows) bun.strings.toWPathNormalized(&w_path, pathname) else pathname; + const createFileOS = if (Environment.isWindows) std.fs.Dir.createFileW else std.fs.Dir.createFileZ; + const file = createFileOS(dir, os_path, .{ .truncate = true, .mode = mode }) catch |err| brk: { switch (err) { error.AccessDenied, error.FileNotFound => { dir.makePath(std.fs.path.dirname(slice) orelse return err) catch {}; - break :brk try dir.createFileZ(pathname, .{ + break :brk try createFileOS(dir, os_path, .{ .truncate = true, .mode = mode, }); @@ -593,9 +598,19 @@ pub const Archive = struct { }; const file_handle = bun.toLibUVOwnedFD(file.handle); - defer { - if (comptime close_handles) _ = bun.sys.close(file_handle); - } + defer if (comptime close_handles) { + if (Environment.isWindows) { + // Using Async.Closer defers closing the file to a different thread. + // On windows, AV hangs these closes really badly. + // + // 'bun i @mui/icons-material' takes like 20 seconds to extract + // + // The install still takes a long time but this makes it a little bit better. + bun.Async.Closer.close(bun.uvfdcast(file_handle), loop); + } else { + _ = bun.sys.close(file_handle); + } + }; const entry_size = @max(lib.archive_entry_size(entry), 0); const size = @as(usize, @intCast(entry_size)); diff --git a/src/shell/interpreter.zig b/src/shell/interpreter.zig index 09f27a7538..13d928d2dc 100644 --- a/src/shell/interpreter.zig +++ b/src/shell/interpreter.zig @@ -1024,6 +1024,7 @@ pub fn NewInterpreter(comptime EventLoopKind: JSC.EventLoopKind) type { defer file.close(); break :src try file.reader().readAllAlloc(arena.allocator(), std.math.maxInt(u32)); }; + defer arena.deinit(); const jsobjs: []JSValue = &[_]JSValue{}; var out_parser: ?bun.shell.Parser = null; @@ -1067,6 +1068,52 @@ pub fn NewInterpreter(comptime EventLoopKind: JSC.EventLoopKind) type { mini.tick(&is_done, @as(fn (*anyopaque) bool, IsDone.isDone)); } + pub fn initAndRunFromSource(mini: *JSC.MiniEventLoop, path_for_errors: []const u8, src: []const u8) !void { + var arena = bun.ArenaAllocator.init(bun.default_allocator); + defer arena.deinit(); + + const jsobjs: []JSValue = &[_]JSValue{}; + var out_parser: ?bun.shell.Parser = null; + var out_lex_result: ?bun.shell.LexResult = null; + const script = ThisInterpreter.parse(&arena, src, jsobjs, &out_parser, &out_lex_result) catch |err| { + if (err == bun.shell.ParseError.Lex) { + std.debug.assert(out_lex_result != null); + const str = out_lex_result.?.combineErrors(arena.allocator()); + bun.Output.prettyErrorln("error: Failed to run script {s} due to error {s}", .{ path_for_errors, str }); + bun.Global.exit(1); + } + + if (out_parser) |*p| { + const errstr = p.combineErrors(); + bun.Output.prettyErrorln("error: Failed to run script {s} due to error {s}", .{ path_for_errors, errstr }); + bun.Global.exit(1); + } + + return err; + }; + const script_heap = try arena.allocator().create(ast.Script); + script_heap.* = script; + var interp = switch (ThisInterpreter.init(mini, bun.default_allocator, &arena, script_heap, jsobjs)) { + .err => |e| { + GlobalHandle.init(mini).actuallyThrow(e); + return; + }, + .result => |i| i, + }; + const IsDone = struct { + done: bool = false, + + fn isDone(this: *anyopaque) bool { + const asdlfk = bun.cast(*const @This(), this); + return asdlfk.done; + } + }; + var is_done: IsDone = .{}; + interp.done = &is_done.done; + try interp.run(); + mini.tick(&is_done, @as(fn (*anyopaque) bool, IsDone.isDone)); + } + pub fn run(this: *ThisInterpreter) !void { var root = Script.init(this, &this.root_shell, this.script, Script.ParentPtr.init(this), this.root_shell.io); this.started.store(true, .SeqCst); diff --git a/src/string.zig b/src/string.zig index c70920715e..2a1bbf5539 100644 --- a/src/string.zig +++ b/src/string.zig @@ -1270,7 +1270,7 @@ pub const SliceWithUnderlyingString = struct { } if (this.utf8.allocator.get()) |_| { - if (bun.strings.toUTF16Alloc(bun.default_allocator, this.utf8.slice(), false) catch null) |utf16| { + if (bun.strings.toUTF16Alloc(bun.default_allocator, this.utf8.slice(), false, false) catch null) |utf16| { this.utf8.deinit(); this.utf8 = .{}; return JSC.ZigString.toExternalU16(utf16.ptr, utf16.len, globalObject); diff --git a/src/string_immutable.zig b/src/string_immutable.zig index 7b7e8bf4cf..d3337312df 100644 --- a/src/string_immutable.zig +++ b/src/string_immutable.zig @@ -1316,7 +1316,7 @@ pub fn withoutUTF8BOM(bytes: []const u8) []const u8 { /// Convert a UTF-8 string to a UTF-16 string IF there are any non-ascii characters /// If there are no non-ascii characters, this returns null /// This is intended to be used for strings that go to JavaScript -pub fn toUTF16Alloc(allocator: std.mem.Allocator, bytes: []const u8, comptime fail_if_invalid: bool) !?[]u16 { +pub fn toUTF16Alloc(allocator: std.mem.Allocator, bytes: []const u8, comptime fail_if_invalid: bool, comptime sentinel: bool) !if (sentinel) ?[:0]u16 else ?[]u16 { if (strings.firstNonASCII(bytes)) |i| { const output_: ?std.ArrayList(u16) = if (comptime bun.FeatureFlags.use_simdutf) simd: { const trimmed = bun.simdutf.trim.utf8(bytes); @@ -1329,11 +1329,15 @@ pub fn toUTF16Alloc(allocator: std.mem.Allocator, bytes: []const u8, comptime fa if (out_length == 0) break :simd null; - var out = try allocator.alloc(u16, out_length); + var out = try allocator.alloc(u16, out_length + if (sentinel) 1 else 0); log("toUTF16 {d} UTF8 -> {d} UTF16", .{ bytes.len, out_length }); const res = bun.simdutf.convert.utf8.to.utf16.with_errors.le(trimmed, out); if (res.status == .success) { + if (comptime sentinel) { + out[out_length] = 0; + return out[0 .. out_length + 1 :0]; + } return out; } @@ -1429,13 +1433,33 @@ pub fn toUTF16Alloc(allocator: std.mem.Allocator, bytes: []const u8, comptime fa strings.copyU8IntoU16(output.items[output.items.len - remaining.len ..], remaining); } + if (comptime sentinel) { + output.items[output.items.len] = 0; + return output.items[0 .. output.items.len + 1 :0]; + } + return output.items; } return null; } -pub fn toUTF16AllocNoTrim(allocator: std.mem.Allocator, bytes: []const u8, comptime fail_if_invalid: bool) !?[]u16 { +// this one does the thing it's named after +pub fn toUTF16AllocForReal(allocator: std.mem.Allocator, bytes: []const u8, comptime fail_if_invalid: bool, comptime sentinel: bool) !if (sentinel) [:0]u16 else []u16 { + return (try toUTF16Alloc(allocator, bytes, fail_if_invalid, sentinel)) orelse { + const output = try allocator.alloc(u16, bytes.len + if (sentinel) 1 else 0); + bun.strings.copyU8IntoU16(output, bytes); + + if (comptime sentinel) { + output[bytes.len] = 0; + return output[0..bytes.len :0]; + } + + return output; + }; +} + +pub fn toUTF16AllocNoTrim(allocator: std.mem.Allocator, bytes: []const u8, comptime fail_if_invalid: bool, comptime _: bool) !?[]u16 { if (strings.firstNonASCII(bytes)) |i| { const output_: ?std.ArrayList(u16) = if (comptime bun.FeatureFlags.use_simdutf) simd: { const out_length = bun.simdutf.length.utf16.from.utf8(bytes); @@ -5256,34 +5280,37 @@ pub fn concatIfNeeded( std.debug.assert(remain.len == 0); } +/// This will simply ignore invalid UTF-8 and just do it pub fn convertUTF8toUTF16InBuffer( buf: []u16, input: []const u8, ) []u16 { - if (!Environment.isWindows) @compileError("please dont't use this function on posix until fixing the todos."); - - const result = bun.simdutf.convert.utf8.to.utf16.with_errors.le(input, buf); - switch (result.status) { - .success => return buf[0..result.count], - // TODO(@paperdave): handle surrogate - .surrogate => @panic("TODO: handle surrogate in convertUTF8toUTF16"), - else => @panic("TODO: handle error in convertUTF8toUTF16"), - } + // TODO(@paperdave): implement error handling here. + // for now this will cause invalid utf-8 to be ignored and become empty. + // this is lame because of https://github.com/oven-sh/bun/issues/8197 + // it will cause process.env.whatever to be len=0 instead of the data + // but it's better than failing the run entirely + // + // 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 + const result = bun.simdutf.convert.utf8.to.utf16.le(input, buf); + return buf[0..result]; } pub fn convertUTF16toUTF8InBuffer( buf: []u8, input: []const u16, ) ![]const u8 { - if (!Environment.isWindows) @compileError("please dont't use this function on posix until fixing the todos."); + // See above - const result = bun.simdutf.convert.utf16.to.utf8.with_errors.le(input, buf); - switch (result.status) { - .success => return buf[0..result.count], - // TODO(@paperdave): handle surrogate - .surrogate => @panic("TODO: handle surrogate in convertUTF8toUTF16"), - else => @panic("TODO: handle error in convertUTF16toUTF8InBuffer"), - } + const result = bun.simdutf.convert.utf16.to.utf8.le(input, buf); + // switch (result.status) { + // .success => return buf[0..result.count], + // // TODO(@paperdave): handle surrogate + // .surrogate => @panic("TODO: handle surrogate in convertUTF8toUTF16"), + // else => @panic("TODO: handle error in convertUTF16toUTF8InBuffer"), + // } + return buf[0..result]; } pub inline fn charIsAnySlash(char: u8) bool { From 01d41838c574fba34a2564b9281258a59f4cdd1b Mon Sep 17 00:00:00 2001 From: dave caruso Date: Mon, 29 Jan 2024 11:07:39 -0800 Subject: [PATCH 04/45] fix(builtin-bundler): do not replace globals when extending classes (#8557) * fix(builtin-bundler): do not replace globals when extending classes * format --- src/codegen/bundle-functions.ts | 3 ++- src/codegen/bundle-modules.ts | 3 ++- src/codegen/replacements.ts | 7 ++++++- src/js/README.md | 8 ++++---- src/js/private.d.ts | 5 ----- 5 files changed, 14 insertions(+), 12 deletions(-) diff --git a/src/codegen/bundle-functions.ts b/src/codegen/bundle-functions.ts index fa3907ceb1..417b910fbc 100644 --- a/src/codegen/bundle-functions.ts +++ b/src/codegen/bundle-functions.ts @@ -206,7 +206,8 @@ $$capture_start$$(${fn.async ? "async " : ""}${ ) ) .replace(/^\((async )?function\(/, "($1function (") - .replace(/__intrinsic__/g, "@") + "\n"; + .replace(/__intrinsic__/g, "@") + .replace(/__no_intrinsic__/g, "") + "\n"; bundledFunctions.push({ name: fn.name, diff --git a/src/codegen/bundle-modules.ts b/src/codegen/bundle-modules.ts index be479ae081..73d1b4c13e 100644 --- a/src/codegen/bundle-modules.ts +++ b/src/codegen/bundle-modules.ts @@ -232,7 +232,8 @@ for (const entrypoint of bundledEntryPoints) { .replace(/import.meta.require\((.*?)\)/g, (expr, specifier) => { throw new Error(`Builtin Bundler: do not use import.meta.require() (in ${file_path}))`); }) - .replace(/__intrinsic__/g, "@") + "\n"; + .replace(/__intrinsic__/g, "@") + .replace(/__no_intrinsic__/g, "") + "\n"; captured = captured.replace( /function\s*\(.*?\)\s*{/, '$&"use strict";' + diff --git a/src/codegen/replacements.ts b/src/codegen/replacements.ts index 2d16667949..8f8581f591 100644 --- a/src/codegen/replacements.ts +++ b/src/codegen/replacements.ts @@ -54,6 +54,11 @@ export const globalsToPrefix = [ "undefined", ]; +replacements.push({ + from: new RegExp(`\\bextends\\s+(${globalsToPrefix.join("|")})`, "g"), + to: "extends __no_intrinsic__%1", +}); + // These enums map to $IdToLabel and $LabelToId // Make sure to define in ./builtins.d.ts export const enums = { @@ -131,7 +136,7 @@ export function applyReplacements(src: string, length: number) { let rest = src.slice(length); slice = slice.replace(/([^a-zA-Z0-9_\$])\$([a-zA-Z0-9_]+\b)/gm, `$1__intrinsic__$2`); for (const replacement of replacements) { - slice = slice.replace(replacement.from, replacement.to.replaceAll("$", "__intrinsic__")); + slice = slice.replace(replacement.from, replacement.to.replaceAll("$", "__intrinsic__").replaceAll("%", "$")); } let match; if ((match = slice.match(/__intrinsic__(debug|assert)$/)) && rest.startsWith("(")) { diff --git a/src/js/README.md b/src/js/README.md index 52c9ba6f5e..3723492955 100644 --- a/src/js/README.md +++ b/src/js/README.md @@ -38,13 +38,13 @@ On top of this, we have some special functions that are handled by the builtin p - `require` works, but it must be passed a **string literal** that resolves to a module within `src/js`. This call gets replaced with `$getInternalField($internalModuleRegistery, )`, which directly loads the module by its generated numerical ID, skipping the resolver for inter-internal modules. -- `$debug` is exactly like console.log, but is stripped in release builds. It is disabled by default, requiring you to pass one of: `BUN_DEBUG_MODULE_NAME=1`, `BUN_DEBUG_JS=1`, or `BUN_DEBUG_ALL=1`. You can also do `if($debug) {}` to check if debug env var is set. +- `$debug()` is exactly like console.log, but is stripped in release builds. It is disabled by default, requiring you to pass one of: `BUN_DEBUG_MODULE_NAME=1`, `BUN_DEBUG_JS=1`, or `BUN_DEBUG_ALL=1`. You can also do `if($debug) {}` to check if debug env var is set. + +- `$assert()` in debug builds will assert the condition, but it is stripped in release builds. If an assertion fails, the program continues to run, but an error is logged in the console containing the original source condition and any extra messages specified. - `IS_BUN_DEVELOPMENT` is inlined to be `true` in all development builds. -- `process.platform` is properly inlined and DCE'd. Do use this to run different code on different platforms. - -- `$bundleError()` is like Zig's `@compileError`. It will stop a compile from succeeding. +- `process.platform` and `process.arch` is properly inlined and DCE'd. Do use this to run different code on different platforms. ## Builtin Modules diff --git a/src/js/private.d.ts b/src/js/private.d.ts index e7564e6b72..f79ded7f45 100644 --- a/src/js/private.d.ts +++ b/src/js/private.d.ts @@ -1,11 +1,6 @@ // The types in this file are not publicly defined, but do exist. // Stuff like `Bun.fs()` and so on. -/** - * Works like the zig `@compileError` built-in, but only supports plain strings. - */ -declare function $bundleError(error: string); - type BunFSWatchOptions = { encoding?: BufferEncoding; persistent?: boolean; recursive?: boolean; signal?: AbortSignal }; type BunWatchEventType = "rename" | "change" | "error" | "close"; type BunWatchListener = (event: WatchEventType, filename: T | undefined) => void; From 3046b0ee39a63731bdab18da3535fc106df41e5a Mon Sep 17 00:00:00 2001 From: dave caruso Date: Mon, 29 Jan 2024 11:08:09 -0800 Subject: [PATCH 05/45] feat(windows): allow open and Bun.file() with `/dev/null` (#8499) * DEV NULL * oops * ok --- src/bun.js/node/node_fs.zig | 6 +++- src/bun.js/webcore/blob.zig | 28 +++++++++++-------- test/js/bun/plugin/muh.ts | 24 ---------------- test/js/bun/shell/leak.test.ts | 6 ++-- .../bun/spawn/spawn-streaming-stdin.test.ts | 6 ++-- .../bun/spawn/spawn-streaming-stdout.test.ts | 5 ++-- test/js/bun/util/bun-file-windows.test.ts | 16 +++++++++++ 7 files changed, 47 insertions(+), 44 deletions(-) delete mode 100644 test/js/bun/plugin/muh.ts create mode 100644 test/js/bun/util/bun-file-windows.test.ts diff --git a/src/bun.js/node/node_fs.zig b/src/bun.js/node/node_fs.zig index 2e505b8de4..280f49078f 100644 --- a/src/bun.js/node/node_fs.zig +++ b/src/bun.js/node/node_fs.zig @@ -4486,7 +4486,11 @@ pub const NodeFS = struct { } pub fn open(this: *NodeFS, args: Arguments.Open, comptime _: Flavor) Maybe(Return.Open) { - const path = args.path.sliceZ(&this.sync_error_buf); + const path = if (Environment.isWindows and bun.strings.eqlComptime(args.path.slice(), "/dev/null")) + "\\\\.\\NUL" + else + args.path.sliceZ(&this.sync_error_buf); + return switch (Syscall.open(path, @intFromEnum(args.flags), args.mode)) { .err => |err| .{ .err = err.withPath(args.path.slice()), diff --git a/src/bun.js/webcore/blob.zig b/src/bun.js/webcore/blob.zig index 394c544112..29b439e287 100644 --- a/src/bun.js/webcore/blob.zig +++ b/src/bun.js/webcore/blob.zig @@ -1519,21 +1519,27 @@ pub const Blob = struct { return ptr.toJS(globalObject); } - pub fn findOrCreateFileFromPath(path_: *JSC.Node.PathOrFileDescriptor, globalThis: *JSGlobalObject) Blob { + pub fn findOrCreateFileFromPath(path_or_fd: *JSC.Node.PathOrFileDescriptor, globalThis: *JSGlobalObject) Blob { var vm = globalThis.bunVM(); const allocator = bun.default_allocator; const path: JSC.Node.PathOrFileDescriptor = brk: { - switch (path_.*) { + switch (path_or_fd.*) { .path => { - const slice = path_.path.slice(); + const slice = path_or_fd.path.slice(); + + if (Environment.isWindows and bun.strings.eqlComptime(slice, "/dev/null")) { + // it is okay to use rodata here, because the '.string' case + // in PathLike.deinit does not free anything. + path_or_fd.* = .{ .path = .{ .string = bun.PathString.init("\\\\.\\NUL") } }; + } if (vm.standalone_module_graph) |graph| { if (graph.find(slice)) |file| { defer { - if (path_.path != .string) { - path_.deinit(); - path_.* = .{ .path = .{ .string = bun.PathString.empty } }; + if (path_or_fd.path != .string) { + path_or_fd.deinit(); + path_or_fd.* = .{ .path = .{ .string = bun.PathString.empty } }; } } @@ -1541,13 +1547,13 @@ pub const Blob = struct { } } - path_.toThreadSafe(); - const copy = path_.*; - path_.* = .{ .path = .{ .string = bun.PathString.empty } }; + path_or_fd.toThreadSafe(); + const copy = path_or_fd.*; + path_or_fd.* = .{ .path = .{ .string = bun.PathString.empty } }; break :brk copy; }, .fd => { - switch (bun.FDTag.get(path_.fd)) { + switch (bun.FDTag.get(path_or_fd.fd)) { .stdin => return Blob.initWithStore( vm.rareData().stdin(), globalThis, @@ -1562,7 +1568,7 @@ pub const Blob = struct { ), else => {}, } - break :brk path_.*; + break :brk path_or_fd.*; }, } }; diff --git a/test/js/bun/plugin/muh.ts b/test/js/bun/plugin/muh.ts deleted file mode 100644 index d5fbbcd864..0000000000 --- a/test/js/bun/plugin/muh.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { plugin } from "bun"; - -await plugin({ - name: "svelte loader", - async setup(builder) { - var { compile } = await import("svelte/compiler"); - var { readFileSync } = await import("fs"); - await 2; - console.log(1); - builder.onLoad({ filter: /\.svelte$/ }, ({ path }) => { - console.log(2); - return { - contents: compile(readFileSync(path, "utf8"), { - filename: path, - generate: "ssr", - }).js.code, - loader: "js", - }; - }); - await 1; - }, -}); - -console.log(require("./hello.svelte")); diff --git a/test/js/bun/shell/leak.test.ts b/test/js/bun/shell/leak.test.ts index dd6979089d..2d38686cbe 100644 --- a/test/js/bun/shell/leak.test.ts +++ b/test/js/bun/shell/leak.test.ts @@ -4,7 +4,7 @@ import { $ } from "bun"; import { describe, expect, test } from "bun:test"; import { bunEnv } from "harness"; import { appendFileSync, closeSync, openSync, writeFileSync } from "node:fs"; -import { tmpdir } from "os"; +import { tmpdir, devNull } from "os"; import { join } from "path"; import { TestBuilder } from "./util"; @@ -55,13 +55,13 @@ describe("fd leak", () => { await builder().quiet().run(); } - const baseline = openSync("/dev/null", "r"); + const baseline = openSync(devNull, "r"); closeSync(baseline); for (let i = 0; i < runs; i++) { await builder().quiet().run(); } - const fd = openSync("/dev/null", "r"); + const fd = openSync(devNull, "r"); closeSync(fd); expect(fd).toBe(baseline); }, 100_000); diff --git a/test/js/bun/spawn/spawn-streaming-stdin.test.ts b/test/js/bun/spawn/spawn-streaming-stdin.test.ts index 1f9d804733..10f8c0d65f 100644 --- a/test/js/bun/spawn/spawn-streaming-stdin.test.ts +++ b/test/js/bun/spawn/spawn-streaming-stdin.test.ts @@ -3,13 +3,13 @@ import { it, test, expect } from "bun:test"; import { spawn } from "bun"; import { bunExe, bunEnv, gcTick } from "harness"; import { closeSync, openSync } from "fs"; -import { tmpdir } from "node:os"; +import { tmpdir, devNull } from "node:os"; import { join } from "path"; import { unlinkSync } from "node:fs"; const N = 100; test("spawn can write to stdin multiple chunks", async () => { - const maxFD = openSync("/dev/null", "w"); + const maxFD = openSync(devNull, "w"); for (let i = 0; i < N; i++) { var exited; await (async function () { @@ -59,7 +59,7 @@ test("spawn can write to stdin multiple chunks", async () => { } closeSync(maxFD); - const newMaxFD = openSync("/dev/null", "w"); + const newMaxFD = openSync(devNull, "w"); closeSync(newMaxFD); // assert we didn't leak any file descriptors diff --git a/test/js/bun/spawn/spawn-streaming-stdout.test.ts b/test/js/bun/spawn/spawn-streaming-stdout.test.ts index 7fc7161661..7947ff9170 100644 --- a/test/js/bun/spawn/spawn-streaming-stdout.test.ts +++ b/test/js/bun/spawn/spawn-streaming-stdout.test.ts @@ -3,6 +3,7 @@ import { it, test, expect } from "bun:test"; import { spawn } from "bun"; import { bunExe, bunEnv, gcTick } from "harness"; import { closeSync, openSync } from "fs"; +import { devNull } from "os"; test("spawn can read from stdout multiple chunks", async () => { gcTick(true); @@ -35,11 +36,11 @@ test("spawn can read from stdout multiple chunks", async () => { await proc.exited; })(); if (maxFD === -1) { - maxFD = openSync("/dev/null", "w"); + maxFD = openSync(devNull, "w"); closeSync(maxFD); } } - const newMaxFD = openSync("/dev/null", "w"); + const newMaxFD = openSync(devNull, "w"); closeSync(newMaxFD); expect(newMaxFD).toBe(maxFD); }, 60_000); diff --git a/test/js/bun/util/bun-file-windows.test.ts b/test/js/bun/util/bun-file-windows.test.ts new file mode 100644 index 0000000000..cdbf5ab71a --- /dev/null +++ b/test/js/bun/util/bun-file-windows.test.ts @@ -0,0 +1,16 @@ +import { openSync, closeSync } from "fs"; +import { open } from "fs/promises"; + +test('Bun.file("/dev/null") works on windows', async () => { + expect(await Bun.file("/dev/null").arrayBuffer()).toHaveLength(0); +}); + +test('openSync("/dev/null") works on windows', async () => { + const handle = openSync("/dev/null", "r"); + closeSync(handle); +}); + +test('open("/dev/null") works on windows', async () => { + const handle = await open("/dev/null", "r"); + await handle.close(); +}); From 1e9b44addac640b02fac38f992736aafabdd5a9c Mon Sep 17 00:00:00 2001 From: dave caruso Date: Mon, 29 Jan 2024 15:17:14 -0800 Subject: [PATCH 06/45] internal: report all failing tests to our internal feed (#8563) * yeah * oop * yeahh --- .github/workflows/bun-windows.yml | 16 ++++++++++ .../bun-internal-test/src/runner.node.mjs | 31 +++++++------------ 2 files changed, 28 insertions(+), 19 deletions(-) diff --git a/.github/workflows/bun-windows.yml b/.github/workflows/bun-windows.yml index 8b2a201dba..3595a5aea8 100644 --- a/.github/workflows/bun-windows.yml +++ b/.github/workflows/bun-windows.yml @@ -434,6 +434,22 @@ jobs: $null = node packages/bun-internal-test/src/runner.node.mjs ${{runner.temp}}/release/${{env.tag}}-${{ matrix.arch == 'x86_64' && 'x64' || 'aarch64' }}${{ matrix.cpu == 'nehalem' && '-baseline' || '' }}-profile/bun.exe || $true } catch {} $ErrorActionPreference = "Stop" + - uses: sarisia/actions-status-discord@v1 + if: always() && steps.test.outputs.failing_tests != '' && github.event_name == 'pull_request' + with: + title: "" + webhook: ${{ secrets.DISCORD_WEBHOOK_WINTEST }} + status: "failure" + noprefix: true + nocontext: true + description: | + ### ❌🪟 [${{github.event.pull_request.title}}](https://github.com/oven-sh/bun/pull/${{github.event.number}}) + + @${{ github.actor }}, there are **${{ steps.test.outputs.failing_test_count }} failing tests** on Windows ${{ matrix.arch }}${{ matrix.cpu == 'nehalem' && ' Baseline' || '' }} + + ${{ steps.test.outputs.failing_tests }} + + [Full Test Output](https://github.com/oven-sh/bun/actions/runs/${{github.run_id}}) - uses: sarisia/actions-status-discord@v1 if: always() && steps.test.outputs.regressing_tests != '' && github.event_name == 'pull_request' with: diff --git a/packages/bun-internal-test/src/runner.node.mjs b/packages/bun-internal-test/src/runner.node.mjs index b1b4e3bc6e..cec84753bf 100644 --- a/packages/bun-internal-test/src/runner.node.mjs +++ b/packages/bun-internal-test/src/runner.node.mjs @@ -107,9 +107,9 @@ async function runTest(path) { const expected_crash_reason = windows ? await readFile(resolve(path), "utf-8").then(data => { - const match = data.match(/@known-failing-on-windows:(.*)\n/); - return match ? match[1].trim() : null; - }) + const match = data.match(/@known-failing-on-windows:(.*)\n/); + return match ? match[1].trim() : null; + }) : null; const start = Date.now(); @@ -195,8 +195,7 @@ async function runTest(path) { } console.log( - `\x1b[2m${formatTime(duration).padStart(6, " ")}\x1b[0m ${ - passed ? "\x1b[32m✔" : expected_crash_reason ? "\x1b[33m⚠" : "\x1b[31m✖" + `\x1b[2m${formatTime(duration).padStart(6, " ")}\x1b[0m ${passed ? "\x1b[32m✔" : expected_crash_reason ? "\x1b[33m⚠" : "\x1b[31m✖" } ${name}\x1b[0m${reason ? ` (${reason})` : ""}`, ); @@ -320,10 +319,9 @@ console.log("\n" + "-".repeat(Math.min(process.stdout.columns || 40, 80)) + "\n" console.log(header); console.log("\n" + "-".repeat(Math.min(process.stdout.columns || 40, 80)) + "\n"); -let report = `# bun test on ${ - process.env["GITHUB_REF"] ?? +let report = `# bun test on ${process.env["GITHUB_REF"] ?? spawnSync("git", ["rev-parse", "--abbrev-ref", "HEAD"], { encoding: "utf-8" }).stdout.trim() -} + } \`\`\` ${header} @@ -347,8 +345,7 @@ if (regressions.length > 0) { report += regressions .map( ({ path, reason, expected_crash_reason }) => - `- [\`${path}\`](${sectionLink(path)}) ${reason}${ - expected_crash_reason ? ` (expected: ${expected_crash_reason})` : "" + `- [\`${path}\`](${sectionLink(path)}) ${reason}${expected_crash_reason ? ` (expected: ${expected_crash_reason})` : "" }`, ) .join("\n"); @@ -403,18 +400,14 @@ console.log("-> test-report.md, test-report.json"); if (ci) { if (windows) { - if (regressions.length > 0) { - action.setFailed(`${regressions.length} regressing tests`); - } action.setOutput("regressing_tests", regressions.map(({ path }) => `- \`${path}\``).join("\n")); action.setOutput("regressing_test_count", regressions.length); - } else { - if (failing_tests.length > 0) { - action.setFailed(`${failing_tests.length} files with failing tests`); - } - action.setOutput("failing_tests", failingTestDisplay); - action.setOutput("failing_tests_count", failing_tests.length); } + if (failing_tests.length > 0) { + action.setFailed(`${failing_tests.length} files with failing tests`); + } + action.setOutput("failing_tests", failingTestDisplay); + action.setOutput("failing_tests_count", failing_tests.length); let truncated_report = report; if (truncated_report.length > 512 * 1000) { truncated_report = truncated_report.slice(0, 512 * 1000) + "\n\n...truncated..."; From c538bf87d1d45451ac99bf8098d8476a7211c238 Mon Sep 17 00:00:00 2001 From: Dylan Conway <35280289+dylan-conway@users.noreply.github.com> Date: Mon, 29 Jan 2024 15:17:23 -0800 Subject: [PATCH 07/45] fix(windows): transpiler cache and other test fixes (#8471) * umask * process args * update reportError.test.ts * file exists * transpiler cache * back to const * remove failing comments * [autofix.ci] apply automated fixes * update comment * debug assert and remmove branch * oops * escape * path sep * seekTo * disable --------- Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> Co-authored-by: Jarred Sumner --- src/bun.js/RuntimeTranspilerCache.zig | 26 ++++++++++++++--------- src/bun.js/bindings/BunProcess.cpp | 9 ++++---- src/bun.js/webcore/blob.zig | 7 +----- src/bun.zig | 15 +++++++++++++ src/cli.zig | 17 +++++++++++++-- src/cli/create_command.zig | 2 ++ src/cli/init_command.zig | 2 ++ src/cli/install_completions_command.zig | 4 ++++ src/darwin_c.zig | 2 +- src/feature_flags.zig | 1 - src/fs.zig | 4 ++++ src/install/install.zig | 3 +++ src/linux_c.zig | 1 + src/sys.zig | 5 ++++- src/sys_uv.zig | 2 +- src/tmp.zig | 4 ++-- src/windows_c.zig | 6 ++++++ test/bundler/bundler_edgecase.test.ts | 3 ++- test/cli/run/run-process-env.test.ts | 7 +++--- test/cli/run/transpiler-cache.test.ts | 1 - test/js/bun/util/bun-file-exists.test.js | 1 - test/js/bun/util/reportError.test.ts | 6 +++--- test/js/node/process/process-args.test.js | 4 ++-- test/js/node/process/process.test.js | 23 +++++++++++++------- 24 files changed, 106 insertions(+), 49 deletions(-) diff --git a/src/bun.js/RuntimeTranspilerCache.zig b/src/bun.js/RuntimeTranspilerCache.zig index 891e3e4866..0bc5536c6a 100644 --- a/src/bun.js/RuntimeTranspilerCache.zig +++ b/src/bun.js/RuntimeTranspilerCache.zig @@ -210,16 +210,16 @@ pub const RuntimeTranspilerCache = struct { break :brk metadata_buf[0..metadata_stream.pos]; }; - const vecs: []const std.os.iovec_const = if (output_bytes.len > 0) + const vecs: []const bun.PlatformIOVecConst = if (output_bytes.len > 0) &.{ - .{ .iov_base = metadata_bytes.ptr, .iov_len = metadata_bytes.len }, - .{ .iov_base = output_bytes.ptr, .iov_len = output_bytes.len }, - .{ .iov_base = sourcemap.ptr, .iov_len = sourcemap.len }, + bun.platformIOVecConstCreate(metadata_bytes), + bun.platformIOVecConstCreate(output_bytes), + bun.platformIOVecConstCreate(sourcemap), } else &.{ - .{ .iov_base = metadata_bytes.ptr, .iov_len = metadata_bytes.len }, - .{ .iov_base = sourcemap.ptr, .iov_len = sourcemap.len }, + bun.platformIOVecConstCreate(metadata_bytes), + bun.platformIOVecConstCreate(sourcemap), }; var position: isize = 0; @@ -228,8 +228,13 @@ pub const RuntimeTranspilerCache = struct { if (bun.Environment.allow_assert) { var total: usize = 0; for (vecs) |v| { - std.debug.assert(v.iov_len > 0); - total += v.iov_len; + if (comptime bun.Environment.isWindows) { + std.debug.assert(v.len > 0); + total += v.len; + } else { + std.debug.assert(v.iov_len > 0); + total += v.iov_len; + } } std.debug.assert(end_position == total); } @@ -246,7 +251,7 @@ pub const RuntimeTranspilerCache = struct { } } - try tmpfile.finish(destination_path.sliceAssumeZ()); + try tmpfile.finish(@ptrCast(std.fs.path.basename(destination_path.slice()))); } pub fn load( @@ -481,6 +486,7 @@ pub const RuntimeTranspilerCache = struct { const file = cache_fd.asFile(); const metadata_bytes = try file.preadAll(&metadata_bytes_buf, 0); + if (comptime bun.Environment.isWindows) try file.seekTo(0); var metadata_stream = std.io.fixedBufferStream(metadata_bytes_buf[0..metadata_bytes]); var entry = Entry{ @@ -539,7 +545,7 @@ pub const RuntimeTranspilerCache = struct { const cache_dir_fd = brk: { if (std.fs.path.dirname(cache_file_path)) |dirname| { const dir = try std.fs.cwd().makeOpenPath(dirname, .{ .access_sub_paths = true }); - break :brk bun.toFD(dir.fd); + break :brk bun.toLibUVOwnedFD(dir.fd); } break :brk bun.toFD(std.fs.cwd().fd); diff --git a/src/bun.js/bindings/BunProcess.cpp b/src/bun.js/bindings/BunProcess.cpp index 2d6aef1963..a383be2ed8 100644 --- a/src/bun.js/bindings/BunProcess.cpp +++ b/src/bun.js/bindings/BunProcess.cpp @@ -32,6 +32,10 @@ #include #include #include +// Using the same typedef and define for `mode_t` and `umask` as node on windows. +// https://github.com/nodejs/node/blob/ad5e2dab4c8306183685973387829c2f69e793da/src/node_process_methods.cc#L29 +#define umask _umask +typedef int mode_t; #endif #include "JSNextTickQueue.h" #include "ProcessBindingUV.h" @@ -342,8 +346,6 @@ JSC_DEFINE_HOST_FUNCTION(Process_functionDlopen, JSC_DEFINE_HOST_FUNCTION(Process_functionUmask, (JSC::JSGlobalObject * globalObject, JSC::CallFrame* callFrame)) { -#if !OS(WINDOWS) - if (callFrame->argumentCount() == 0 || callFrame->argument(0).isUndefined()) { mode_t currentMask = umask(0); umask(currentMask); @@ -376,9 +378,6 @@ JSC_DEFINE_HOST_FUNCTION(Process_functionUmask, } return JSC::JSValue::encode(JSC::jsNumber(umask(newUmask))); -#else - return JSC::JSValue::encode(JSC::jsNumber(0)); -#endif } extern "C" uint64_t Bun__readOriginTimer(void*); diff --git a/src/bun.js/webcore/blob.zig b/src/bun.js/webcore/blob.zig index 29b439e287..e01a81d86d 100644 --- a/src/bun.js/webcore/blob.zig +++ b/src/bun.js/webcore/blob.zig @@ -2900,14 +2900,9 @@ pub const Blob = struct { return JSValue.jsBoolean(true); } - if (comptime Environment.isWindows) { - this.globalThis.throwTODO("exists is not implemented on Windows"); - return JSValue.jsUndefined(); - } - // We say regular files and pipes exist. // This is mostly meant for "Can we use this in new Response(file)?" - return JSValue.jsBoolean(bun.isRegularFile(store.data.file.mode) or std.os.S.ISFIFO(store.data.file.mode)); + return JSValue.jsBoolean(bun.isRegularFile(store.data.file.mode) or bun.C.S.ISFIFO(store.data.file.mode)); } // This mostly means 'can it be read?' diff --git a/src/bun.zig b/src/bun.zig index aebeb4f2ac..64eb4c5b61 100644 --- a/src/bun.zig +++ b/src/bun.zig @@ -130,6 +130,11 @@ pub const PlatformIOVec = if (Environment.isWindows) else std.os.iovec; +pub const PlatformIOVecConst = if (Environment.isWindows) + windows.libuv.uv_buf_t +else + std.os.iovec_const; + pub fn platformIOVecCreate(input: []const u8) PlatformIOVec { if (Environment.isWindows) return windows.libuv.uv_buf_t.init(input); if (Environment.allow_assert) { @@ -140,6 +145,16 @@ pub fn platformIOVecCreate(input: []const u8) PlatformIOVec { return .{ .iov_len = @intCast(input.len), .iov_base = @constCast(input.ptr) }; } +pub fn platformIOVecConstCreate(input: []const u8) PlatformIOVecConst { + if (Environment.isWindows) return windows.libuv.uv_buf_t.init(input); + if (Environment.allow_assert) { + if (input.len > @as(usize, std.math.maxInt(u32))) { + Output.debugWarn("call to bun.PlatformIOVecConst.init with length larger than u32, this will overflow on windows", .{}); + } + } + return .{ .iov_len = @intCast(input.len), .iov_base = input.ptr }; +} + pub fn platformIOVecToSlice(iovec: PlatformIOVec) []u8 { if (Environment.isWindows) return windows.libuv.uv_buf_t.slice(iovec); return iovec.base[0..iovec.len]; diff --git a/src/cli.zig b/src/cli.zig index a6bd336d3f..12e726d3e6 100644 --- a/src/cli.zig +++ b/src/cli.zig @@ -1779,6 +1779,8 @@ pub const Command = struct { const script_name_to_search = ctx.args.entry_points[0]; + var absolute_script_path: ?string = null; + var file_path = script_name_to_search; const file_: anyerror!std.fs.File = brk: { if (std.fs.path.isAbsoluteWindows(script_name_to_search)) { @@ -1787,6 +1789,7 @@ pub const Command = struct { if (comptime Environment.isWindows) { resolved = resolve_path.normalizeString(resolved, true, .windows); } + absolute_script_path = resolved; break :brk bun.openFile( resolved, .{ .mode = .read_only }, @@ -1822,7 +1825,17 @@ pub const Command = struct { Global.configureAllocator(.{ .long_running = true }); // the case where this doesn't work is if the script name on disk doesn't end with a known JS-like file extension - const absolute_script_path = bun.getFdPath(file.handle, &script_name_buf) catch return false; + absolute_script_path = absolute_script_path orelse brk: { + if (comptime !Environment.isWindows) break :brk bun.getFdPath(file.handle, &script_name_buf) catch return false; + + var fd_path_buf: bun.PathBuffer = undefined; + const path = bun.getFdPath(file.handle, &fd_path_buf) catch return false; + break :brk resolve_path.normalizeString( + resolve_path.PosixToWinNormalizer.resolveCWDWithExternalBufZ(&script_name_buf, path) catch @panic("Could not resolve path"), + true, + .windows, + ); + }; if (!ctx.debug.loaded_bunfig) { bun.CLI.Arguments.loadConfigPath(ctx.allocator, true, "bunfig.toml", ctx, .RunCommand) catch {}; @@ -1830,7 +1843,7 @@ pub const Command = struct { BunJS.Run.boot( ctx.*, - absolute_script_path, + absolute_script_path.?, ) catch |err| { if (Output.enable_ansi_colors) { ctx.log.printForLogLevelWithEnableAnsiColors(Output.errorWriter(), true) catch {}; diff --git a/src/cli/create_command.zig b/src/cli/create_command.zig index df48d36b09..99cfcbba52 100644 --- a/src/cli/create_command.zig +++ b/src/cli/create_command.zig @@ -555,6 +555,7 @@ pub const CreateCommand = struct { package_json_contents = try MutableString.init(ctx.allocator, size); package_json_contents.list.expandToCapacity(); + const prev_file_pos = if (comptime Environment.isWindows) try pkg.getPos() else 0; _ = pkg.preadAll(package_json_contents.list.items, 0) catch |err| { package_json_file = null; @@ -565,6 +566,7 @@ pub const CreateCommand = struct { Output.prettyErrorln("Error reading package.json: {s}", .{@errorName(err)}); break :read_package_json; }; + if (comptime Environment.isWindows) try pkg.seekTo(prev_file_pos); // The printer doesn't truncate, so we must do so manually std.os.ftruncate(pkg.handle, 0) catch {}; diff --git a/src/cli/init_command.zig b/src/cli/init_command.zig index fa982cf214..05153b35dc 100644 --- a/src/cli/init_command.zig +++ b/src/cli/init_command.zig @@ -136,10 +136,12 @@ pub const InitCommand = struct { package_json_contents = try MutableString.init(alloc, size); package_json_contents.list.expandToCapacity(); + const prev_file_pos = if (comptime Environment.isWindows) try pkg.getPos() else 0; _ = pkg.preadAll(package_json_contents.list.items, 0) catch { package_json_file = null; break :read_package_json; }; + if (comptime Environment.isWindows) try pkg.seekTo(prev_file_pos); } } diff --git a/src/cli/install_completions_command.zig b/src/cli/install_completions_command.zig index b2cf708249..b0953d9b99 100644 --- a/src/cli/install_completions_command.zig +++ b/src/cli/install_completions_command.zig @@ -446,6 +446,10 @@ pub const InstallCompletionsCommand = struct { 0, ) catch break :brk true; + if (comptime Environment.isWindows) { + try dot_zshrc.seekTo(0); + } + const contents = buf[0..read]; // Do they possibly have it in the file already? diff --git a/src/darwin_c.zig b/src/darwin_c.zig index d3f715fc45..b3ec5b1015 100644 --- a/src/darwin_c.zig +++ b/src/darwin_c.zig @@ -775,8 +775,8 @@ pub const preallocate_length = std.math.maxInt(u51); pub const Mode = std.os.mode_t; pub const E = std.os.E; +pub const S = std.os.S; pub fn getErrno(rc: anytype) E { return std.c.getErrno(rc); } - pub extern "c" fn umask(Mode) Mode; diff --git a/src/feature_flags.zig b/src/feature_flags.zig index 4c3874bc40..d42ae89f68 100644 --- a/src/feature_flags.zig +++ b/src/feature_flags.zig @@ -177,5 +177,4 @@ pub const concurrent_transpiler = !env.isWindows; // https://github.com/oven-sh/bun/issues/5426#issuecomment-1813865316 pub const disable_auto_js_to_ts_in_node_modules = true; -// TODO: implement the IO for rtc for windows pub const runtime_transpiler_cache = !env.isWindows; diff --git a/src/fs.zig b/src/fs.zig index dab45f0df6..973e54ce6a 100644 --- a/src/fs.zig +++ b/src/fs.zig @@ -1133,10 +1133,12 @@ pub const FileSystem = struct { while (true) { // We use pread to ensure if the file handle was open, it doesn't seek from the last position + const prev_file_pos = if (comptime Environment.isWindows) try file.getPos() else 0; const read_count = file.preadAll(shared_buffer.list.items[offset..], offset) catch |err| { fs.readFileError(path, err); return err; }; + if (comptime Environment.isWindows) try file.seekTo(prev_file_pos); shared_buffer.list.items = shared_buffer.list.items[0 .. read_count + offset]; file_contents = shared_buffer.list.items; debug("pread({d}, {d}) = {d}", .{ file.handle, size, read_count }); @@ -1179,10 +1181,12 @@ pub const FileSystem = struct { // stick a zero at the end buf[size] = 0; + const prev_file_pos = if (comptime Environment.isWindows) try file.getPos() else 0; const read_count = file.preadAll(buf, 0) catch |err| { fs.readFileError(path, err); return err; }; + if (comptime Environment.isWindows) try file.seekTo(prev_file_pos); file_contents = buf[0..read_count]; debug("pread({d}, {d}) = {d}", .{ file.handle, size, read_count }); diff --git a/src/install/install.zig b/src/install/install.zig index 41fd42c9e5..2a07a5a6d8 100644 --- a/src/install/install.zig +++ b/src/install/install.zig @@ -6582,6 +6582,7 @@ pub const PackageManager = struct { current_package_json_buf, 0, ); + if (comptime Environment.isWindows) try manager.root_package_json_file.seekTo(0); const package_json_source = logger.Source.initPathString( package_json_cwd, @@ -6757,6 +6758,7 @@ pub const PackageManager = struct { current_package_json_buf, 0, ); + if (comptime Environment.isWindows) try manager.root_package_json_file.seekTo(0); const package_json_source = logger.Source.initPathString( package_json_cwd, @@ -7462,6 +7464,7 @@ pub const PackageManager = struct { current_package_json_buf, 0, ); + if (comptime Environment.isWindows) try manager.root_package_json_file.seekTo(0); const package_json_source = logger.Source.initPathString( package_json_cwd, diff --git a/src/linux_c.zig b/src/linux_c.zig index f15eb555c5..19f289f9d5 100644 --- a/src/linux_c.zig +++ b/src/linux_c.zig @@ -576,6 +576,7 @@ pub const IFF_LOOPBACK = net_c.IFF_LOOPBACK; pub const Mode = u32; pub const E = std.os.E; +pub const S = std.os.S; pub extern "c" fn umask(Mode) Mode; diff --git a/src/sys.zig b/src/sys.zig index f978375452..d96b738ea2 100644 --- a/src/sys.zig +++ b/src/sys.zig @@ -872,7 +872,10 @@ pub fn writev(fd: bun.FileDescriptor, buffers: []std.os.iovec) Maybe(usize) { } } -pub fn pwritev(fd: bun.FileDescriptor, buffers: []const std.os.iovec_const, position: isize) Maybe(usize) { +pub fn pwritev(fd: bun.FileDescriptor, buffers: []const bun.PlatformIOVecConst, position: isize) Maybe(usize) { + if (comptime Environment.isWindows) { + return sys_uv.pwritev(fd, buffers, position); + } if (comptime Environment.isMac) { const rc = pwritev_sym(fd.cast(), buffers.ptr, @as(i32, @intCast(buffers.len)), position); if (comptime Environment.allow_assert) diff --git a/src/sys_uv.zig b/src/sys_uv.zig index 696302b6c5..f1ef3de9b5 100644 --- a/src/sys_uv.zig +++ b/src/sys_uv.zig @@ -333,7 +333,7 @@ pub fn preadv(fd: FileDescriptor, bufs: []const bun.PlatformIOVec, position: i64 } } -pub fn pwritev(fd: FileDescriptor, bufs: []const bun.PlatformIOVec, position: i64) Maybe(usize) { +pub fn pwritev(fd: FileDescriptor, bufs: []const bun.PlatformIOVecConst, position: i64) Maybe(usize) { const uv_fd = bun.uvfdcast(fd); comptime std.debug.assert(bun.PlatformIOVec == uv.uv_buf_t); diff --git a/src/tmp.zig b/src/tmp.zig index ce30a8b058..2efd8c82ec 100644 --- a/src/tmp.zig +++ b/src/tmp.zig @@ -28,7 +28,7 @@ pub const Tmpfile = struct { if (comptime allow_tmpfile) { switch (bun.sys.openat(destination_dir, ".", O.WRONLY | O.TMPFILE | O.CLOEXEC, perm)) { .result => |fd| { - tmpfile.fd = fd; + tmpfile.fd = bun.toLibUVOwnedFD(fd); break :open; }, .err => |err| { @@ -43,7 +43,7 @@ pub const Tmpfile = struct { } tmpfile.fd = switch (bun.sys.openat(destination_dir, tmpfilename, O.CREAT | O.CLOEXEC | O.WRONLY, perm)) { - .result => |fd| fd, + .result => |fd| bun.toLibUVOwnedFD(fd), .err => |err| return .{ .err = err }, }; break :open; diff --git a/src/windows_c.zig b/src/windows_c.zig index 37c6fb5d83..964451f719 100644 --- a/src/windows_c.zig +++ b/src/windows_c.zig @@ -1269,6 +1269,12 @@ pub fn renameAtW( new_path_w: []const u16, replace_if_exists: bool, ) Maybe(void) { + if (comptime bun.Environment.allow_assert) { + // if the directories are the same and the destination path is absolute, the old path name is kept + if (old_dir_fd == new_dir_fd) { + std.debug.assert(!std.fs.path.isAbsoluteWindowsWTF16(new_path_w)); + } + } const src_fd = switch (bun.sys.ntCreateFile( old_dir_fd, old_path_w, diff --git a/test/bundler/bundler_edgecase.test.ts b/test/bundler/bundler_edgecase.test.ts index f9a427ab0f..ea9cd810e2 100644 --- a/test/bundler/bundler_edgecase.test.ts +++ b/test/bundler/bundler_edgecase.test.ts @@ -1,5 +1,6 @@ import assert from "assert"; import dedent from "dedent"; +import { sep } from "path"; import { itBundled, testForFile } from "./expectBundled"; var { describe, test, expect } = testForFile(import.meta.path); @@ -36,7 +37,7 @@ describe("bundler", () => { }, target: "bun", run: { - stdout: "a/b", + stdout: `a${sep}b`, }, }); itBundled("edgecase/ImportStarFunction", { diff --git a/test/cli/run/run-process-env.test.ts b/test/cli/run/run-process-env.test.ts index af4f86b541..44bc169824 100644 --- a/test/cli/run/run-process-env.test.ts +++ b/test/cli/run/run-process-env.test.ts @@ -1,4 +1,3 @@ -// @known-failing-on-windows: 1 failing import { describe, expect, test } from "bun:test"; import { bunExe, bunRunAsScript, tempDirWithFiles } from "harness"; @@ -7,7 +6,7 @@ describe("process.env", () => { const scriptName = "start:dev"; const dir = tempDirWithFiles("processenv", { - "package.json": `{'scripts': {'${scriptName}': '${bunExe()} run index.ts'}}`, + "package.json": JSON.stringify({ "scripts": { [`${scriptName}`]: `${bunExe()} run index.ts` } }), "index.ts": "console.log(process.env.npm_lifecycle_event);", }); @@ -18,9 +17,9 @@ describe("process.env", () => { // https://github.com/oven-sh/bun/issues/3589 test("npm_lifecycle_event should have the value of the last call", () => { const dir = tempDirWithFiles("processenv_ls_call", { - "package.json": `{"scripts": { "first": "${bunExe()} run --cwd lsc second" } }`, + "package.json": JSON.stringify({ scripts: { first: `${bunExe()} run --cwd lsc second` } }), "lsc": { - "package.json": `{"scripts": { "second": "${bunExe()} run index.ts" } }`, + "package.json": JSON.stringify({ scripts: { second: `${bunExe()} run index.ts` } }), "index.ts": "console.log(process.env.npm_lifecycle_event);", }, }); diff --git a/test/cli/run/transpiler-cache.test.ts b/test/cli/run/transpiler-cache.test.ts index cc4a915912..2ce052cdfb 100644 --- a/test/cli/run/transpiler-cache.test.ts +++ b/test/cli/run/transpiler-cache.test.ts @@ -1,4 +1,3 @@ -// @known-failing-on-windows: 1 failing import assert from "assert"; import { Subprocess } from "bun"; import { beforeEach, describe, expect, test } from "bun:test"; diff --git a/test/js/bun/util/bun-file-exists.test.js b/test/js/bun/util/bun-file-exists.test.js index 603c7adf34..cca28e3599 100644 --- a/test/js/bun/util/bun-file-exists.test.js +++ b/test/js/bun/util/bun-file-exists.test.js @@ -1,4 +1,3 @@ -// @known-failing-on-windows: 1 failing import { test, expect } from "bun:test"; import { join } from "path"; import { tmpdir } from "os"; diff --git a/test/js/bun/util/reportError.test.ts b/test/js/bun/util/reportError.test.ts index 223e0f626a..f6520b90b7 100644 --- a/test/js/bun/util/reportError.test.ts +++ b/test/js/bun/util/reportError.test.ts @@ -1,12 +1,12 @@ -// @known-failing-on-windows: 1 failing import { test, expect } from "bun:test"; import { spawnSync } from "bun"; +import { join } from "path"; import { bunEnv, bunExe } from "harness"; test("reportError", () => { const cwd = import.meta.dir; const { stderr } = spawnSync({ - cmd: [bunExe(), new URL("./reportError.ts", import.meta.url).pathname], + cmd: [bunExe(), join(import.meta.dir, "reportError.ts")], cwd, env: { ...bunEnv, @@ -14,6 +14,6 @@ test("reportError", () => { BUN_JSC_showPrivateScriptsInStackTraces: "0", }, }); - const output = stderr.toString().replaceAll(cwd, ""); + const output = stderr.toString().replaceAll(cwd, "").replaceAll("\\", "/"); expect(output).toMatchSnapshot(); }); diff --git a/test/js/node/process/process-args.test.js b/test/js/node/process/process-args.test.js index cb9bda00d0..c95d2e22b6 100644 --- a/test/js/node/process/process-args.test.js +++ b/test/js/node/process/process-args.test.js @@ -1,11 +1,11 @@ -// @known-failing-on-windows: 1 failing import { spawn } from "bun"; import { test, expect } from "bun:test"; +import { join } from "path"; import { bunExe } from "harness"; test("args exclude run", async () => { const arg0 = process.argv[0]; - const arg1 = import.meta.dir + "/print-process-args.js"; + const arg1 = join(import.meta.dir, "/print-process-args.js"); const exe = bunExe(); const { stdout: s1 } = spawn([exe, "print-process-args.js"], { cwd: import.meta.dir, diff --git a/test/js/node/process/process.test.js b/test/js/node/process/process.test.js index d2d18b4dbb..ffdd7ab269 100644 --- a/test/js/node/process/process.test.js +++ b/test/js/node/process/process.test.js @@ -9,13 +9,14 @@ it("process", () => { // this property isn't implemented yet but it should at least return a string const isNode = !process.isBun; - if (!isNode && process.title !== "bun") throw new Error("process.title is not 'bun'"); + if (!isNode && process.platform !== "win32" && process.title !== "bun") throw new Error("process.title is not 'bun'"); if (typeof process.env.USER !== "string") throw new Error("process.env is not an object"); if (process.env.USER.length === 0) throw new Error("process.env is missing a USER property"); - if (process.platform !== "darwin" && process.platform !== "linux") throw new Error("process.platform is invalid"); + if (process.platform !== "darwin" && process.platform !== "linux" && process.platform !== "win32") + throw new Error("process.platform is invalid"); if (isNode) throw new Error("process.isBun is invalid"); @@ -68,8 +69,9 @@ it("process.hrtime.bigint()", () => { it("process.release", () => { expect(process.release.name).toBe("node"); + const platform = process.platform == "win32" ? "windows" : process.platform; expect(process.release.sourceUrl).toContain( - `https://github.com/oven-sh/bun/release/bun-v${process.versions.bun}/bun-${process.platform}-${ + `https://github.com/oven-sh/bun/release/bun-v${process.versions.bun}/bun-${platform}-${ { arm64: "aarch64", x64: "x64" }[process.arch] || process.arch }`, ); @@ -155,11 +157,16 @@ it("process.umask()", () => { }).toThrow(RangeError); } - const orig = process.umask(0o777); - expect(orig).toBeGreaterThan(0); - expect(process.umask()).toBe(0o777); - expect(process.umask(undefined)).toBe(0o777); - expect(process.umask(Number(orig))).toBe(0o777); + const mask = process.platform == "win32" ? 0o600 : 0o777; + const orig = process.umask(mask); + if (process.platform == "win32") { + expect(orig).toBe(0); + } else { + expect(orig).toBeGreaterThan(0); + } + expect(process.umask()).toBe(mask); + expect(process.umask(undefined)).toBe(mask); + expect(process.umask(Number(orig))).toBe(mask); expect(process.umask()).toBe(orig); }); From 413aaaff3307abcbaffb11612ae93aa0d9f74580 Mon Sep 17 00:00:00 2001 From: Jarred Sumner Date: Mon, 29 Jan 2024 15:17:51 -0800 Subject: [PATCH 08/45] [bun:sqlite] Support multiple statements in db.run() (#8541) * [bun:sqlite] Support multiple statements in db.run() * Update sqlite.test.js * Update JSSQLStatement.cpp * Another test --------- Co-authored-by: Jarred Sumner <709451+Jarred-Sumner@users.noreply.github.com> --- src/bun.js/bindings/sqlite/JSSQLStatement.cpp | 128 ++++++++++++------ test/js/bun/sqlite/sqlite.test.js | 74 ++++++++++ 2 files changed, 159 insertions(+), 43 deletions(-) diff --git a/src/bun.js/bindings/sqlite/JSSQLStatement.cpp b/src/bun.js/bindings/sqlite/JSSQLStatement.cpp index b0d0f0cdfc..b381eaca39 100644 --- a/src/bun.js/bindings/sqlite/JSSQLStatement.cpp +++ b/src/bun.js/bindings/sqlite/JSSQLStatement.cpp @@ -96,6 +96,16 @@ static void enableFastMallocForSQLite() #endif } +class AutoDestructingSQLiteStatement { +public: + sqlite3_stmt* stmt { nullptr }; + + ~AutoDestructingSQLiteStatement() + { + sqlite3_finalize(stmt); + } +}; + static void initializeSQLite() { static std::once_flag onceFlag; @@ -899,6 +909,11 @@ JSC_DEFINE_HOST_FUNCTION(jsSQLStatementLoadExtensionFunction, (JSC::JSGlobalObje RELEASE_AND_RETURN(scope, JSValue::encode(JSC::jsUndefined())); } +static bool isSkippedInSQLiteQuery(const char c) +{ + return c == ' ' || c == ';' || (c >= '\t' && c <= '\r'); +} + // This runs a query one-off // without the overhead of a long-lived statement object // does not return anything @@ -945,65 +960,92 @@ JSC_DEFINE_HOST_FUNCTION(jsSQLStatementExecuteFunction, (JSC::JSGlobalObject * l return JSValue::encode(JSC::jsUndefined()); } - // TODO: trim whitespace & newlines before sending - // we don't because webkit doesn't expose a function that makes this super - // easy without using unicode whitespace definition the - // StringPrototype.trim() implementation GC allocates a new JSString* and - // potentially re-allocates the string (not 100% sure if reallocates) so we - // can't use that here - sqlite3_stmt* statement = nullptr; + CString utf8; + + const char* sqlStringHead; + const char* end; + bool didSetBindings = false; - int rc = SQLITE_OK; if ( // fast path: ascii latin1 string is utf8 sqlString.is8Bit() && simdutf::validate_ascii(reinterpret_cast(sqlString.characters8()), sqlString.length())) { - rc = sqlite3_prepare_v3(db, reinterpret_cast(sqlString.characters8()), sqlString.length(), 0, &statement, nullptr); + + sqlStringHead = reinterpret_cast(sqlString.characters8()); + end = sqlStringHead + sqlString.length(); } else { // slow path: utf16 or latin1 string with supplemental characters - CString utf8 = sqlString.utf8(); - rc = sqlite3_prepare_v3(db, utf8.data(), utf8.length(), 0, &statement, nullptr); + utf8 = sqlString.utf8(); + sqlStringHead = utf8.data(); + end = sqlStringHead + utf8.length(); } - if (UNLIKELY(rc != SQLITE_OK || statement == nullptr)) { - throwException(lexicalGlobalObject, scope, createError(lexicalGlobalObject, rc == SQLITE_OK ? "Query contained no valid SQL statement; likely empty query."_s : WTF::String::fromUTF8(sqlite3_errmsg(db)))); - // sqlite3 handles when the pointer is null - sqlite3_finalize(statement); - return JSValue::encode(JSC::jsUndefined()); - } + bool didExecuteAny = false; - if (!bindingsAliveScope.value().isUndefinedOrNull()) { - if (bindingsAliveScope.value().isObject()) { - JSC::JSValue reb = rebindStatement(lexicalGlobalObject, bindingsAliveScope.value(), scope, db, statement, false); - if (UNLIKELY(!reb.isNumber())) { - sqlite3_finalize(statement); - return JSValue::encode(reb); /* this means an error */ - } - } else { - throwException(lexicalGlobalObject, scope, createTypeError(lexicalGlobalObject, "Expected bindings to be an object or array"_s)); - sqlite3_finalize(statement); - return JSValue::encode(jsUndefined()); + int rc = SQLITE_OK; + +#if ASSERT_ENABLED + int maxSqlStringBytes = end - sqlStringHead; +#endif + + while (sqlStringHead && sqlStringHead < end) { + if (UNLIKELY(isSkippedInSQLiteQuery(*sqlStringHead))) { + sqlStringHead++; + + while (sqlStringHead < end && isSkippedInSQLiteQuery(*sqlStringHead)) + sqlStringHead++; } + + AutoDestructingSQLiteStatement sql; + const char* tail = nullptr; + + // Bounds checks + ASSERT(end >= sqlStringHead); + ASSERT(end - sqlStringHead >= 0); + ASSERT(end - sqlStringHead <= maxSqlStringBytes); + + rc = sqlite3_prepare_v3(db, sqlStringHead, end - sqlStringHead, 0, &sql.stmt, &tail); + + if (rc != SQLITE_OK) + break; + + if (!sql.stmt) { + // this happens for an empty statement + sqlStringHead = tail; + continue; + } + + // First statement gets the bindings. + if (!didSetBindings && !bindingsAliveScope.value().isUndefinedOrNull()) { + if (bindingsAliveScope.value().isObject()) { + JSC::JSValue reb = rebindStatement(lexicalGlobalObject, bindingsAliveScope.value(), scope, db, sql.stmt, false); + if (UNLIKELY(!reb.isNumber())) { + return JSValue::encode(reb); /* this means an error */ + } + } else { + throwException(lexicalGlobalObject, scope, createTypeError(lexicalGlobalObject, "Expected bindings to be an object or array"_s)); + return JSValue::encode(jsUndefined()); + } + didSetBindings = true; + } + + do { + rc = sqlite3_step(sql.stmt); + } while (rc == SQLITE_ROW); + + didExecuteAny = true; + sqlStringHead = tail; } - rc = sqlite3_step(statement); - if (!sqlite3_stmt_readonly(statement)) { - databases()[handle]->version++; - } - - while (rc == SQLITE_ROW) { - rc = sqlite3_step(statement); - } - - if (UNLIKELY(rc != SQLITE_DONE && rc != SQLITE_OK)) { + if (UNLIKELY(rc != SQLITE_OK && rc != SQLITE_DONE)) { throwException(lexicalGlobalObject, scope, createSQLiteError(lexicalGlobalObject, db)); - // we finalize after just incase something about error messages in - // sqlite depends on the existence of the most recent statement i don't - // think that's actually how this works - just being cautious - sqlite3_finalize(statement); return JSValue::encode(JSC::jsUndefined()); } - sqlite3_finalize(statement); + if (!didExecuteAny) { + throwException(lexicalGlobalObject, scope, createError(lexicalGlobalObject, "Query contained no valid SQL statement; likely empty query."_s)); + return JSValue::encode(JSC::jsUndefined()); + } + return JSValue::encode(jsUndefined()); } diff --git a/test/js/bun/sqlite/sqlite.test.js b/test/js/bun/sqlite/sqlite.test.js index 2d2c643d04..79a8514f71 100644 --- a/test/js/bun/sqlite/sqlite.test.js +++ b/test/js/bun/sqlite/sqlite.test.js @@ -669,3 +669,77 @@ it("empty blob", () => { }, ]); }); + +it("multiple statements with a schema change", () => { + const db = new Database(":memory:"); + db.run( + ` + CREATE TABLE foo (id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT); + CREATE TABLE bar (id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT); + + INSERT INTO foo (name) VALUES ('foo'); + INSERT INTO foo (name) VALUES ('bar'); + + INSERT INTO bar (name) VALUES ('foo'); + INSERT INTO bar (name) VALUES ('bar'); + `, + ); + + expect(db.query("SELECT * FROM foo").all()).toEqual([ + { + id: 1, + name: "foo", + }, + { + id: 2, + name: "bar", + }, + ]); + + expect(db.query("SELECT * FROM bar").all()).toEqual([ + { + id: 1, + name: "foo", + }, + { + id: 2, + name: "bar", + }, + ]); +}); + +it("multiple statements", () => { + const fixtures = [ + "INSERT INTO foo (name) VALUES ('foo')", + "INSERT INTO foo (name) VALUES ('barabc')", + "INSERT INTO foo (name) VALUES ('!bazaspdok')", + ]; + for (let separator of [";", ";\n", "\n;", "\r\n;", ";\r\n", ";\t", "\t;", "\r\n;"]) { + for (let spaceOffset of [1, 0, -1]) { + for (let spacesCount = 0; spacesCount < 8; spacesCount++) { + const db = new Database(":memory:"); + db.run("CREATE TABLE foo (id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT)"); + + const prefix = spaceOffset < 0 ? " ".repeat(spacesCount) : ""; + const suffix = spaceOffset > 0 ? " ".repeat(spacesCount) : ""; + const query = fixtures.join(prefix + separator + suffix); + db.run(query); + + expect(db.query("SELECT * FROM foo").all()).toEqual([ + { + id: 1, + name: "foo", + }, + { + id: 2, + name: "barabc", + }, + { + id: 3, + name: "!bazaspdok", + }, + ]); + } + } + } +}); From 9fa26e6a09e777238e7db4245d336053a48ad9c7 Mon Sep 17 00:00:00 2001 From: Jarred Sumner Date: Mon, 29 Jan 2024 15:48:26 -0800 Subject: [PATCH 09/45] Close more file descriptors in `bun --watch` (#8533) * Close more file descriptors in `bun --watch` * Reset signals * Add comment * Update bun.zig --------- Co-authored-by: Jarred Sumner <709451+Jarred-Sumner@users.noreply.github.com> --- packages/bun-usockets/src/bsd.c | 2 ++ src/bun.js/bindings/c-bindings.cpp | 24 ++++++++++++++++++++++++ src/bun.zig | 14 +++++++++++++- 3 files changed, 39 insertions(+), 1 deletion(-) diff --git a/packages/bun-usockets/src/bsd.c b/packages/bun-usockets/src/bsd.c index e520fa8721..4a58b984eb 100644 --- a/packages/bun-usockets/src/bsd.c +++ b/packages/bun-usockets/src/bsd.c @@ -269,6 +269,8 @@ LIBUS_SOCKET_DESCRIPTOR apple_no_sigpipe(LIBUS_SOCKET_DESCRIPTOR fd) { LIBUS_SOCKET_DESCRIPTOR bsd_set_nonblocking(LIBUS_SOCKET_DESCRIPTOR fd) { #ifdef _WIN32 /* Libuv will set windows sockets as non-blocking */ +#elif defined(__APPLE__) + fcntl(fd, F_SETFL, fcntl(fd, F_GETFL, 0) | O_NONBLOCK | O_CLOEXEC); #else fcntl(fd, F_SETFL, fcntl(fd, F_GETFL, 0) | O_NONBLOCK); #endif diff --git a/src/bun.js/bindings/c-bindings.cpp b/src/bun.js/bindings/c-bindings.cpp index 1576fa4e20..91f646e7cc 100644 --- a/src/bun.js/bindings/c-bindings.cpp +++ b/src/bun.js/bindings/c-bindings.cpp @@ -160,3 +160,27 @@ extern "C" int clock_gettime_monotonic(int64_t* tv_sec, int64_t* tv_nsec) return 0; } #endif + +#if OS(LINUX) + +#include + +// close_range is glibc > 2.33, which is very new +static ssize_t bun_close_range(unsigned int start, unsigned int end, unsigned int flags) +{ + return syscall(__NR_close_range, start, end, flags); +} + +extern "C" void on_before_reload_process_linux() +{ + // close all file descriptors except stdin, stdout, stderr and possibly IPC. + // if you're passing additional file descriptors to Bun, you're probably not passing more than 8. + bun_close_range(8, ~0U, 0U); + + // reset all signals to default + sigset_t signal_set; + sigfillset(&signal_set); + sigprocmask(SIG_SETMASK, &signal_set, nullptr); +} + +#endif \ No newline at end of file diff --git a/src/bun.zig b/src/bun.zig index 64eb4c5b61..d819465324 100644 --- a/src/bun.zig +++ b/src/bun.zig @@ -1437,7 +1437,10 @@ pub fn reloadProcess( actions.inherit(posix.STDIN_FD) catch unreachable; actions.inherit(posix.STDOUT_FD) catch unreachable; actions.inherit(posix.STDERR_FD) catch unreachable; + var attrs = PosixSpawn.Attr.init() catch unreachable; + attrs.resetSignals() catch {}; + attrs.set( C.POSIX_SPAWN_CLOEXEC_DEFAULT | // Apple Extension: If this bit is set, rather @@ -1453,13 +1456,22 @@ pub fn reloadProcess( }, .result => |_| {}, } - } else { + } else if (comptime Environment.isPosix) { + const on_before_reload_process_linux = struct { + pub extern "C" fn on_before_reload_process_linux() void; + }.on_before_reload_process_linux; + + on_before_reload_process_linux(); const err = std.os.execveZ( exec_path, newargv, envp, ); Output.panic("Unexpected error while reloading: {s}", .{@errorName(err)}); + } else if (comptime Environment.isWindows) { + @panic("TODO on Windows!"); + } else { + @panic("Unsupported platform"); } } pub var auto_reload_on_crash = false; From 4989ef88b8ce7bc530a829458a7af9cd0d5e4f56 Mon Sep 17 00:00:00 2001 From: dave caruso Date: Mon, 29 Jan 2024 15:49:06 -0800 Subject: [PATCH 10/45] docs: mark PerformanceEntry and friends as implemented (#8466) * docs: mark PerformanceEntry and friends as implemented * Update docs/runtime/nodejs-apis.md --------- Co-authored-by: Jarred Sumner --- docs/runtime/nodejs-apis.md | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/docs/runtime/nodejs-apis.md b/docs/runtime/nodejs-apis.md index f11a9d3c19..ef3b8271a0 100644 --- a/docs/runtime/nodejs-apis.md +++ b/docs/runtime/nodejs-apis.md @@ -312,23 +312,23 @@ The table below lists all globals implemented by Node.js and Bun's current compa ### [`PerformanceEntry`](https://developer.mozilla.org/en-US/docs/Web/API/PerformanceEntry) -🔴 Not implemented. +🟢 Fully implemented. ### [`PerformanceMark`](https://developer.mozilla.org/en-US/docs/Web/API/PerformanceMark) -🔴 Not implemented. +🟢 Fully implemented. ### [`PerformanceMeasure`](https://developer.mozilla.org/en-US/docs/Web/API/PerformanceMeasure) -🔴 Not implemented. +🟢 Fully implemented. ### [`PerformanceObserver`](https://developer.mozilla.org/en-US/docs/Web/API/PerformanceObserver) -🔴 Not implemented. +🟢 Fully implemented. ### [`PerformanceObserverEntryList`](https://developer.mozilla.org/en-US/docs/Web/API/PerformanceObserverEntryList) -🔴 Not implemented. +🟢 Fully implemented. ### [`PerformanceResourceTiming`](https://developer.mozilla.org/en-US/docs/Web/API/PerformanceResourceTiming) @@ -356,11 +356,11 @@ The table below lists all globals implemented by Node.js and Bun's current compa ### [`ReadableStreamBYOBReader`](https://developer.mozilla.org/en-US/docs/Web/API/ReadableStreamBYOBReader) -🔴 Not implemented. +🟢 Fully implemented. ### [`ReadableStreamBYOBRequest`](https://developer.mozilla.org/en-US/docs/Web/API/ReadableStreamBYOBRequest) -🔴 Not implemented. +🟢 Fully implemented. ### [`ReadableStreamDefaultController`](https://developer.mozilla.org/en-US/docs/Web/API/ReadableStreamDefaultController) From 2eede4f43510a1b49c34af56c5b22281e257a021 Mon Sep 17 00:00:00 2001 From: Jarred Sumner Date: Mon, 29 Jan 2024 17:52:08 -0800 Subject: [PATCH 11/45] Fixes #8555 (#8566) * Fixes #8555 * Make this closer to what npm does --------- Co-authored-by: Jarred Sumner <709451+Jarred-Sumner@users.noreply.github.com> --- src/install/install.zig | 36 +++++++++++++++++++++++++++++++++--- src/install/semver.zig | 28 +++++++++++++++++++--------- 2 files changed, 52 insertions(+), 12 deletions(-) diff --git a/src/install/install.zig b/src/install/install.zig index 2a07a5a6d8..0aba136aee 100644 --- a/src/install/install.zig +++ b/src/install/install.zig @@ -1061,10 +1061,40 @@ pub const PackageInstall = struct { var package_json_checker = json_parser.PackageJSONVersionChecker.init(allocator, &source, &log) catch return false; _ = package_json_checker.parseExpr() catch return false; if (!package_json_checker.has_found_name or !package_json_checker.has_found_version or log.errors > 0) return false; + const found_version = package_json_checker.found_version; + // Check if the version matches + if (!strings.eql(found_version, this.package_version)) { + const offset = brk: { + // ASCII only. + for (0..found_version.len) |c| { + switch (found_version[c]) { + // newlines & whitespace + ' ', + '\t', + '\n', + '\r', + std.ascii.control_code.vt, + std.ascii.control_code.ff, - // Version is more likely to not match than name, so we check it first. - return strings.eql(package_json_checker.found_version, this.package_version) and - strings.eql(package_json_checker.found_name, this.package_name); + // version separators + 'v', + '=', + => {}, + else => { + break :brk c; + }, + } + } + // If we didn't find any of these characters, there's no point in checking the version again. + // it will never match. + return false; + }; + + if (!strings.eql(found_version[offset..], this.package_version)) return false; + } + + // lastly, check the name. + return strings.eql(package_json_checker.found_name, this.package_name); } pub const Result = union(Tag) { diff --git a/src/install/semver.zig b/src/install/semver.zig index b1fafd5f47..2942c384ca 100644 --- a/src/install/semver.zig +++ b/src/install/semver.zig @@ -1043,17 +1043,27 @@ pub const Version = extern struct { var i: usize = 0; - i += strings.lengthOfLeadingWhitespaceASCII(input[i..]); - if (i == input.len) { - result.valid = false; - return result; + for (0..input.len) |c| { + switch (input[c]) { + // newlines & whitespace + ' ', + '\t', + '\n', + '\r', + std.ascii.control_code.vt, + std.ascii.control_code.ff, + + // version separators + 'v', + '=', + => {}, + else => { + i = c; + break; + }, + } } - if (input[i] == 'v' or input[i] == '=') { - i += 1; - } - - i += strings.lengthOfLeadingWhitespaceASCII(input[i..]); if (i == input.len) { result.valid = false; return result; From cf1c849e4a2dbf0280247f9fc66c727a5f1b9530 Mon Sep 17 00:00:00 2001 From: dave caruso Date: Mon, 29 Jan 2024 20:03:02 -0800 Subject: [PATCH 12/45] fix(windows): more reliable extracting (#8567) * finally * a * Update src/install/extract_tarball.zig Co-authored-by: Jarred Sumner * fix compilation --------- Co-authored-by: Jarred Sumner --- docs/project/building-windows.md | 1 + scripts/make-old-js.ps1 | 64 ++++++------- src/cli/create_command.zig | 30 ++++-- src/feature_flags.zig | 16 ---- src/install/extract_tarball.zig | 158 +++++++++++++++++++++++++------ src/install/install.zig | 18 ++-- src/libarchive/libarchive.zig | 79 ++++++++++------ src/windows.zig | 2 +- src/windows_c.zig | 7 +- 9 files changed, 248 insertions(+), 127 deletions(-) diff --git a/docs/project/building-windows.md b/docs/project/building-windows.md index 9de2eae8eb..9fe9208bb0 100644 --- a/docs/project/building-windows.md +++ b/docs/project/building-windows.md @@ -114,6 +114,7 @@ bun install # or npm install .\scripts\env.ps1 .\scripts\update-submodules.ps1 # this syncs git submodule state +.\scripts\make-old-js.ps1 # runs some old code generators .\scripts\all-dependencies.ps1 # this builds all dependencies cd build # this was created by the codegen.ps1 script earlier diff --git a/scripts/make-old-js.ps1 b/scripts/make-old-js.ps1 index f51122aa41..42cdf20c85 100644 --- a/scripts/make-old-js.ps1 +++ b/scripts/make-old-js.ps1 @@ -1,32 +1,32 @@ -$npm_client = "npm" - -# & ${npm_client} i - -$root = Join-Path (Split-Path -Path $MyInvocation.MyCommand.Definition -Parent) "..\" -$esbuild = Join-Path $root "node_modules\.bin\esbuild.cmd" - -$env:NODE_ENV = "production" - -# runtime.js -echo $esbuild -& ${esbuild} ` - "--target=esnext" "--bundle" ` - "src/runtime.bun.js" ` - "--format=esm" "--platform=node" "--minify" "--external:/bun:*" ` - "--outfile=src/runtime.out.js" -if ($LASTEXITCODE -ne 0) { throw "esbuild failed with exit code $LASTEXITCODE" } - -# fallback_decoder -& ${esbuild} --target=esnext --bundle src/fallback.ts --format=iife --platform=browser --minify > src/fallback.out.js - -# bun-error -Push-Location packages\bun-error -& ${npm_client} install -& ${npm_client} run build -Pop-Location - -# node-fallbacks -Push-Location src\node-fallbacks -& ${npm_client} install -& ${esbuild} --bundle @(Get-Item .\*.js) --outdir=out --format=esm --minify --platform=browser -Pop-Location +$npm_client = "npm" + +# & ${npm_client} i + +$root = Join-Path (Split-Path -Path $MyInvocation.MyCommand.Definition -Parent) "..\" +$esbuild = Join-Path $root "node_modules\.bin\esbuild.cmd" + +$env:NODE_ENV = "production" + +# runtime.js +echo $esbuild +& ${esbuild} ` + "--target=esnext" "--bundle" ` + "src/runtime.bun.js" ` + "--format=esm" "--platform=node" "--minify" "--external:/bun:*" ` + "--outfile=src/runtime.out.js" +if ($LASTEXITCODE -ne 0) { throw "esbuild failed with exit code $LASTEXITCODE" } + +# fallback_decoder +& ${esbuild} --target=esnext --bundle src/fallback.ts --format=iife --platform=browser --minify > src/fallback.out.js + +# bun-error +Push-Location packages\bun-error +& ${npm_client} install +& ${npm_client} run build +Pop-Location + +# node-fallbacks +Push-Location src\node-fallbacks +& ${npm_client} install +& ${esbuild} --bundle @(Get-Item .\*.js) --outdir=out --format=esm --minify --platform=browser +Pop-Location diff --git a/src/cli/create_command.zig b/src/cli/create_command.zig index 99cfcbba52..f51ea93f73 100644 --- a/src/cli/create_command.zig +++ b/src/cli/create_command.zig @@ -2255,18 +2255,30 @@ const GitHandler = struct { ) !bool { const git_start = std.time.nanoTimestamp(); - // This feature flag is disabled. - // using libgit2 is slower than the CLI. - // [481.00ms] git - // [89.00ms] git - // if (comptime FeatureFlags.use_libgit2) { - // } + // Not sure why... + // But using libgit for this operation is slower than the CLI! + // Used to have a feature flag to try it but was removed: + // https://github.com/oven-sh/bun/commit/deafd3d0d42fb8d7ddf2b06cde2d7c7ee8bc7144 + // + // ~/Build/throw + // ❯ hyperfine "bun create react3 app --force --no-install" --prepare="rm -rf app" + // Benchmark #1: bun create react3 app --force --no-install + // Time (mean ± σ): 974.6 ms ± 6.8 ms [User: 170.5 ms, System: 798.3 ms] + // Range (min … max): 960.8 ms … 984.6 ms 10 runs + // + // ❯ mv /usr/local/opt/libgit2/lib/libgit2.dylib /usr/local/opt/libgit2/lib/libgit2.dylib.1 + // + // ~/Build/throw + // ❯ hyperfine "bun create react3 app --force --no-install" --prepare="rm -rf app" + // Benchmark #1: bun create react3 app --force --no-install + // Time (mean ± σ): 306.7 ms ± 6.1 ms [User: 31.7 ms, System: 269.8 ms] + // Range (min … max): 299.5 ms … 318.8 ms 10 runs if (which(&bun_path_buf, PATH, destination, "git")) |git| { const git_commands = .{ - &[_]string{ bun.asByteSlice(git), "init", "--quiet" }, - &[_]string{ bun.asByteSlice(git), "add", destination, "--ignore-errors" }, - &[_]string{ bun.asByteSlice(git), "commit", "-am", "Initial commit (via bun create)", "--quiet" }, + &[_]string{ git, "init", "--quiet" }, + &[_]string{ git, "add", destination, "--ignore-errors" }, + &[_]string{ git, "commit", "-am", "Initial commit (via bun create)", "--quiet" }, }; if (comptime verbose) { diff --git a/src/feature_flags.zig b/src/feature_flags.zig index d42ae89f68..c458909b52 100644 --- a/src/feature_flags.zig +++ b/src/feature_flags.zig @@ -69,22 +69,6 @@ pub const verbose_analytics = false; pub const disable_compression_in_http_client = false; pub const enable_keepalive = true; -// Not sure why... -// But this is slower! -// ~/Build/throw -// ❯ hyperfine "bun create react3 app --force --no-install" --prepare="rm -rf app" -// Benchmark #1: bun create react3 app --force --no-install -// Time (mean ± σ): 974.6 ms ± 6.8 ms [User: 170.5 ms, System: 798.3 ms] -// Range (min … max): 960.8 ms … 984.6 ms 10 runs - -// ❯ mv /usr/local/opt/libgit2/lib/libgit2.dylib /usr/local/opt/libgit2/lib/libgit2.dylib.1 - -// ~/Build/throw -// ❯ hyperfine "bun create react3 app --force --no-install" --prepare="rm -rf app" -// Benchmark #1: bun create react3 app --force --no-install -// Time (mean ± σ): 306.7 ms ± 6.1 ms [User: 31.7 ms, System: 269.8 ms] -// Range (min … max): 299.5 ms … 318.8 ms 10 runs -pub const use_libgit2 = true; pub const atomic_file_watcher = env.isLinux; diff --git a/src/install/extract_tarball.zig b/src/install/extract_tarball.zig index 8f1931ebab..4c1bd8969e 100644 --- a/src/install/extract_tarball.zig +++ b/src/install/extract_tarball.zig @@ -17,6 +17,7 @@ const string = @import("../string_types.zig").string; const strings = @import("../string_immutable.zig"); const Path = @import("../resolver/resolve_path.zig"); const Environment = bun.Environment; +const w = std.os.windows; const ExtractTarball = @This(); @@ -157,7 +158,7 @@ threadlocal var folder_name_buf: bun.PathBuffer = undefined; threadlocal var json_path_buf: bun.PathBuffer = undefined; fn extract(this: *const ExtractTarball, tgz_bytes: []const u8) !Install.ExtractData { - var tmpdir = this.temp_dir; + const tmpdir = this.temp_dir; var tmpname_buf: [bun.MAX_PATH_BYTES]u8 = undefined; const name = this.name.slice(); const basename = brk: { @@ -183,8 +184,21 @@ fn extract(this: *const ExtractTarball, tgz_bytes: []const u8) !Install.ExtractD var resolved: string = ""; const tmpname = try FileSystem.instance.tmpname(basename[0..@min(basename.len, 32)], &tmpname_buf, tgz_bytes.len); - { - var extract_destination = tmpdir.makeOpenPath(std.mem.span(tmpname), .{}) catch |err| { + const extract_fd_on_windows = brk: { + var extract_destination = switch (Environment.os) { + .windows => makeOpenPathAccessMaskW( + tmpdir, + std.mem.span(tmpname), + w.STANDARD_RIGHTS_READ | + w.FILE_READ_ATTRIBUTES | + w.FILE_READ_EA | + w.SYNCHRONIZE | + w.FILE_TRAVERSE | + w.DELETE, + false, + ), + else => tmpdir.makeOpenPath(std.mem.span(tmpname), .{}), + } catch |err| { this.package_manager.log.addErrorFmt( null, logger.Loc.Empty, @@ -195,7 +209,8 @@ fn extract(this: *const ExtractTarball, tgz_bytes: []const u8) !Install.ExtractD return error.InstallFailed; }; - defer extract_destination.close(); + errdefer if (Environment.isWindows) extract_destination.close(); + defer if (!Environment.isWindows) extract_destination.close(); if (PackageManager.verbose_install) { Output.prettyErrorln("[{s}] Start extracting {s}", .{ name, tmpname }); @@ -276,7 +291,11 @@ fn extract(this: *const ExtractTarball, tgz_bytes: []const u8) !Install.ExtractD Output.prettyErrorln("[{s}] Extracted", .{name}); Output.flush(); } - } + + if (Environment.isWindows) { + break :brk bun.toFD(extract_destination.fd); + } + }; const folder_name = switch (this.resolution.tag) { .npm => this.package_manager.cachedNPMPackageFolderNamePrint(&folder_name_buf, name, this.resolution.value.npm.version), .github => PackageManager.cachedGitHubFolderNamePrint(&folder_name_buf, resolved), @@ -295,33 +314,23 @@ fn extract(this: *const ExtractTarball, tgz_bytes: []const u8) !Install.ExtractD // Now that we've extracted the archive, we rename. if (comptime Environment.isWindows) { - // TODO(dylan-conway) make this less painful - var from_buf: bun.PathBuffer = undefined; - const tmpdir_path = try bun.getFdPath(tmpdir.fd, &from_buf); - const from_path = Path.joinAbsStringZ(tmpdir_path, &.{bun.sliceTo(tmpname, 0)}, .auto); + defer _ = bun.sys.close(extract_fd_on_windows); - var to_buf: bun.PathBuffer = undefined; - const cache_dir_path = try bun.getFdPath(cache_dir.fd, &to_buf); - const to_path = Path.joinAbsStringBufZ(cache_dir_path, &to_buf, &.{folder_name}, .auto); + var folder_name_wbuf: bun.WPathBuffer = undefined; + const folder_name_w = bun.strings.toWPathNormalized(&folder_name_wbuf, folder_name); - var from_path_buf_w: bun.WPathBuffer = undefined; - const from_path_w = bun.strings.toWPath(&from_path_buf_w, from_path); - var to_path_buf_w: bun.WPathBuffer = undefined; - const to_path_w = bun.strings.toWPath(&to_path_buf_w, to_path); - - if (bun.windows.MoveFileExW( - from_path_w, - to_path_w, - bun.windows.MOVEFILE_COPY_ALLOWED | bun.windows.MOVEFILE_REPLACE_EXISTING | bun.windows.MOVEFILE_WRITE_THROUGH, - ) == bun.windows.FALSE) { - this.package_manager.log.addErrorFmt( - null, - logger.Loc.Empty, - this.package_manager.allocator, - "moving \"{s}\" to cache dir failed: From: {s}\n To: {s}", - .{ name, tmpname, folder_name }, - ) catch unreachable; - return error.InstallFailed; + switch (bun.C.moveOpenedFileAtLoose(extract_fd_on_windows, bun.toFD(cache_dir.fd), folder_name_w, false)) { + .err => |err| { + this.package_manager.log.addErrorFmt( + null, + logger.Loc.Empty, + this.package_manager.allocator, + "moving \"{s}\" to cache dir failed: {}\n From: {s}\n To: {}", + .{ name, err, tmpname, std.unicode.fmtUtf16le(folder_name_w) }, + ) catch unreachable; + return error.InstallFailed; + }, + .result => {}, } } else { switch (bun.sys.renameat(bun.toFD(tmpdir.fd), bun.sliceTo(tmpname, 0), bun.toFD(cache_dir.fd), folder_name)) { @@ -432,3 +441,92 @@ fn extract(this: *const ExtractTarball, tgz_bytes: []const u8) !Install.ExtractD .json_len = json_len, }; } + +// TODO(@paperdave): upstream making this public into zig std +// there is zero reason this must be copied +// +/// Calls makeOpenDirAccessMaskW iteratively to make an entire path +/// (i.e. creating any parent directories that do not exist). +/// Opens the dir if the path already exists and is a directory. +/// This function is not atomic, and if it returns an error, the file system may +/// have been modified regardless. +fn makeOpenPathAccessMaskW(self: std.fs.Dir, sub_path: []const u8, access_mask: u32, no_follow: bool) std.os.OpenError!std.fs.Dir { + var it = try std.fs.path.componentIterator(sub_path); + // If there are no components in the path, then create a dummy component with the full path. + var component = it.last() orelse std.fs.path.NativeUtf8ComponentIterator.Component{ + .name = "", + .path = sub_path, + }; + + while (true) { + const sub_path_w = try w.sliceToPrefixedFileW(self.fd, component.path); + const is_last = it.peekNext() == null; + var result = makeOpenDirAccessMaskW(self, sub_path_w.span().ptr, access_mask, .{ + .no_follow = no_follow, + .create_disposition = if (is_last) w.FILE_OPEN_IF else w.FILE_CREATE, + }) catch |err| switch (err) { + error.FileNotFound => |e| { + component = it.previous() orelse return e; + continue; + }, + else => |e| return e, + }; + + component = it.next() orelse return result; + // Don't leak the intermediate file handles + result.close(); + } +} +const MakeOpenDirAccessMaskWOptions = struct { + no_follow: bool, + create_disposition: u32, +}; + +fn makeOpenDirAccessMaskW(self: std.fs.Dir, sub_path_w: [*:0]const u16, access_mask: u32, flags: MakeOpenDirAccessMaskWOptions) std.os.OpenError!std.fs.Dir { + var result = std.fs.Dir{ + .fd = undefined, + }; + + const path_len_bytes = @as(u16, @intCast(std.mem.sliceTo(sub_path_w, 0).len * 2)); + var nt_name = w.UNICODE_STRING{ + .Length = path_len_bytes, + .MaximumLength = path_len_bytes, + .Buffer = @constCast(sub_path_w), + }; + var attr = w.OBJECT_ATTRIBUTES{ + .Length = @sizeOf(w.OBJECT_ATTRIBUTES), + .RootDirectory = if (std.fs.path.isAbsoluteWindowsW(sub_path_w)) null else self.fd, + .Attributes = 0, // Note we do not use OBJ_CASE_INSENSITIVE here. + .ObjectName = &nt_name, + .SecurityDescriptor = null, + .SecurityQualityOfService = null, + }; + const open_reparse_point: w.DWORD = if (flags.no_follow) w.FILE_OPEN_REPARSE_POINT else 0x0; + var io: w.IO_STATUS_BLOCK = undefined; + const rc = w.ntdll.NtCreateFile( + &result.fd, + access_mask, + &attr, + &io, + null, + w.FILE_ATTRIBUTE_NORMAL, + w.FILE_SHARE_READ | w.FILE_SHARE_WRITE, + flags.create_disposition, + w.FILE_DIRECTORY_FILE | w.FILE_SYNCHRONOUS_IO_NONALERT | w.FILE_OPEN_FOR_BACKUP_INTENT | open_reparse_point, + null, + 0, + ); + + switch (rc) { + .SUCCESS => return result, + .OBJECT_NAME_INVALID => return error.BadPathName, + .OBJECT_NAME_NOT_FOUND => return error.FileNotFound, + .OBJECT_PATH_NOT_FOUND => return error.FileNotFound, + .NOT_A_DIRECTORY => return error.NotDir, + // This can happen if the directory has 'List folder contents' permission set to 'Deny' + // and the directory is trying to be opened for iteration. + .ACCESS_DENIED => return error.AccessDenied, + .INVALID_PARAMETER => return error.BadPathName, + else => return w.unexpectedStatus(rc), + } +} diff --git a/src/install/install.zig b/src/install/install.zig index 0aba136aee..e9d0292797 100644 --- a/src/install/install.zig +++ b/src/install/install.zig @@ -692,7 +692,9 @@ const Task = struct { ) catch |err| { if (comptime Environment.isDebug) { if (@errorReturnTrace()) |trace| { - std.debug.dumpStackTrace(trace.*); + _ = trace; // autofix + + // std.debug.dumpStackTrace(trace.*); } } @@ -5086,7 +5088,7 @@ pub const PackageManager = struct { }, ); } else if (comptime log_level != .silent) { - const fmt = "error: {s} extracting tarball for {s}"; + const fmt = "error: {s} extracting tarball for {s}\n"; const args = .{ @errorName(err), alias, @@ -9362,14 +9364,11 @@ pub const PackageManager = struct { } } - switch (Output.enable_ansi_colors) { - inline else => |enable_ansi_colors| { - try manager.log.printForLogLevelWithEnableAnsiColors(Output.errorWriter(), enable_ansi_colors); - }, - } - + try manager.log.printForLogLevel(Output.errorWriter()); if (manager.log.hasErrors()) Global.crash(); + manager.log.reset(); + // This operation doesn't perform any I/O, so it should be relatively cheap. manager.lockfile = try manager.lockfile.cleanWithLogger( manager.package_json_updates, @@ -9506,6 +9505,9 @@ pub const PackageManager = struct { ); } + try manager.log.printForLogLevel(Output.errorWriter()); + if (manager.log.hasErrors()) Global.crash(); + if (needs_new_lockfile) { manager.summary.add = @as(u32, @truncate(manager.lockfile.packages.len)); } diff --git a/src/libarchive/libarchive.zig b/src/libarchive/libarchive.zig index 9226c817e5..7f3bfeccee 100644 --- a/src/libarchive/libarchive.zig +++ b/src/libarchive/libarchive.zig @@ -471,7 +471,7 @@ pub const Archive = struct { pub fn extractToDir( file_buffer: []const u8, - dir_: std.fs.Dir, + dir: std.fs.Dir, ctx: ?*Archive.Context, comptime ContextType: type, appender: ContextType, @@ -487,10 +487,8 @@ pub const Archive = struct { _ = stream.openRead(); const archive = stream.archive; var count: u32 = 0; - const dir = dir_; const dir_fd = dir.fd; - const loop = if (Environment.isWindows) bun.Async.Loop.get() else {}; var w_path: if (Environment.isWindows) bun.WPathBuffer else void = undefined; loop: while (true) { @@ -564,7 +562,7 @@ pub const Archive = struct { Kind.sym_link => { const link_target = lib.archive_entry_symlink(entry).?; if (comptime Environment.isWindows) { - @panic("TODO on Windows"); + @panic("TODO on Windows: Extracting archives containing symbolic links."); } std.os.symlinkatZ(link_target, dir_fd, pathname) catch |err| brk: { switch (err) { @@ -580,36 +578,57 @@ pub const Archive = struct { }, Kind.file => { const mode: bun.Mode = if (comptime Environment.isWindows) 0 else @intCast(lib.archive_entry_perm(entry)); - const os_path = if (Environment.isWindows) bun.strings.toWPathNormalized(&w_path, pathname) else pathname; - const createFileOS = if (Environment.isWindows) std.fs.Dir.createFileW else std.fs.Dir.createFileZ; - const file = createFileOS(dir, os_path, .{ .truncate = true, .mode = mode }) catch |err| brk: { - switch (err) { - error.AccessDenied, error.FileNotFound => { - dir.makePath(std.fs.path.dirname(slice) orelse return err) catch {}; - break :brk try createFileOS(dir, os_path, .{ - .truncate = true, - .mode = mode, - }); - }, - else => { - return err; - }, + + const file_handle_native = brk: { + if (Environment.isWindows) { + const flags = std.os.O.WRONLY | std.os.O.CREAT | std.os.O.TRUNC; + const os_path = bun.strings.toWPathNormalized(&w_path, slice); + switch (bun.sys.openatWindows(bun.toFD(dir_fd), os_path, flags)) { + .result => |fd| break :brk fd, + .err => |e| switch (e.errno) { + @intFromEnum(bun.C.E.PERM), @intFromEnum(bun.C.E.NOENT) => { + dir.makePath(std.fs.path.dirname(slice) orelse return bun.errnoToZigErr(e.errno)) catch {}; + break :brk try bun.sys.openatWindows(bun.toFD(dir_fd), os_path, flags).unwrap(); + }, + else => { + return bun.errnoToZigErr(e.errno); + }, + }, + } + } else { + break :brk (dir.createFileZ(pathname, .{ .truncate = true, .mode = mode }) catch |err| { + switch (err) { + error.AccessDenied, error.FileNotFound => { + dir.makePath(std.fs.path.dirname(slice) orelse return err) catch {}; + break :brk (try dir.createFileZ(pathname, .{ + .truncate = true, + .mode = mode, + })).handle; + }, + else => { + return err; + }, + } + }).handle; } }; - const file_handle = bun.toLibUVOwnedFD(file.handle); + const file_handle = bun.toLibUVOwnedFD(file_handle_native); defer if (comptime close_handles) { - if (Environment.isWindows) { - // Using Async.Closer defers closing the file to a different thread. - // On windows, AV hangs these closes really badly. - // - // 'bun i @mui/icons-material' takes like 20 seconds to extract - // - // The install still takes a long time but this makes it a little bit better. - bun.Async.Closer.close(bun.uvfdcast(file_handle), loop); - } else { - _ = bun.sys.close(file_handle); - } + // On windows, AV hangs these closes really badly. + // 'bun i @mui/icons-material' takes like 20 seconds to extract + // mostly spend on waiting for things to close closing + // + // Using Async.Closer defers closing the file to a different thread, + // which can make the NtSetInformationFile call fail. + // + // Using async closing doesnt actually improve end user performance + // probably because our process is still waiting on AV to do it's thing. + // + // But this approach does not actually solve the problem, it just + // defers the close to a different thread. And since we are already + // on a worker thread, that doesn't help us. + _ = bun.sys.close(file_handle); }; const entry_size = @max(lib.archive_entry_size(entry), 0); diff --git a/src/windows.zig b/src/windows.zig index c74afad7c0..28fa600c4b 100644 --- a/src/windows.zig +++ b/src/windows.zig @@ -2938,7 +2938,7 @@ pub const Win32Error = enum(u16) { } pub fn toSystemErrno(this: Win32Error) ?SystemErrno { - return SystemErrno.init(this); + return SystemErrno.init(@intFromEnum(translateWinErrorToErrno(@enumFromInt(@intFromEnum(this))))); } pub fn fromNTStatus(status: win32.NTSTATUS) Win32Error { diff --git a/src/windows_c.zig b/src/windows_c.zig index 964451f719..dbac3b63bc 100644 --- a/src/windows_c.zig +++ b/src/windows_c.zig @@ -704,7 +704,7 @@ pub const SystemErrno = enum(u16) { if (comptime @TypeOf(code) == Win32Error) { return switch (code) { Win32Error.NOACCESS => SystemErrno.EACCES, - @as(Win32Error, @enumFromInt(10013)) => SystemErrno.EACCES, + Win32Error.WSAEACCES => SystemErrno.EACCES, Win32Error.ELEVATION_REQUIRED => SystemErrno.EACCES, Win32Error.CANT_ACCESS_FILE => SystemErrno.EACCES, Win32Error.ADDRESS_ALREADY_ASSOCIATED => SystemErrno.EADDRINUSE, @@ -1315,6 +1315,11 @@ pub fn moveOpenedFileAt( // and therefore having different behavior when the Windows version is >= rs1 but < rs5. comptime std.debug.assert(builtin.target.os.version_range.windows.min.isAtLeast(.win10_rs5)); + if (bun.Environment.allow_assert) { + std.debug.assert(std.mem.indexOfScalar(u16, new_file_name, '\\') == null); // Call moveOpenedFileAtLoose + std.debug.assert(std.mem.indexOfScalar(u16, new_file_name, '/') == null); // Call moveOpenedFileAtLoose + } + const struct_buf_len = @sizeOf(w.FILE_RENAME_INFORMATION_EX) + (bun.MAX_PATH_BYTES - 1); var rename_info_buf: [struct_buf_len]u8 align(@alignOf(w.FILE_RENAME_INFORMATION_EX)) = undefined; From 692fb220a404fbe56415f81379a830c2bae63b6d Mon Sep 17 00:00:00 2001 From: vinnichase Date: Tue, 30 Jan 2024 07:38:43 +0100 Subject: [PATCH 13/45] Update launch.json to use `${workspaceFolder}/build/bun-debug` (#8570) --- .vscode/launch.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.vscode/launch.json b/.vscode/launch.json index de9075c40c..9100299202 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -155,7 +155,7 @@ "type": "lldb", "request": "launch", "name": "bun run [file]", - "program": "bun-debug", + "program": "${workspaceFolder}/build/bun-debug", "args": ["run", "${file}"], "cwd": "${fileDirname}", "env": { From ca801a35fe2df15a16818ef1c364028c182d8876 Mon Sep 17 00:00:00 2001 From: Dylan Conway <35280289+dylan-conway@users.noreply.github.com> Date: Tue, 30 Jan 2024 06:49:59 -0800 Subject: [PATCH 14/45] fix(windows): fix win32 errors (#8569) * translate eexist * use SystemErrno translation * update --- src/bun.js/api/bun/subprocess.zig | 4 ++++ src/bun.js/node/node_fs.zig | 19 ++++++++++++++++--- src/bun.js/node/types.zig | 10 +++++++++- src/sys.zig | 29 ++++++++++++++--------------- src/windows.zig | 25 ++----------------------- src/windows_c.zig | 6 +++--- 6 files changed, 48 insertions(+), 45 deletions(-) diff --git a/src/bun.js/api/bun/subprocess.zig b/src/bun.js/api/bun/subprocess.zig index 3dd2291cf5..094c8102b3 100644 --- a/src/bun.js/api/bun/subprocess.zig +++ b/src/bun.js/api/bun/subprocess.zig @@ -731,6 +731,10 @@ pub const Subprocess = struct { if (comptime Environment.isWindows) { if (std.os.windows.kernel32.TerminateProcess(this.pid.process_handle, @intCast(sig)) == 0) { const err = bun.windows.getLastErrno(); + if (comptime Environment.allow_assert) { + std.debug.assert(err != .UNKNOWN); + } + // if the process was already killed don't throw // // "After a process has terminated, call to TerminateProcess with open diff --git a/src/bun.js/node/node_fs.zig b/src/bun.js/node/node_fs.zig index 280f49078f..6ae05bd220 100644 --- a/src/bun.js/node/node_fs.zig +++ b/src/bun.js/node/node_fs.zig @@ -5990,7 +5990,16 @@ pub const NodeFS = struct { } const flags = os.O.DIRECTORY | os.O.RDONLY; - const fd = switch (Syscall.openatOSPath(bun.toFD((std.fs.cwd().fd)), src, flags, 0)) { + var wbuf: if (Environment.isWindows) bun.WPathBuffer else void = undefined; + const fd = switch (Syscall.openatOSPath( + bun.toFD((std.fs.cwd().fd)), + if (Environment.isWindows and std.fs.path.isAbsoluteWindowsWTF16(src)) + bun.strings.addNTPathPrefixIfNeeded(&wbuf, src) + else + src, + flags, + 0, + )) { .err => |err| { return .{ .err = err.withPath(this.osPathIntoSyncErrorBuf(src)) }; }, @@ -6327,9 +6336,13 @@ pub const NodeFS = struct { if (Environment.isWindows) { const result = windows.CopyFileW(src, dest, @intFromBool(mode.shouldntOverwrite())); - if (Maybe(Return.CopyFile).errnoSysP(result, .copyfile, this.osPathIntoSyncErrorBuf(src))) |e| { - return e; + if (result == bun.windows.FALSE) { + if (Maybe(Return.CopyFile).errnoSysP(result, .copyfile, this.osPathIntoSyncErrorBuf(src))) |e| { + return e; + } } + + return ret.success; } return ret.todo(); diff --git a/src/bun.js/node/types.zig b/src/bun.js/node/types.zig index e829fc396d..07336d146d 100644 --- a/src/bun.js/node/types.zig +++ b/src/bun.js/node/types.zig @@ -141,6 +141,9 @@ pub fn Maybe(comptime ResultType: type) type { } pub inline fn errnoSys(rc: anytype, syscall: Syscall.Tag) ?@This() { + if (comptime Environment.isWindows) { + if (rc != 0) return null; + } return switch (Syscall.getErrno(rc)) { .SUCCESS => null, else => |err| @This(){ @@ -164,6 +167,9 @@ pub fn Maybe(comptime ResultType: type) type { } pub inline fn errnoSysFd(rc: anytype, syscall: Syscall.Tag, fd: bun.FileDescriptor) ?@This() { + if (comptime Environment.isWindows) { + if (rc != 0) return null; + } return switch (Syscall.getErrno(rc)) { .SUCCESS => null, else => |err| @This(){ @@ -181,6 +187,9 @@ pub fn Maybe(comptime ResultType: type) type { if (std.meta.Child(@TypeOf(path)) == u16) { @compileError("Do not pass WString path to errnoSysP, it needs the path encoded as utf8"); } + if (comptime Environment.isWindows) { + if (rc != 0) return null; + } return switch (Syscall.getErrno(rc)) { .SUCCESS => null, else => |err| @This(){ @@ -198,7 +207,6 @@ pub fn Maybe(comptime ResultType: type) type { fn translateToErrInt(err: anytype) bun.sys.Error.Int { return switch (@TypeOf(err)) { - bun.windows.Win32Error => @intFromEnum(bun.windows.translateWinErrorToErrno(err)), bun.windows.NTSTATUS => @intFromEnum(bun.windows.translateNTStatusToErrno(err)), else => @truncate(@intFromEnum(err)), }; diff --git a/src/sys.zig b/src/sys.zig index d96b738ea2..528909a172 100644 --- a/src/sys.zig +++ b/src/sys.zig @@ -271,11 +271,11 @@ pub fn mkdir(file_path: [:0]const u8, flags: bun.Mode) Maybe(void) { .windows => { var wbuf: bun.WPathBuffer = undefined; - const rc = kernel32.CreateDirectoryW(bun.strings.toWPath(&wbuf, file_path).ptr, null); - return if (rc != 0) - Maybe(void).success - else - Maybe(void).errnoSys(rc, .mkdir) orelse Maybe(void).success; + return Maybe(void).errnoSysP( + kernel32.CreateDirectoryW(bun.strings.toWPath(&wbuf, file_path).ptr, null), + .mkdir, + file_path, + ) orelse Maybe(void).success; }, else => @compileError("mkdir is not implemented on this platform"), @@ -303,11 +303,11 @@ pub fn mkdirA(file_path: []const u8, flags: bun.Mode) Maybe(void) { if (comptime Environment.isWindows) { var wbuf: bun.WPathBuffer = undefined; - const rc = kernel32.CreateDirectoryW(bun.strings.toWPath(&wbuf, file_path).ptr, null); - return if (rc != 0) - Maybe(void).success - else - Maybe(void).errnoSys(rc, .mkdir) orelse Maybe(void).success; + return Maybe(void).errnoSysP( + kernel32.CreateDirectoryW(bun.strings.toWPath(&wbuf, file_path).ptr, null), + .mkdir, + file_path, + ) orelse Maybe(void).success; } } @@ -315,11 +315,10 @@ pub fn mkdirOSPath(file_path: bun.OSPathSliceZ, flags: bun.Mode) Maybe(void) { return switch (Environment.os) { else => mkdir(file_path, flags), .windows => { - const rc = kernel32.CreateDirectoryW(file_path, null); - return if (rc != 0) - Maybe(void).success - else - Maybe(void).errnoSys(rc, .mkdir) orelse Maybe(void).success; + return Maybe(void).errnoSys( + kernel32.CreateDirectoryW(file_path, null), + .mkdir, + ) orelse Maybe(void).success; }, }; } diff --git a/src/windows.zig b/src/windows.zig index 28fa600c4b..054994619f 100644 --- a/src/windows.zig +++ b/src/windows.zig @@ -2938,7 +2938,7 @@ pub const Win32Error = enum(u16) { } pub fn toSystemErrno(this: Win32Error) ?SystemErrno { - return SystemErrno.init(@intFromEnum(translateWinErrorToErrno(@enumFromInt(@intFromEnum(this))))); + return SystemErrno.init(this); } pub fn fromNTStatus(status: win32.NTSTATUS) Win32Error { @@ -2985,28 +2985,7 @@ pub extern "kernel32" fn SetFileInformationByHandle( ) BOOL; pub fn getLastErrno() bun.C.E { - return translateWinErrorToErrno(bun.windows.kernel32.GetLastError()); -} - -pub fn translateWinErrorToErrno(err: win32.Win32Error) bun.C.E { - return switch (err) { - .SUCCESS => .SUCCESS, - .FILE_NOT_FOUND => .NOENT, - .PATH_NOT_FOUND => .NOENT, - .TOO_MANY_OPEN_FILES => .NOMEM, - .ACCESS_DENIED => .PERM, - .INVALID_HANDLE => .BADF, - .NOT_ENOUGH_MEMORY => .NOMEM, - .OUTOFMEMORY => .NOMEM, - .INVALID_PARAMETER => .INVAL, - - else => |t| { - // if (bun.Environment.isDebug) { - bun.Output.warn("Called translateWinErrorToErrno with {s} which does not have a mapping to errno.", .{@tagName(t)}); - // } - return .UNKNOWN; - }, - }; + return (bun.C.SystemErrno.init(bun.windows.kernel32.GetLastError()) orelse SystemErrno.EUNKNOWN).toE(); } pub fn translateNTStatusToErrno(err: win32.NTSTATUS) bun.C.E { diff --git a/src/windows_c.zig b/src/windows_c.zig index dbac3b63bc..6bb7c68361 100644 --- a/src/windows_c.zig +++ b/src/windows_c.zig @@ -701,8 +701,8 @@ pub const SystemErrno = enum(u16) { } } - if (comptime @TypeOf(code) == Win32Error) { - return switch (code) { + if (comptime @TypeOf(code) == Win32Error or @TypeOf(code) == std.os.windows.Win32Error) { + return switch (@as(Win32Error, @enumFromInt(@intFromEnum(code)))) { Win32Error.NOACCESS => SystemErrno.EACCES, Win32Error.WSAEACCES => SystemErrno.EACCES, Win32Error.ELEVATION_REQUIRED => SystemErrno.EACCES, @@ -803,7 +803,7 @@ pub const SystemErrno = enum(u16) { Win32Error.META_EXPANSION_TOO_LONG => SystemErrno.E2BIG, Win32Error.WSAESOCKTNOSUPPORT => SystemErrno.ESOCKTNOSUPPORT, Win32Error.DELETE_PENDING => SystemErrno.EBUSY, - else => return null, + else => null, }; } From 0aba51230c191601c522d2747ca1fbbd4477412d Mon Sep 17 00:00:00 2001 From: vinnichase Date: Tue, 30 Jan 2024 18:41:18 +0100 Subject: [PATCH 15/45] dx: create symlink on every execution (#8568) Symlink is not created on first setup. --- scripts/download-zig.sh | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/scripts/download-zig.sh b/scripts/download-zig.sh index cec996d47d..19860243a4 100755 --- a/scripts/download-zig.sh +++ b/scripts/download-zig.sh @@ -59,13 +59,11 @@ update_repo_if_needed() { done printf "Zig was updated to ${zig_version}. Please commit new files." - - # symlink extracted zig to extracted zig.exe - # TODO: Workaround for https://github.com/ziglang/vscode-zig/issues/164 - ln -sf "${extract_at}/zig" "${extract_at}/zig.exe" - chmod +x "${extract_at}/zig.exe" - fi + # symlink extracted zig to extracted zig.exe + # TODO: Workaround for https://github.com/ziglang/vscode-zig/issues/164 + ln -sf "${extract_at}/zig" "${extract_at}/zig.exe" + chmod +x "${extract_at}/zig.exe" } if [ -e "${extract_at}/.version" ]; then From 8fdb46cb913e16ccfd0a1a8b09b78761c9310e84 Mon Sep 17 00:00:00 2001 From: dave caruso Date: Tue, 30 Jan 2024 10:04:06 -0800 Subject: [PATCH 16/45] ok --- src/string_immutable.zig | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/string_immutable.zig b/src/string_immutable.zig index d3337312df..2982d161cc 100644 --- a/src/string_immutable.zig +++ b/src/string_immutable.zig @@ -1665,6 +1665,15 @@ pub fn addNTPathPrefix(wbuf: []u16, utf16: []const u16) [:0]const u16 { return wbuf[0 .. utf16.len + bun.windows.nt_object_prefix.len :0]; } +pub fn addNTPathPrefixIfNeeded(wbuf: []u16, utf16: []const u16) [:0]const u16 { + if (hasPrefixComptimeType(u16, utf16, &bun.windows.nt_object_prefix)) { + @memcpy(wbuf[0..utf16.len], utf16); + wbuf[utf16.len] = 0; + return wbuf[0..utf16.len :0]; + } + return addNTPathPrefix(wbuf, utf16); +} + // These are the same because they don't have rules like needing a trailing slash pub const toNTDir = toNTPath; From 82829c5925ae79371160d937bd3602a70f8a78e2 Mon Sep 17 00:00:00 2001 From: Dylan Conway Date: Tue, 30 Jan 2024 13:44:00 -0800 Subject: [PATCH 17/45] fix crash --- src/string_immutable.zig | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/string_immutable.zig b/src/string_immutable.zig index 2982d161cc..20314a7714 100644 --- a/src/string_immutable.zig +++ b/src/string_immutable.zig @@ -5302,6 +5302,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{}; const result = bun.simdutf.convert.utf8.to.utf16.le(input, buf); return buf[0..result]; } @@ -5312,6 +5313,7 @@ pub fn convertUTF16toUTF8InBuffer( ) ![]const u8 { // See above + if (input.len == 0) return &[_]u8{}; const result = bun.simdutf.convert.utf16.to.utf8.le(input, buf); // switch (result.status) { // .success => return buf[0..result.count], From 0333c7b0bf1a8242a7dc5f9136d07dc3fdde00c8 Mon Sep 17 00:00:00 2001 From: dave caruso Date: Tue, 30 Jan 2024 16:17:32 -0800 Subject: [PATCH 18/45] Fix "__dirname" on windows (#8579) --- src/bun.js/bindings/CommonJSModuleRecord.cpp | 24 +++++++++++++------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/src/bun.js/bindings/CommonJSModuleRecord.cpp b/src/bun.js/bindings/CommonJSModuleRecord.cpp index cdcb36451d..c6f95d628b 100644 --- a/src/bun.js/bindings/CommonJSModuleRecord.cpp +++ b/src/bun.js/bindings/CommonJSModuleRecord.cpp @@ -66,6 +66,7 @@ #include #include #include +#include "PathInlines.h" extern "C" bool Bun__isBunMain(JSC::JSGlobalObject* global, const BunString*); @@ -469,7 +470,7 @@ JSC_DEFINE_HOST_FUNCTION(functionCommonJSModuleRecord_compile, (JSGlobalObject * JSSourceCode* jsSourceCode = JSSourceCode::create(vm, WTFMove(sourceCode)); moduleObject->sourceCode.set(vm, moduleObject, jsSourceCode); - auto index = filenameString.reverseFind('/', filenameString.length()); + auto index = filenameString.reverseFind(PLATFORM_SEP, filenameString.length()); String dirnameString; if (index != WTF::notFound) { dirnameString = filenameString.substring(0, index); @@ -621,10 +622,14 @@ JSCommonJSModule* JSCommonJSModule::create( { auto& vm = globalObject->vm(); JSString* requireMapKey = JSC::jsStringWithCache(vm, key); - auto index = key.reverseFind('/', key.length()); - JSString* dirname = jsEmptyString(vm); + + auto index = key.reverseFind(PLATFORM_SEP, key.length()); + + JSString* dirname; if (index != WTF::notFound) { dirname = JSC::jsSubstring(globalObject, requireMapKey, 0, index); + } else { + dirname = jsEmptyString(vm); } auto* out = JSCommonJSModule::create( @@ -918,7 +923,6 @@ JSC_DEFINE_HOST_FUNCTION(jsFunctionRequireCommonJS, (JSGlobalObject * lexicalGlo BunString typeAttributeStr = { BunStringTag::Dead }; String typeAttribute = String(); - // We need to be able to wire in the "type" import attribute from bundled code.. // so we do it via CommonJS require(). int32_t previousArgumentCount = callframe->argument(2).asInt32(); @@ -1019,11 +1023,13 @@ std::optional createCommonJSModule( if (!moduleObject) { auto& vm = globalObject->vm(); auto* requireMapKey = jsStringWithCache(vm, sourceURL); - auto index = sourceURL.reverseFind('/', sourceURL.length()); - JSString* dirname = jsEmptyString(vm); + auto index = sourceURL.reverseFind(PLATFORM_SEP, sourceURL.length()); + JSString* dirname; JSString* filename = requireMapKey; if (index != WTF::notFound) { dirname = JSC::jsSubstring(globalObject, requireMapKey, 0, index); + } else { + dirname = jsEmptyString(vm); } moduleObject = JSCommonJSModule::create( @@ -1089,10 +1095,12 @@ JSObject* JSCommonJSModule::createBoundRequireFunction(VM& vm, JSGlobalObject* l auto* globalObject = jsCast(lexicalGlobalObject); JSString* filename = JSC::jsStringWithCache(vm, pathString); - auto index = pathString.reverseFind('/', pathString.length()); - JSString* dirname = jsEmptyString(vm); + auto index = pathString.reverseFind(PLATFORM_SEP, pathString.length()); + JSString* dirname; if (index != WTF::notFound) { dirname = JSC::jsSubstring(globalObject, filename, 0, index); + } else { + dirname = jsEmptyString(vm); } auto moduleObject = Bun::JSCommonJSModule::create( From 2d9db9b28f187625b1ceeb787e3dfca65a31f1a7 Mon Sep 17 00:00:00 2001 From: dave caruso Date: Tue, 30 Jan 2024 20:21:52 -0800 Subject: [PATCH 19/45] fix napi-rs libraries on windows (#8583) * yay * a * ok * typo * un-CRLFify these files --- src/bun.js/bindings/napi.cpp | 19 +++++++ src/cli/run_command.zig | 2 +- src/install/bin.zig | 12 ++--- src/install/install.zig | 9 ++-- src/install/windows-shim/bun_shim_impl.exe | Bin 10240 -> 10240 bytes src/install/windows-shim/bun_shim_impl.zig | 59 +++++++++++++-------- src/napi/napi.zig | 9 +++- src/symbols.dyn | 1 + src/symbols.txt | 1 + src/windows_c.zig | 2 +- 10 files changed, 76 insertions(+), 38 deletions(-) diff --git a/src/bun.js/bindings/napi.cpp b/src/bun.js/bindings/napi.cpp index 6b4460a4f9..9ed16ea24c 100644 --- a/src/bun.js/bindings/napi.cpp +++ b/src/bun.js/bindings/napi.cpp @@ -2089,6 +2089,25 @@ extern "C" napi_status napi_get_element(napi_env env, napi_value objectValue, return napi_ok; } +extern "C" napi_status napi_delete_element(napi_env env, napi_value objectValue, + uint32_t index, bool* result) +{ + NAPI_PREMABLE + + JSValue jsValue = toJS(objectValue); + if (UNLIKELY(!env || !jsValue || !jsValue.isObject())) { + return napi_invalid_arg; + } + + JSObject* object = jsValue.getObject(); + + auto scope = DECLARE_THROW_SCOPE(object->vm()); + *result = JSObject::deletePropertyByIndex(object, toJS(env), index); + RETURN_IF_EXCEPTION(scope, napi_generic_failure); + + return napi_ok; +} + extern "C" napi_status napi_create_object(napi_env env, napi_value* result) { NAPI_PREMABLE diff --git a/src/cli/run_command.zig b/src/cli/run_command.zig index eabf004d4d..3466b2e10f 100644 --- a/src/cli/run_command.zig +++ b/src/cli/run_command.zig @@ -1339,7 +1339,7 @@ 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, true); + 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 diff --git a/src/install/bin.zig b/src/install/bin.zig index 31278c69aa..e95ffa1525 100644 --- a/src/install/bin.zig +++ b/src/install/bin.zig @@ -448,19 +448,17 @@ pub const Bin = extern struct { destination_wpath.len -= 1; @memcpy(destination_wpath[destination_wpath.len - 3 ..], &[_]u16{ 'e', 'x', 'e' }); - if (node_modules.createFileW(destination_wpath, .{ - .exclusive = true, - })) |exe_file| { + // truncate=false is intentional so that the exe is always rewritten. this helps + // - you upgrade to a new version of bin_shim_impl (unlikely but possible) + // - if otherwise corrupt it yourself + if (node_modules.createFileW(destination_wpath, .{})) |exe_file| { defer exe_file.close(); exe_file.writer().writeAll(WinBinLinkingShim.embedded_executable_data) catch |err| { this.err = err; return; }; } else |err| { - if (err != error.PathAlreadyExists) { - this.err = err; - return; - } + this.err = err; } } } diff --git a/src/install/install.zig b/src/install/install.zig index e9d0292797..4fdcaa578f 100644 --- a/src/install/install.zig +++ b/src/install/install.zig @@ -1448,11 +1448,12 @@ pub const PackageInstall = struct { // Windows limits hardlinks to 1023 per file if (bun.windows.CreateHardLinkW(dest, src, null) == 0) { if (bun.windows.CopyFileW(src, dest, 0) == 0) { - if (bun.windows.Win32Error.get().toSystemErrno()) |_| { - // TODO: make this better - return error.FailedToCopyFile; + const e = bun.windows.Win32Error.get(); + if (e.toSystemErrno()) |err| { + return bun.errnoToZigErr(err); } - + // If this code path is reached, it should have a toSystemErrno mapping + Output.warn("Failed to copy file during installation: {s}", .{@tagName(e)}); return error.FailedToCopyFile; } } diff --git a/src/install/windows-shim/bun_shim_impl.exe b/src/install/windows-shim/bun_shim_impl.exe index 8ccadc89417958545460e9f70dc486d522335203..4ec808262ee5800acafc277363b38c57c3af7c9b 100755 GIT binary patch delta 1091 zcmah{T}TvB6ux&`cdckAABr?gi`iO^%kT16ltXtgW+ROH*4J45JcUAJBjB|*Ep452s|Ewmui6X$vg;1)hi3BA}a zD03E0aR5={9+9+5s2U-^M;YiAp`b5VC@INVzcL_tFoOc2LlZ52t|-c8Lu9uNfS?4U@xC06g*C*~;phL>oOyqaLBN(UWEC?n1>4BB&c?OUEWR#a92yH^p z`<(8!t;HvGV(TlJ%t^1a0Yrw$+)d{0gA?i4hjG}bJMJEtaKgW!m)JL0-puQ=-DBI! zREPEJ_Kl&0q=~$hRHV45G;f-cYgkeeR_NYKKaW>3My;KeXhIsAEDm(ic%3E|J4nUH z;AvTV&XWjcJxzOE-(~tkrZZkTYTR4Kz996>qR_Kzimkj}J6UFt=@?<6Uq%Q-Ui@9( zkz26w7<<;LI>zVVeMWjaSNJe&so#WSyw=-t^S4B;Y-?%WurGnjATnkkAJ*wJ+`z`3 z=I+Fnlq>dac{x6MH5ZajSR(|d%u%A{^tupw3-<)O``1G@0CN#+NCU^VwBo@cxh_rm z7}>um4OT8GHpjYFZo#2b1qcmij_Ev}g^y(6{aLu~KiHEMulyIUac=*YaBc&v3#-l* acKtL>1xy9I0d^nk30OZ^GFIsLlK%%Sv#Y-V delta 1033 zcmah`Z%9*76u);q-7xaihoBXi)mt^AaQ!tb+LJfDFWxjrGia8S4hgAhi`m1s+e zi|@a1Mvm6C$WebgfaXS`-qLC~YsGAVTa|<~Q3r+UASBR~F0x0SA4h(43bmvCG%OIf z5rJ0drIK)lZD$GfXm`15oe?1K-HuS44}ZW2MdEzqJ)Gj`jM$64V#qM@p%DnU9Ank$ zV3Pzdc`HLX$K%^2t6>2-5t2j0Y3~+(br?h=#IJTjWCrsWGmt8W60_dWpyUMKLsVzR zKdI1Kp;4lW(0N!^t4xG`B=%P%_=-aqYF306zCO+`m4tEvS~n`xCzQ}uq0BMna>QMZ zH~M%1s9p-HeM^|)`|vV}_6T?2?`*n$8wizrzqA+8q0mG+m|F1!WJUZ;4*vd#<0F0G zdK2ryf)Ye%EfbjubO?tbze8yB7b!GF=uep@^=9pQLI37&`MKuQLYj_Du-#!3X zr!TNbv??jnVR>k##Me&a`&Fr=i0sz=t%~{-W{8={q}x8F&>sp-yXm<8AdihB!6uAZ z5%$lljvBRTyGfxFgprbZ07z~;70@CU>zZQ>sC(L`XW>KrZnj}vLIBPI1D5_mdttF| zjBu=xxwvkb1DAlL|Au@RkVoGNQE$w!?7*hwrLmob<+$OK1+q?XL4sc!<3!EJr3k%* zvx1!iD= @intFromPtr(buf1_u16)); + // we start our search right before the . as we know the extension is '.bunx' std.debug.assert(ptr[1] == '.'); - inline for (0..1) |_| { - while (true) { - if (ptr[0] == '\\') { - break; - } + while (true) { + if (dbg) debug("1 - {}\n", .{std.unicode.fmtUtf16le(ptr[0..1])}); + if (ptr[0] == '\\') { left -= 1; - if (left == 0) { - fail(.NoDirname); - } - ptr -= 2; - std.debug.assert(@intFromPtr(ptr) >= @intFromPtr(buf1_u16)); + // ptr is of type [*]u16, which means -= operates on number of ITEMS, not BYTES + ptr -= 1; + break; } + left -= 1; + if (left == 0) { + fail(.NoDirname); + } + ptr -= 1; + std.debug.assert(@intFromPtr(ptr) >= @intFromPtr(buf1_u16)); } - // in this state, ptr is pointing to what is marked 'S' above - // adding one to get to the read ptr - ptr = @ptrFromInt(@intFromPtr(ptr) + @sizeOf(u16)); - break :brk ptr; + // inlined loop to do this again, because the completion case is different + // using `inline for` caused comptime issues that made the code much harder to read + 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* + break :brk ptr + 1; + } + left -= 1; + if (left == 0) { + fail(.NoDirname); + } + ptr -= 1; + std.debug.assert(@intFromPtr(ptr) >= @intFromPtr(buf1_u16)); + } + comptime unreachable; }; std.debug.assert(read_ptr[0] != '\\'); + std.debug.assert((read_ptr - 1)[0] == '\\'); const read_max_len = buf1.len * 2 - (@intFromPtr(read_ptr) - @intFromPtr(buf1_u16)); diff --git a/src/napi/napi.zig b/src/napi/napi.zig index 135cb9314f..a3eb87860e 100644 --- a/src/napi/napi.zig +++ b/src/napi/napi.zig @@ -560,6 +560,7 @@ pub export fn napi_has_element(env: napi_env, object: napi_value, index: c_uint, return .ok; } pub extern fn napi_get_element(env: napi_env, object: napi_value, index: u32, result: *napi_value) napi_status; +pub extern fn napi_delete_element(env: napi_env, object: napi_value, index: u32, result: *napi_value) napi_status; pub extern fn napi_define_properties(env: napi_env, object: napi_value, property_count: usize, properties: [*c]const napi_property_descriptor) napi_status; pub export fn napi_is_array(_: napi_env, value: napi_value, result: *bool) napi_status { log("napi_is_array", .{}); @@ -1203,8 +1204,12 @@ pub export fn napi_get_node_version(_: napi_env, version: **const napi_node_vers } pub export fn napi_get_uv_event_loop(env: napi_env, loop: **JSC.EventLoop) napi_status { log("napi_get_uv_event_loop", .{}); - // lol - loop.* = env.bunVM().eventLoop(); + if (bun.Environment.isWindows) { + loop.* = @ptrCast(@alignCast(env.bunVM().uvLoop())); + } else { + // there is no uv event loop on posix, we use our event loop handle. + loop.* = env.bunVM().eventLoop(); + } return .ok; } pub extern fn napi_fatal_exception(env: napi_env, err: napi_value) napi_status; diff --git a/src/symbols.dyn b/src/symbols.dyn index 63b17aa619..0be0db03ca 100644 --- a/src/symbols.dyn +++ b/src/symbols.dyn @@ -67,6 +67,7 @@ _napi_get_dataview_info; _napi_get_date_value; _napi_get_element; + _napi_delete_element; _napi_get_global; _napi_get_instance_data; _napi_get_last_error_info; diff --git a/src/symbols.txt b/src/symbols.txt index 4d39e499be..1d047fe7e2 100644 --- a/src/symbols.txt +++ b/src/symbols.txt @@ -66,6 +66,7 @@ _napi_get_cb_info _napi_get_dataview_info _napi_get_date_value _napi_get_element +_napi_delete_element _napi_get_global _napi_get_instance_data _napi_get_last_error_info diff --git a/src/windows_c.zig b/src/windows_c.zig index 6bb7c68361..7221835e30 100644 --- a/src/windows_c.zig +++ b/src/windows_c.zig @@ -695,7 +695,7 @@ pub const SystemErrno = enum(u16) { return init(@as(Win32Error, @enumFromInt(code))); } else { if (comptime bun.Environment.allow_assert) - bun.Output.debug("Unknown error code: {any}\n", .{code}); + bun.Output.debugWarn("Unknown error code: {any}\n", .{code}); return null; } From 1969204fa5cd5ed15263e91218bf04c9b789dcd9 Mon Sep 17 00:00:00 2001 From: Luke Ingalls <45518011+lukeingalls@users.noreply.github.com> Date: Wed, 31 Jan 2024 00:08:26 -0800 Subject: [PATCH 20/45] docs: remove outdated callout (#8584) seems like this is already patched https://github.com/oven-sh/bun/issues/5394 --- docs/cli/test.md | 4 ---- 1 file changed, 4 deletions(-) diff --git a/docs/cli/test.md b/docs/cli/test.md index 3dc58d0b5c..205d367f11 100644 --- a/docs/cli/test.md +++ b/docs/cli/test.md @@ -113,10 +113,6 @@ See [Test > Lifecycle](/docs/test/lifecycle) for complete documentation. ## Mocks -{% callout %} -Module mocking (`jest.mock()`) is not yet supported. Track support for it [here](https://github.com/oven-sh/bun/issues/5394). -{% /callout %} - Create mock functions with the `mock` function. Mocks are automatically reset between tests. ```ts From 4e09f8ef8f5a5dbd97550f3293fdea77096b1961 Mon Sep 17 00:00:00 2001 From: Joe Date: Wed, 31 Jan 2024 17:12:26 -0500 Subject: [PATCH 21/45] Issue-6526 - clarify web docs for the use of `.only()` (#8600) * Update writing.md clarify the use of `.only()` * Update test.d.ts Small clarification in JSDoc --- docs/test/writing.md | 8 +++++++- packages/bun-types/test.d.ts | 2 +- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/docs/test/writing.md b/docs/test/writing.md index dd9f19179d..e073ced734 100644 --- a/docs/test/writing.md +++ b/docs/test/writing.md @@ -109,7 +109,7 @@ $ bun test --todo ## `test.only` -To run a particular test or suite of tests use `test.only()` or `describe.only()`. Once declared, running `bun test --only` will only execute tests/suites that have been marked with `.only()`. +To run a particular test or suite of tests use `test.only()` or `describe.only()`. Once declared, running `bun test --only` will only execute tests/suites that have been marked with `.only()`. Running `bun test` without the `--only` option with `test.only()` declared will result in all tests in the given suite being executed _up to_ the test with `.only()`. `describe.only()` functions the same in both execution scenarios. ```ts import { test, describe } from "bun:test"; @@ -135,6 +135,12 @@ The following command will only execute tests #2 and #3. $ bun test --only ``` +The following command will only execute tests #1, #2 and #3. + +```sh +$ bun test +``` + ## `test.if` To run a test conditionally, use `test.if()`. The test will run if the condition is truthy. This is particularly useful for tests that should only run on specific architectures or operating systems. diff --git a/packages/bun-types/test.d.ts b/packages/bun-types/test.d.ts index 50b3e03b39..43c84e50ee 100644 --- a/packages/bun-types/test.d.ts +++ b/packages/bun-types/test.d.ts @@ -346,7 +346,7 @@ declare module "bun:test" { options?: number | TestOptions, ): void; /** - * Skips all other tests, except this test. + * Skips all other tests, except this test when run with the `--only` option. * * @param label the label for the test * @param fn the test function From f5754e3c194fe101cdf8e52c80d2f12b3cab94d0 Mon Sep 17 00:00:00 2001 From: dave caruso Date: Wed, 31 Jan 2024 18:13:39 -0800 Subject: [PATCH 22/45] add baseline in crash report (#8606) --- src/bun.js/WebKit | 2 +- src/report.zig | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/bun.js/WebKit b/src/bun.js/WebKit index 347037014a..54eae570cd 160000 --- a/src/bun.js/WebKit +++ b/src/bun.js/WebKit @@ -1 +1 @@ -Subproject commit 347037014ae069eed1c4f4687001a256949b124e +Subproject commit 54eae570cdca140cff95a1dda35ee2eb7a3523ab diff --git a/src/report.zig b/src/report.zig index d5bf3ed7d2..2dfb5b0513 100644 --- a/src/report.zig +++ b/src/report.zig @@ -119,10 +119,12 @@ pub fn printMetadata() void { const analytics_platform = Platform.forOS(); + const maybe_baseline = if (Environment.baseline) " (baseline)" else ""; + crash_report_writer.print( \\ \\----- bun meta ----- - ++ "\nBun v" ++ Global.package_json_version_with_sha ++ " " ++ platform ++ " " ++ arch ++ " {s}\n" ++ + ++ "\nBun v" ++ Global.package_json_version_with_sha ++ " " ++ platform ++ " " ++ arch ++ maybe_baseline ++ " {s}\n" ++ \\{s}: {} \\ , .{ From af1a1248319ffdac6fff2a4cf8ae7583dbc15a6a Mon Sep 17 00:00:00 2001 From: dave caruso Date: Wed, 31 Jan 2024 19:08:50 -0800 Subject: [PATCH 23/45] allow linking bins that do not exist. (#8605) --- src/install/bin.zig | 57 ++++++++++---------- src/install/windows-shim/BinLinkingShim.zig | 5 +- src/install/windows-shim/bun_shim_impl.exe | Bin 10240 -> 10752 bytes src/install/windows-shim/bun_shim_impl.zig | 37 +++++++++---- 4 files changed, 61 insertions(+), 38 deletions(-) diff --git a/src/install/bin.zig b/src/install/bin.zig index e95ffa1525..1d752d9be2 100644 --- a/src/install/bin.zig +++ b/src/install/bin.zig @@ -392,40 +392,41 @@ pub const Bin = extern struct { }; defer file.close(); - const first_content_chunk = contents: { - const fd = bun.sys.openatWindows( - this.package_installed_node_modules, - if (link_global) - bun.strings.toWPathNormalized( - &filename3_buf, - target_path[this.relative_path_to_bin_for_windows_global_link_offset..], - ) - else - target_wpath, - std.os.O.RDONLY, - ).unwrap() catch |err| { - this.err = err; - return; + const shebang = shebang: { + const first_content_chunk = contents: { + const fd = bun.sys.openatWindows( + this.package_installed_node_modules, + if (link_global) + bun.strings.toWPathNormalized( + &filename3_buf, + target_path[this.relative_path_to_bin_for_windows_global_link_offset..], + ) + else + target_wpath, + std.os.O.RDONLY, + ).unwrap() catch break :contents null; + defer _ = bun.sys.close(fd); + const reader = fd.asFile().reader(); + const read = reader.read(&read_in_buf) catch break :contents null; + if (read == 0) { + break :contents null; + } + break :contents read_in_buf[0..read]; }; - defer _ = bun.sys.close(fd); - const reader = fd.asFile().reader(); - const read = reader.read(&read_in_buf) catch |err| { - this.err = err; - return; - }; - if (read == 0) { - this.err = error.FileNotFound; - return; + + if (first_content_chunk) |chunk| { + break :shebang WinBinLinkingShim.Shebang.parse(chunk, target_wpath) catch { + this.err = error.InvalidBinContent; + return; + }; + } else { + break :shebang WinBinLinkingShim.Shebang.parseFromBinPath(target_wpath); } - break :contents read_in_buf[0..read]; }; const shim = WinBinLinkingShim{ .bin_path = target_wpath, - .shebang = WinBinLinkingShim.Shebang.parse(first_content_chunk, target_wpath) catch { - this.err = error.InvalidBinContent; - return; - }, + .shebang = shebang, }; const len = shim.encodedLength(); diff --git a/src/install/windows-shim/BinLinkingShim.zig b/src/install/windows-shim/BinLinkingShim.zig index 8cb4c552e9..ae8438d7e3 100644 --- a/src/install/windows-shim/BinLinkingShim.zig +++ b/src/install/windows-shim/BinLinkingShim.zig @@ -30,10 +30,13 @@ 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 = .v2; + pub const current = .v3; v1 = 5474, + // Fix bug where paths were not joined correctly v2 = 5475, + // Added an error message for when the process is not found + v3 = 5476, _, }; diff --git a/src/install/windows-shim/bun_shim_impl.exe b/src/install/windows-shim/bun_shim_impl.exe index 4ec808262ee5800acafc277363b38c57c3af7c9b..e2e560b5d03ff04d1bc2dd73e69e0bdff2fe582c 100755 GIT binary patch delta 1624 zcmbtUQEXFH7(VCPu3fwBP67)KQ|@XvEx{#aK!R*@U0S$ZTEz$pE6DW;r14% zS<+%x;X0booTvd8!M#W{#&nGmXEfE3p=OD&C*)y{g~*+du&5bxD)m3@UFez^A3Vu_ zzVrRx_n-gd+>@Kwm)L(M(J($aAs}@uA~P~FbqLjD-WIi2w;|L{F!Q2zunnQZnWjx7 zf%Q9g8o*r-Q3Ih-D^=hS>TebOUdfBlr|TgJ3Df{lPrky9b)yU{yC7&;OYUdf&M-ZC zV$t+6W<43weBS)B9-)ry2<6$~b}*;%Y-$&v{n|Zy76ibAn)E0+U-iL$UcTY9<(;Q`4~tbxy-P`<3QO< z`Hov~MmF^c6}w=md_h-JFgfI#*A|?%8C%{qbEk#fJdDVY&VH26}qr`GrQB8QHt!k}tcyzRO92a=s_+;Oh8Y*JB|#rG-C%kt=y+ z$z#3E%Rjp16^}fpSm1gC-B*T-su{Vg_rUVNllDK(xaFHXH7zTJM-cMh+le8|K{)yf zG_twnKY009enhhLyW~Y>mO9eyuPn@hsN{=xdD@ZyoG8Fhp3{=Neq&strI%b1l#|e! zl2hPPUIPNRe@Mx~g5Gh=pu9na_vsXZDbPxT*o-uBZo8ZOrKFtXpe_NR2(^|U~p-I3#iW4xwYtq(jp=48u zyk1mHC6ZC?ZWSks;>MELT`Z#qggt&QDvc#lTU5*lt)o(iO4F+&_k-~X9G`y-q1DXv zbfNi11)r(l-&gQ&{)10c=;IZ<`NN9DSOq@^oMdVvFnO?qB+JqyOEAWSicp*(*qHZ&Y%MVnob2%%}0V zWdjorh9yBfDhe=j!l2-bN#22w!1*Gvke>@jByK?90+CqQj|PI_a)&=6M2iMtFc_7% vNSN~nqeswDG2#=VQBD+&#eyPOgphFDD+MEA^l(p~D0n4-18>gS62kugKDQV} delta 1266 zcmb7DZAcVB7@pboyz5SGElT7fzuL152~RD^FVfE1mAk$ZlL#%7s3=7eE1vvr&>O_enT;g~`N5N%%_duL#Y`V#&Hkh31aQ{e&W55x141BQ*Nea1TOvEq9b7P@p zGYf5F(>}tRGCcB6O_S66WbZ2;y1La5Be~4+Z=ICYS90%Ya&4U&E)6)7t-UtQ|7TOFbg79mtq9BG_gO+|A07(`~a2dZ;8mpUx5&^PLVkQ;wi z>cw*S(1aOHbGX#+k~$&zYsE@g9j6CaB9MP>7>AuEL(9HiY{A z49$LUfv#AfcP-GR|Ioz?)^q3U<@T)khJ7t)Qe?f5cyUV8yZ}GoI-m{k1keqLOoi6P z-Cvr9;6nc%C*ZoV3!XX0s{^k&yCucu3G41#o!{w9b)MAucehxmWFt}MOr5vrT&{C8 n1vgf&|4aWY^xqqh{#DmPbz&1H$vCqkZ8;ZG)+m_7amRiE%te=* diff --git a/src/install/windows-shim/bun_shim_impl.zig b/src/install/windows-shim/bun_shim_impl.zig index 76b1cb2f25..ef918cfc72 100644 --- a/src/install/windows-shim/bun_shim_impl.zig +++ b/src/install/windows-shim/bun_shim_impl.zig @@ -34,7 +34,7 @@ //! Prior Art: //! - https://github.com/ScoopInstaller/Shim/blob/master/src/shim.cs //! -//! The compiled binary is 10240 bytes and is `@embedFile`d into Bun itself. +//! The compiled binary is 10752 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"); @@ -148,6 +148,9 @@ const FailReason = enum { InvalidShimValidation, InvalidShimBounds, CouldNotDirectLaunch, + BinNotFound, + InterpreterNotFound, + ElevationRequired, pub fn render(reason: FailReason) []const u8 { return switch (reason) { @@ -159,6 +162,12 @@ const FailReason = enum { .InvalidShimDataSize => "bin metadata is corrupt (size)", .InvalidShimValidation => "bin metadata is corrupt (validate)", .InvalidShimBounds => "bin metadata is corrupt (bounds)", + // 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", + .BinNotFound => "bin executable does not exist on disk", + .ElevationRequired => "process requires elevation", .CreateProcessFailed => "could not create process", .CouldNotDirectLaunch => if (!is_standalone) @@ -678,17 +687,27 @@ fn launcher(bun_ctx: anytype) noreturn { &process, ); if (did_process_spawn == 0) { + const spawn_err = k32.GetLastError(); if (dbg) { - const spawn_err = k32.GetLastError(); printError("CreateProcessW failed: {s}\n", .{@tagName(spawn_err)}); } - // 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 - fail(.CreateProcessFailed); + 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), + + else => fail(.CreateProcessFailed), + } + comptime unreachable; } _ = k32.WaitForSingleObject(process.hProcess, w.INFINITE); From fcf0047763cffc03123d67dcb66fb5e8a533b02d Mon Sep 17 00:00:00 2001 From: Meghan Denny Date: Wed, 31 Jan 2024 21:08:19 -0800 Subject: [PATCH 24/45] windows: pass ws.test.ts (#8611) --- test/js/first_party/ws/ws.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/js/first_party/ws/ws.test.ts b/test/js/first_party/ws/ws.test.ts index 778c18f9e3..6c8334f115 100644 --- a/test/js/first_party/ws/ws.test.ts +++ b/test/js/first_party/ws/ws.test.ts @@ -1,9 +1,9 @@ -// @known-failing-on-windows: 1 failing import { describe, it, expect, beforeEach, afterEach } from "bun:test"; import type { Subprocess } from "bun"; import { spawn } from "bun"; import { bunEnv, bunExe, nodeExe } from "harness"; import { Server, WebSocket, WebSocketServer } from "ws"; +import path from "node:path"; const strings = [ { @@ -362,7 +362,7 @@ function test(label: string, fn: (ws: WebSocket, done: (err?: unknown) => void) } async function listen(): Promise { - const { pathname } = new URL("../../web/websocket/websocket-server-echo.mjs", import.meta.url); + const pathname = path.resolve(import.meta.dir, "../../web/websocket/websocket-server-echo.mjs"); const server = spawn({ cmd: [nodeExe() ?? bunExe(), pathname], cwd: import.meta.dir, From 16b7b94aea0132d5ad8d8974c311c993d8fae9c5 Mon Sep 17 00:00:00 2001 From: huseeiin <122984423+huseeiin@users.noreply.github.com> Date: Thu, 1 Feb 2024 08:13:12 +0300 Subject: [PATCH 25/45] Update bun.d.ts (#8595) Add `string | ArrayBuffer` to compressing/uncompression functions --- packages/bun-types/bun.d.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/bun-types/bun.d.ts b/packages/bun-types/bun.d.ts index 9c3fc0631a..ca6d4c4206 100644 --- a/packages/bun-types/bun.d.ts +++ b/packages/bun-types/bun.d.ts @@ -3158,26 +3158,26 @@ declare module "bun" { * @param options Compression options to use * @returns The output buffer with the compressed data */ - function deflateSync(data: Uint8Array, options?: ZlibCompressionOptions): Uint8Array; + function deflateSync(data: Uint8Array | string | ArrayBuffer, options?: ZlibCompressionOptions): Uint8Array; /** * Compresses a chunk of data with `zlib` GZIP algorithm. * @param data The buffer of data to compress * @param options Compression options to use * @returns The output buffer with the compressed data */ - function gzipSync(data: Uint8Array, options?: ZlibCompressionOptions): Uint8Array; + function gzipSync(data: Uint8Array | string | ArrayBuffer, options?: ZlibCompressionOptions): Uint8Array; /** * Decompresses a chunk of data with `zlib` INFLATE algorithm. * @param data The buffer of data to decompress * @returns The output buffer with the decompressed data */ - function inflateSync(data: Uint8Array): Uint8Array; + function inflateSync(data: Uint8Array | string | ArrayBuffer): Uint8Array; /** * Decompresses a chunk of data with `zlib` GUNZIP algorithm. * @param data The buffer of data to decompress * @returns The output buffer with the decompressed data */ - function gunzipSync(data: Uint8Array): Uint8Array; + function gunzipSync(data: Uint8Array | string | ArrayBuffer): Uint8Array; type Target = /** From 690346b30bcc465233a9c631c77df5cfc887ae60 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?3=CE=BBi=C8=AF+?= Date: Thu, 1 Feb 2024 06:18:57 +0100 Subject: [PATCH 26/45] docs: Fix typo in code example for Transpiler.transformSync (#8553) * Docs: Fix typo in code example for transformSync * Remove await since transformSync is synchronous Co-authored-by: Jarred Sumner --------- Co-authored-by: Jarred Sumner --- docs/api/transpiler.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/api/transpiler.md b/docs/api/transpiler.md index ede4ee7cce..308a4e152d 100644 --- a/docs/api/transpiler.md +++ b/docs/api/transpiler.md @@ -50,7 +50,7 @@ export default jsx( To override the default loader specified in the `new Bun.Transpiler()` constructor, pass a second argument to `.transformSync()`. ```ts -await transpiler.transform("
hi!
", "tsx"); +transpiler.transformSync("
hi!
", "tsx"); ``` {% details summary="Nitty gritty" %} From 345a061d3b15ce245a2398478d15fb89af794cd0 Mon Sep 17 00:00:00 2001 From: dave caruso Date: Wed, 31 Jan 2024 21:52:50 -0800 Subject: [PATCH 27/45] fix(windows): make `process.env` case-insensitive (#8578) * yay!!!!!! * [autofix.ci] apply automated fixes * ok * do not use reflect here * ok * [autofix.ci] apply automated fixes --------- Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> --- CMakeLists.txt | 2 +- src/bun.js/ConsoleObject.zig | 56 ++++++++++--------- .../bindings/JSEnvironmentVariableMap.cpp | 47 +++++++++++++++- src/js/builtins/ProcessObjectInternals.ts | 44 +++++++++++++++ src/string_immutable.zig | 1 - test/cli/run/env.test.ts | 21 ++++++- test/js/node/env-windows.test.ts | 33 +++++++++++ 7 files changed, 171 insertions(+), 33 deletions(-) create mode 100644 test/js/node/env-windows.test.ts diff --git a/CMakeLists.txt b/CMakeLists.txt index 147cec1fcf..7b3fb3d6dc 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -2,7 +2,7 @@ cmake_minimum_required(VERSION 3.22) cmake_policy(SET CMP0091 NEW) cmake_policy(SET CMP0067 NEW) -set(Bun_VERSION "1.0.26") +set(Bun_VERSION "1.1.0") set(WEBKIT_TAG 9c501b9aa712b7959f80dc99491e8758c151c20e) set(BUN_WORKDIR "${CMAKE_CURRENT_BINARY_DIR}") diff --git a/src/bun.js/ConsoleObject.zig b/src/bun.js/ConsoleObject.zig index a332cd3bc5..840b05d350 100644 --- a/src/bun.js/ConsoleObject.zig +++ b/src/bun.js/ConsoleObject.zig @@ -1107,22 +1107,24 @@ pub const Formatter = struct { return .{ .tag = switch (js_type) { - JSValue.JSType.ErrorInstance => .Error, - JSValue.JSType.NumberObject => .Double, - JSValue.JSType.DerivedArray, JSValue.JSType.Array => .Array, - JSValue.JSType.DerivedStringObject, JSValue.JSType.String, JSValue.JSType.StringObject => .String, - JSValue.JSType.RegExpObject => .String, - JSValue.JSType.Symbol => .Symbol, - JSValue.JSType.BooleanObject => .Boolean, - JSValue.JSType.JSFunction => .Function, - JSValue.JSType.JSWeakMap, JSValue.JSType.JSMap => .Map, - JSValue.JSType.JSMapIterator => .MapIterator, - JSValue.JSType.JSSetIterator => .SetIterator, - JSValue.JSType.JSWeakSet, JSValue.JSType.JSSet => .Set, - JSValue.JSType.JSDate => .JSON, - JSValue.JSType.JSPromise => .Promise, - JSValue.JSType.Object, - JSValue.JSType.FinalObject, + .ErrorInstance => .Error, + .NumberObject => .Double, + .DerivedArray, JSValue.JSType.Array => .Array, + .DerivedStringObject, JSValue.JSType.String, JSValue.JSType.StringObject => .String, + .RegExpObject => .String, + .Symbol => .Symbol, + .BooleanObject => .Boolean, + .JSFunction => .Function, + .JSWeakMap, JSValue.JSType.JSMap => .Map, + .JSMapIterator => .MapIterator, + .JSSetIterator => .SetIterator, + .JSWeakSet, JSValue.JSType.JSSet => .Set, + .JSDate => .JSON, + .JSPromise => .Promise, + + .Object, + .FinalObject, + .ProxyObject, .ModuleNamespaceObject, => .Object, @@ -1132,17 +1134,17 @@ pub const Formatter = struct { .GlobalObject, .ArrayBuffer, - JSValue.JSType.Int8Array, - JSValue.JSType.Uint8Array, - JSValue.JSType.Uint8ClampedArray, - JSValue.JSType.Int16Array, - JSValue.JSType.Uint16Array, - JSValue.JSType.Int32Array, - JSValue.JSType.Uint32Array, - JSValue.JSType.Float32Array, - JSValue.JSType.Float64Array, - JSValue.JSType.BigInt64Array, - JSValue.JSType.BigUint64Array, + .Int8Array, + .Uint8Array, + .Uint8ClampedArray, + .Int16Array, + .Uint16Array, + .Int32Array, + .Uint32Array, + .Float32Array, + .Float64Array, + .BigInt64Array, + .BigUint64Array, .DataView, => .TypedArray, diff --git a/src/bun.js/bindings/JSEnvironmentVariableMap.cpp b/src/bun.js/bindings/JSEnvironmentVariableMap.cpp index b23eac87d0..d0514ae781 100644 --- a/src/bun.js/bindings/JSEnvironmentVariableMap.cpp +++ b/src/bun.js/bindings/JSEnvironmentVariableMap.cpp @@ -5,7 +5,13 @@ #include #include +#include +#include +#include +#include + #include "BunClientData.h" + using namespace JSC; extern "C" size_t Bun__getEnvCount(JSGlobalObject* globalObject, void** list_ptr); @@ -48,7 +54,11 @@ JSC_DEFINE_CUSTOM_SETTER(jsSetterEnvironmentVariable, (JSGlobalObject * globalOb if (!object) return false; - object->putDirect(vm, propertyName, JSValue::decode(value), 0); + auto string = JSValue::decode(value).toString(globalObject); + if (UNLIKELY(!string)) + return false; + + object->putDirect(vm, propertyName, string, 0); return true; } @@ -125,19 +135,30 @@ JSValue createEnvironmentVariablesMap(Zig::GlobalObject* globalObject) object = constructEmptyObject(globalObject, globalObject->objectPrototype()); } +#if OS(WINDOWS) + JSArray* keyArray = constructEmptyArray(globalObject, nullptr, count); +#endif + static NeverDestroyed TZ = MAKE_STATIC_STRING_IMPL("TZ"); bool hasTZ = false; for (size_t i = 0; i < count; i++) { unsigned char* chars; size_t len = Bun__getEnvKey(list, i, &chars); auto name = String::fromUTF8(chars, len); +#if OS(WINDOWS) + keyArray->putByIndexInline(globalObject, (unsigned)i, jsString(vm, name), false); +#endif if (name == TZ) { hasTZ = true; continue; } ASSERT(len > 0); - - Identifier identifier = Identifier::fromString(vm, name); +#if OS(WINDOWS) + String idName = name.convertToASCIIUppercase(); +#else + String idName = name; +#endif + Identifier identifier = Identifier::fromString(vm, idName); // CustomGetterSetter doesn't support indexed properties yet. // This causes strange issues when the environment variable name is an integer. @@ -165,6 +186,26 @@ JSValue createEnvironmentVariablesMap(Zig::GlobalObject* globalObject) vm, Identifier::fromString(vm, TZ), JSC::CustomGetterSetter::create(vm, jsTimeZoneEnvironmentVariableGetter, jsTimeZoneEnvironmentVariableSetter), TZAttrs); +#if OS(WINDOWS) + JSC::JSFunction* getSourceEvent = JSC::JSFunction::create(vm, processObjectInternalsWindowsEnvCodeGenerator(vm), globalObject); + RETURN_IF_EXCEPTION(scope, {}); + JSC::MarkedArgumentBuffer args; + args.append(object); + args.append(keyArray); + auto clientData = WebCore::clientData(vm); + JSC::CallData callData = JSC::getCallData(getSourceEvent); + NakedPtr returnedException = nullptr; + auto result = JSC::call(globalObject, getSourceEvent, callData, globalObject->globalThis(), args, returnedException); + RETURN_IF_EXCEPTION(scope, {}); + + if (returnedException) { + throwException(globalObject, scope, returnedException.get()); + return jsUndefined(); + } + + RELEASE_AND_RETURN(scope, result); +#else return object; +#endif } } diff --git a/src/js/builtins/ProcessObjectInternals.ts b/src/js/builtins/ProcessObjectInternals.ts index 48495efdeb..2bff5b66e5 100644 --- a/src/js/builtins/ProcessObjectInternals.ts +++ b/src/js/builtins/ProcessObjectInternals.ts @@ -344,3 +344,47 @@ export function setMainModule(value) { $putByIdDirectPrivate(this, "main", value); return true; } + +type InternalEnvMap = Record; + +export function windowsEnv(internalEnv: InternalEnvMap, envMapList: Array) { + // The use of String(key) here is intentional because Node.js as of v21.5.0 will throw + // on symbol keys as it seems they assume the user uses string keys: + // + // it throws "Cannot convert a Symbol value to a string" + + return new Proxy(internalEnv, { + get(_, p) { + return typeof p === "string" ? Reflect.get(internalEnv, p.toUpperCase()) : undefined; + }, + set(_, p, value) { + var k = String(p).toUpperCase(); + $assert(typeof p === "string"); // proxy is only string and symbol. the symbol would have thrown by now + if (!Reflect.has(internalEnv, k)) { + envMapList.push(p); + } + return Reflect.set(internalEnv, k, String(value)); + }, + has(_, p) { + return typeof p === "string" ? Reflect.has(internalEnv, p.toUpperCase()) : false; + }, + deleteProperty(_, p) { + return typeof p === "string" ? Reflect.deleteProperty(internalEnv, p.toUpperCase()) : true; + }, + defineProperty(_, p, attributes) { + var k = String(p).toUpperCase(); + $assert(typeof p === "string"); // proxy is only string and symbol. the symbol would have thrown by now + if (!Reflect.has(internalEnv, k)) { + envMapList.push(p); + } + return Reflect.defineProperty(internalEnv, k, attributes); + }, + getOwnPropertyDescriptor(target, p) { + return typeof p === "string" ? Reflect.getOwnPropertyDescriptor(target, p.toUpperCase()) : undefined; + }, + ownKeys() { + // .slice() because paranoia that there is a way to call this without the engine cloning it for us + return envMapList.slice(); + }, + }); +} diff --git a/src/string_immutable.zig b/src/string_immutable.zig index 20314a7714..166b49a378 100644 --- a/src/string_immutable.zig +++ b/src/string_immutable.zig @@ -5312,7 +5312,6 @@ pub fn convertUTF16toUTF8InBuffer( input: []const u16, ) ![]const u8 { // See above - if (input.len == 0) return &[_]u8{}; const result = bun.simdutf.convert.utf16.to.utf8.le(input, buf); // switch (result.status) { diff --git a/test/cli/run/env.test.ts b/test/cli/run/env.test.ts index 645b41cf67..6fcacd6ab9 100644 --- a/test/cli/run/env.test.ts +++ b/test/cli/run/env.test.ts @@ -701,8 +701,27 @@ console.log(dynamic().NODE_ENV); }); test("NODE_ENV default is not propogated in bun run", () => { + const getenv = + process.platform !== "win32" ? "env | grep NODE_ENV && exit 1 || true" : "node -e if(process.env.NODE_ENV)throw(1)"; const tmp = tempDirWithFiles("default-node-env", { - "package.json": '{"scripts":{"show-env":"env | grep NODE_ENV && exit 1 || true"}}', + "package.json": '{"scripts":{"show-env":' + JSON.stringify(getenv) + "}}", }); expect(bunRunAsScript(tmp, "show-env", {}).stdout).toBe(""); }); + +const todoOnPosix = process.platform !== "win32" ? test.todo : test; +todoOnPosix("setting process.env coerces the value to a string", () => { + // @ts-expect-error + process.env.SET_TO_TRUE = true; + let did_call = 0; + // @ts-expect-error + process.env.SET_TO_BUN = { + toString() { + did_call++; + return "bun!"; + }, + }; + expect(process.env.SET_TO_TRUE).toBe("true"); + expect(process.env.SET_TO_BUN).toBe("bun!"); + expect(did_call).toBe(1); +}); diff --git a/test/js/node/env-windows.test.ts b/test/js/node/env-windows.test.ts new file mode 100644 index 0000000000..36ddd41d98 --- /dev/null +++ b/test/js/node/env-windows.test.ts @@ -0,0 +1,33 @@ +import { test, expect } from "bun:test"; + +test.if(process.platform === "win32")("process.env is case insensitive on windows", () => { + const keys = Object.keys(process.env); + // this should have at least one character that is lowercase + // it is likely that PATH will be 'Path', and also stuff like 'WindowsLibPath' and so on. + // but not guaranteed, so we just check that there is at least one of each case + expect( + keys + .join("") + .split("") + .some(c => c.toUpperCase() !== c), + ).toBe(true); + expect( + keys + .join("") + .split("") + .some(c => c.toLowerCase() !== c), + ).toBe(true); + expect(process.env.path).toBe(process.env.PATH!); + expect(process.env.pAtH).toBe(process.env.PATH!); + + expect(process.env.doesntexistahahahahaha).toBeUndefined(); + // @ts-expect-error + process.env.doesntExistAHaHaHaHaHa = true; + expect(process.env.doesntexistahahahahaha).toBe("true"); + expect(process.env.doesntexistahahahahaha).toBe("true"); + expect(process.env.doesnteXISTahahahahaha).toBe("true"); + expect(Object.keys(process.env).pop()).toBe("doesntExistAHaHaHaHaHa"); + delete process.env.DOESNTEXISTAHAHAHAHAHA; + expect(process.env.doesntexistahahahahaha).toBeUndefined(); + expect(Object.keys(process.env)).not.toInclude("doesntExistAHaHaHaHaHa"); +}); From dccabd942395c5fd6a10d5b2fd4a3442b76827cc Mon Sep 17 00:00:00 2001 From: Jarred Sumner <709451+Jarred-Sumner@users.noreply.github.com> Date: Wed, 31 Jan 2024 22:03:27 -0800 Subject: [PATCH 28/45] Make test work better on windows --- test/regression/issue/07500.test.ts | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/test/regression/issue/07500.test.ts b/test/regression/issue/07500.test.ts index d7434d40d7..20ac53f761 100644 --- a/test/regression/issue/07500.test.ts +++ b/test/regression/issue/07500.test.ts @@ -4,24 +4,30 @@ import { bunEnv, bunExe } from "harness"; import { tmpdir } from "os"; import { join } from "path"; test("7500 - Bun.stdin.text() doesn't read all data", async () => { - const filename = join(tmpdir(), "/bun.test.offset.txt"); + const filename = join(tmpdir(), "bun.test.offset." + Date.now() + ".txt"); const text = "contents of file to be read with several lines of text and lots and lots and lots and lots of bytes! " .repeat(1000) .repeat(9) .split(" ") .join("\n"); await Bun.write(filename, text); - const bunCommand = `${bunExe()} ${join(import.meta.dir, "7500-repro-fixture.js")}`; const shellCommand = `cat ${filename} | ${bunCommand}`; + + const cmd = process.platform === "win32" ? ["pwsh.exe", `-Command='${shellCommand}'`] : ["bash", "-c", shellCommand]; const proc = Bun.spawnSync({ - cmd: ["bash", "-c", shellCommand], + cmd, stdin: "inherit", stdout: "pipe", stderr: "inherit", env: bunEnv, }); - const output = proc.stdout.toString(); - expect(output).toBe(text); + + const output = proc.stdout.toString().replaceAll("\r\n", "\n"); + if (output !== text) { + expect(output).toHaveLength(text.length); + throw new Error("Output didn't match!\n"); + } + expect(proc.exitCode).toBe(0); }, 100000); From 350cc1178a9ad94e4fa1bf94f28294fec7f5c298 Mon Sep 17 00:00:00 2001 From: Georgijs <48869301+gvilums@users.noreply.github.com> Date: Wed, 31 Jan 2024 22:06:33 -0800 Subject: [PATCH 29/45] `--watch` and `--hot` on windows (#8607) * draft impl of windows watcher * synchronous watcher * working standalone watcher * in progress changes to watcher * make watcher non-global * prepare watcher for windows impl * add windows watcher scaffold and clean up imports * fix inotify * make watch code more generic over platforms * fix visibility * watcher starts without error * printing changes works * basic windows watching works * handle process exit from watcher * cleanup in process cloning * clean up logging and panic handling * fix hot reload test on windows * misc cleanup around watcher * make watch test actually useful * [autofix.ci] apply automated fixes * remove old files * clean up watchers * update .gitignore * rework windows watcher into single watcher instance watching top level project dir * use non-strict utf16 conversion * change to contains * fix mac and linux compile * add baseline in crash report (#8606) * allow linking bins that do not exist. (#8605) * fix linux compile * fix linux compile (again) * remove outdated todo --------- Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> Co-authored-by: dave caruso --- .gitignore | 331 ++++++------ src/__global.zig | 2 + src/bun.js/event_loop.zig | 12 +- src/bun.js/javascript.zig | 45 +- src/bun.js/module_loader.zig | 4 +- src/bun.js/node/path_watcher.zig | 18 +- src/bun.js/node/win_watcher.zig | 61 +-- src/bun.zig | 196 ++++++- src/cli.zig | 8 + src/resolver/resolve_path.zig | 17 + src/string_immutable.zig | 16 + src/watcher.zig | 820 +++++++++++++++++++----------- src/windows.zig | 54 +- test/cli/hot/hot.test.ts | 18 +- test/cli/watch/watch.test.ts | 33 +- test/js/node/fs/fs-stream.link.js | 1 - 16 files changed, 1046 insertions(+), 590 deletions(-) delete mode 120000 test/js/node/fs/fs-stream.link.js diff --git a/.gitignore b/.gitignore index 8721435d6b..8373c489d1 100644 --- a/.gitignore +++ b/.gitignore @@ -1,166 +1,167 @@ -.DS_Store -zig-cache -packages/*/*.wasm -*.o -*.a -profile.json - -node_modules -.envrc -.swcrc -yarn.lock -dist -*.tmp -*.log -*.out.js -*.out.refresh.js -**/package-lock.json -build -*.wat -zig-out -pnpm-lock.yaml -README.md.template -src/deps/zig-clap/example -src/deps/zig-clap/README.md -src/deps/zig-clap/.github -src/deps/zig-clap/.gitattributes -out -outdir - -.trace -cover -coverage -coverv -*.trace -github -out.* -out -.parcel-cache -esbuilddir -*.bun -parceldist -esbuilddir -outdir/ -outcss -.next -txt.js -.idea -.vscode/cpp* -.vscode/clang* - -node_modules_* -*.jsb -*.zip -bun-zigld -bun-singlehtreaded -bun-nomimalloc -bun-mimalloc -examples/lotta-modules/bun-yday -examples/lotta-modules/bun-old -examples/lotta-modules/bun-nofscache - -src/node-fallbacks/out/* -src/node-fallbacks/node_modules -sign.json -release/ -*.dmg -sign.*.json -packages/debug-* -packages/bun-cli/postinstall.js -packages/bun-*/bun -packages/bun-*/bun-profile -packages/bun-*/debug-bun -packages/bun-*/*.o -packages/bun-cli/postinstall.js - -packages/bun-cli/bin/* -bun-test-scratch -misctools/fetch - -src/deps/libiconv -src/deps/openssl -src/tests.zig -*.blob -src/deps/s2n-tls -.npm -.npm.gz - -bun-binary - -src/deps/PLCrashReporter/ - -*.dSYM -*.crash -misctools/sha -packages/bun-wasm/*.mjs -packages/bun-wasm/*.cjs -packages/bun-wasm/*.map -packages/bun-wasm/*.js -packages/bun-wasm/*.d.ts -packages/bun-wasm/*.d.cts -packages/bun-wasm/*.d.mts -*.bc - -src/fallback.version -src/runtime.version -*.sqlite -*.database -*.db -misctools/machbench -*.big -.eslintcache - -/bun-webkit - -src/deps/c-ares/build -src/bun.js/bindings-obj -src/bun.js/debug-bindings-obj - -failing-tests.txt -test.txt -myscript.sh - -cold-jsc-start -cold-jsc-start.d - -/test.ts -/test.js - -src/js/out/modules* -src/js/out/functions* -src/js/out/tmp -src/js/out/DebugPath.h - -make-dev-stats.csv - -.uuid -tsconfig.tsbuildinfo - -test/js/bun/glob/fixtures -*.lib -*.pdb -CMakeFiles -build.ninja -.ninja_deps -.ninja_log -CMakeCache.txt -cmake_install.cmake -compile_commands.json - -*.lib -x64 -**/*.vcxproj* -**/*.sln* -**/*.dir -**/*.pdb - -/.webkit-cache -/.cache -/src/deps/libuv -/build-*/ - -.vs - -**/.verdaccio-db.json -/test-report.md +.DS_Store +zig-cache +packages/*/*.wasm +*.o +*.a +profile.json + +node_modules +.envrc +.swcrc +yarn.lock +dist +*.tmp +*.log +*.out.js +*.out.refresh.js +**/package-lock.json +build +*.wat +zig-out +pnpm-lock.yaml +README.md.template +src/deps/zig-clap/example +src/deps/zig-clap/README.md +src/deps/zig-clap/.github +src/deps/zig-clap/.gitattributes +out +outdir + +.trace +cover +coverage +coverv +*.trace +github +out.* +out +.parcel-cache +esbuilddir +*.bun +parceldist +esbuilddir +outdir/ +outcss +.next +txt.js +.idea +.vscode/cpp* +.vscode/clang* + +node_modules_* +*.jsb +*.zip +bun-zigld +bun-singlehtreaded +bun-nomimalloc +bun-mimalloc +examples/lotta-modules/bun-yday +examples/lotta-modules/bun-old +examples/lotta-modules/bun-nofscache + +src/node-fallbacks/out/* +src/node-fallbacks/node_modules +sign.json +release/ +*.dmg +sign.*.json +packages/debug-* +packages/bun-cli/postinstall.js +packages/bun-*/bun +packages/bun-*/bun-profile +packages/bun-*/debug-bun +packages/bun-*/*.o +packages/bun-cli/postinstall.js + +packages/bun-cli/bin/* +bun-test-scratch +misctools/fetch + +src/deps/libiconv +src/deps/openssl +src/tests.zig +*.blob +src/deps/s2n-tls +.npm +.npm.gz + +bun-binary + +src/deps/PLCrashReporter/ + +*.dSYM +*.crash +misctools/sha +packages/bun-wasm/*.mjs +packages/bun-wasm/*.cjs +packages/bun-wasm/*.map +packages/bun-wasm/*.js +packages/bun-wasm/*.d.ts +packages/bun-wasm/*.d.cts +packages/bun-wasm/*.d.mts +*.bc + +src/fallback.version +src/runtime.version +*.sqlite +*.database +*.db +misctools/machbench +*.big +.eslintcache + +/bun-webkit + +src/deps/c-ares/build +src/bun.js/bindings-obj +src/bun.js/debug-bindings-obj + +failing-tests.txt +test.txt +myscript.sh + +cold-jsc-start +cold-jsc-start.d + +/testdir +/test.ts +/test.js + +src/js/out/modules* +src/js/out/functions* +src/js/out/tmp +src/js/out/DebugPath.h + +make-dev-stats.csv + +.uuid +tsconfig.tsbuildinfo + +test/js/bun/glob/fixtures +*.lib +*.pdb +CMakeFiles +build.ninja +.ninja_deps +.ninja_log +CMakeCache.txt +cmake_install.cmake +compile_commands.json + +*.lib +x64 +**/*.vcxproj* +**/*.sln* +**/*.dir +**/*.pdb + +/.webkit-cache +/.cache +/src/deps/libuv +/build-*/ + +.vs + +**/.verdaccio-db.json +/test-report.md /test-report.json \ No newline at end of file diff --git a/src/__global.zig b/src/__global.zig index fcb03d76d7..4c20917964 100644 --- a/src/__global.zig +++ b/src/__global.zig @@ -69,6 +69,8 @@ pub fn setThreadName(name: StringTypes.stringZ) void { _ = std.os.prctl(.SET_NAME, .{@intFromPtr(name.ptr)}) catch 0; } else if (Environment.isMac) { _ = std.c.pthread_setname_np(name); + } else if (Environment.isWindows) { + // _ = std.os.SetThreadDescription(std.os.GetCurrentThread(), name); } } diff --git a/src/bun.js/event_loop.zig b/src/bun.js/event_loop.zig index fee9a9f27d..0430892028 100644 --- a/src/bun.js/event_loop.zig +++ b/src/bun.js/event_loop.zig @@ -1178,14 +1178,10 @@ pub const EventLoop = struct { } if (!loop.isActive()) { - if (comptime Environment.isWindows) { - bun.todo(@src(), {}); - } else { - if (this.forever_timer == null) { - var t = uws.Timer.create(loop, this); - t.set(this, &noopForeverTimer, 1000 * 60 * 4, 1000 * 60 * 4); - this.forever_timer = t; - } + if (this.forever_timer == null) { + var t = uws.Timer.create(loop, this); + t.set(this, &noopForeverTimer, 1000 * 60 * 4, 1000 * 60 * 4); + this.forever_timer = t; } } diff --git a/src/bun.js/javascript.zig b/src/bun.js/javascript.zig index d06d8b0701..85be94f813 100644 --- a/src/bun.js/javascript.zig +++ b/src/bun.js/javascript.zig @@ -83,6 +83,7 @@ const PendingResolution = @import("../resolver/resolver.zig").PendingResolution; const ThreadSafeFunction = JSC.napi.ThreadSafeFunction; const PackageManager = @import("../install/install.zig").PackageManager; const IPC = @import("ipc.zig"); +pub const GenericWatcher = @import("../watcher.zig"); const ModuleLoader = JSC.ModuleLoader; const FetchFlags = JSC.FetchFlags; @@ -430,22 +431,22 @@ pub const ImportWatcher = union(enum) { pub fn start(this: ImportWatcher) !void { switch (this) { - inline .hot => |watcher| try watcher.start(), - inline .watch => |watcher| try watcher.start(), + inline .hot => |w| try w.start(), + inline .watch => |w| try w.start(), else => {}, } } - pub inline fn watchlist(this: ImportWatcher) Watcher.WatchListArray { + pub inline fn watchlist(this: ImportWatcher) GenericWatcher.WatchList { return switch (this) { - inline .hot, .watch => |wacher| wacher.watchlist, + inline .hot, .watch => |w| w.watchlist, else => .{}, }; } - pub inline fn indexOf(this: ImportWatcher, hash: Watcher.HashType) ?u32 { + pub inline fn indexOf(this: ImportWatcher, hash: GenericWatcher.HashType) ?u32 { return switch (this) { - inline .hot, .watch => |wacher| wacher.indexOf(hash), + inline .hot, .watch => |w| w.indexOf(hash), else => null, }; } @@ -454,7 +455,7 @@ pub const ImportWatcher = union(enum) { this: ImportWatcher, fd: StoredFileDescriptorType, file_path: string, - hash: Watcher.HashType, + hash: GenericWatcher.HashType, loader: options.Loader, dir_fd: StoredFileDescriptorType, package_json: ?*PackageJSON, @@ -2142,7 +2143,7 @@ pub const VirtualMachine = struct { pub fn reloadEntryPoint(this: *VirtualMachine, entry_path: []const u8) !*JSInternalPromise { this.has_loaded = false; this.main = entry_path; - this.main_hash = bun.JSC.Watcher.getHash(entry_path); + this.main_hash = GenericWatcher.getHash(entry_path); try this.entry_point.generate( this.allocator, @@ -2180,7 +2181,7 @@ pub const VirtualMachine = struct { pub fn reloadEntryPointForTestRunner(this: *VirtualMachine, entry_path: []const u8) !*JSInternalPromise { this.has_loaded = false; this.main = entry_path; - this.main_hash = bun.JSC.Watcher.getHash(entry_path); + this.main_hash = GenericWatcher.getHash(entry_path); this.eventLoop().ensureWaker(); @@ -3074,11 +3075,10 @@ extern fn BunDebugger__willHotReload() void; pub fn NewHotReloader(comptime Ctx: type, comptime EventLoopType: type, comptime reload_immediately: bool) type { return struct { - const watcher = @import("../watcher.zig"); - pub const Watcher = watcher.NewWatcher(*@This()); + pub const Watcher = GenericWatcher.NewWatcher(*@This()); const Reloader = @This(); - onAccept: std.ArrayHashMapUnmanaged(@This().Watcher.HashType, bun.BabyList(OnAcceptCallback), bun.ArrayIdentityContext, false) = .{}, + onAccept: std.ArrayHashMapUnmanaged(GenericWatcher.HashType, bun.BabyList(OnAcceptCallback), bun.ArrayIdentityContext, false) = .{}, ctx: *Ctx, verbose: bool = false, @@ -3217,7 +3217,7 @@ pub fn NewHotReloader(comptime Ctx: type, comptime EventLoopType: type, comptime // - Directories outside the root directory // - Directories inside node_modules if (std.mem.indexOf(u8, file_path, "node_modules") == null and std.mem.indexOf(u8, file_path, watch.fs.top_level_dir) != null) { - watch.addDirectory(dir_fd, file_path, @This().Watcher.getHash(file_path), false) catch {}; + watch.addDirectory(dir_fd, file_path, GenericWatcher.getHash(file_path), false) catch {}; } } @@ -3250,9 +3250,9 @@ pub fn NewHotReloader(comptime Ctx: type, comptime EventLoopType: type, comptime pub fn onFileUpdate( this: *@This(), - events: []watcher.WatchEvent, + events: []GenericWatcher.WatchEvent, changed_files: []?[:0]u8, - watchlist: watcher.Watchlist, + watchlist: GenericWatcher.WatchList, ) void { var slice = watchlist.slice(); const file_paths = slice.items(.file_path); @@ -3314,6 +3314,13 @@ pub fn NewHotReloader(comptime Ctx: type, comptime EventLoopType: type, comptime } }, .directory => { + if (comptime Environment.isWindows) { + // on windows we receive file events for all items affected by a directory change + // so we only need to clear the directory cache. all other effects will be handled + // by the file events + resolver.bustDirCache(file_path); + continue; + } var affected_buf: [128][]const u8 = undefined; var entries_option: ?*Fs.FileSystem.RealFS.EntriesOption = null; @@ -3364,7 +3371,7 @@ pub fn NewHotReloader(comptime Ctx: type, comptime EventLoopType: type, comptime resolver.bustDirCache(file_path); if (entries_option) |dir_ent| { - var last_file_hash: @This().Watcher.HashType = std.math.maxInt(@This().Watcher.HashType); + var last_file_hash: GenericWatcher.HashType = std.math.maxInt(GenericWatcher.HashType); for (affected) |changed_name_| { const changed_name: []const u8 = if (comptime Environment.isMac) @@ -3377,14 +3384,14 @@ pub fn NewHotReloader(comptime Ctx: type, comptime EventLoopType: type, comptime var prev_entry_id: usize = std.math.maxInt(usize); if (loader != .file) { var path_string: bun.PathString = undefined; - var file_hash: @This().Watcher.HashType = last_file_hash; + var file_hash: GenericWatcher.HashType = last_file_hash; const abs_path: string = brk: { if (dir_ent.entries.get(@as([]const u8, @ptrCast(changed_name)))) |file_ent| { // reset the file descriptor file_ent.entry.cache.fd = .zero; file_ent.entry.need_stat = true; path_string = file_ent.entry.abs_path; - file_hash = @This().Watcher.getHash(path_string.slice()); + file_hash = GenericWatcher.getHash(path_string.slice()); for (hashes, 0..) |hash, entry_id| { if (hash == file_hash) { if (file_descriptors[entry_id] != .zero) { @@ -3412,7 +3419,7 @@ pub fn NewHotReloader(comptime Ctx: type, comptime EventLoopType: type, comptime @memcpy(_on_file_update_path_buf[file_path_without_trailing_slash.len..][0..changed_name.len], changed_name); const path_slice = _on_file_update_path_buf[0 .. file_path_without_trailing_slash.len + changed_name.len + 1]; - file_hash = @This().Watcher.getHash(path_slice); + file_hash = GenericWatcher.getHash(path_slice); break :brk path_slice; } }; diff --git a/src/bun.js/module_loader.zig b/src/bun.js/module_loader.zig index 9ffc358cfe..e1f7f21d56 100644 --- a/src/bun.js/module_loader.zig +++ b/src/bun.js/module_loader.zig @@ -401,7 +401,7 @@ pub const RuntimeTranspilerStore = struct { var fd: ?StoredFileDescriptorType = null; var package_json: ?*PackageJSON = null; - const hash = JSC.Watcher.getHash(path.text); + const hash = JSC.GenericWatcher.getHash(path.text); switch (vm.bun_watcher) { .hot, .watch => { @@ -1447,7 +1447,7 @@ pub const ModuleLoader = struct { .js, .jsx, .ts, .tsx, .json, .toml, .text => { jsc_vm.transpiled_count += 1; jsc_vm.bundler.resetStore(); - const hash = JSC.Watcher.getHash(path.text); + const hash = JSC.GenericWatcher.getHash(path.text); const is_main = jsc_vm.main.len == path.text.len and jsc_vm.main_hash == hash and strings.eqlLong(jsc_vm.main, path.text, false); diff --git a/src/bun.js/node/path_watcher.zig b/src/bun.js/node/path_watcher.zig index c2740a753d..63cd1d5f27 100644 --- a/src/bun.js/node/path_watcher.zig +++ b/src/bun.js/node/path_watcher.zig @@ -13,6 +13,7 @@ const StoredFileDescriptorType = bun.StoredFileDescriptorType; const string = bun.string; const JSC = bun.JSC; const VirtualMachine = JSC.VirtualMachine; +const GenericWatcher = @import("../../watcher.zig"); const sync = @import("../../sync.zig"); const Semaphore = sync.Semaphore; @@ -21,7 +22,6 @@ var default_manager_mutex: Mutex = Mutex.init(); var default_manager: ?*PathWatcherManager = null; pub const PathWatcherManager = struct { - const GenericWatcher = @import("../../watcher.zig"); const options = @import("../../options.zig"); pub const Watcher = GenericWatcher.NewWatcher(*PathWatcherManager); const log = Output.scoped(.PathWatcherManager, false); @@ -43,7 +43,7 @@ pub const PathWatcherManager = struct { path: [:0]const u8, dirname: string, refs: u32 = 0, - hash: Watcher.HashType, + hash: GenericWatcher.HashType, }; fn refPendingTask(this: *PathWatcherManager) bool { @@ -96,7 +96,7 @@ pub const PathWatcherManager = struct { .is_file = false, .path = cloned_path, .dirname = cloned_path, - .hash = Watcher.getHash(cloned_path), + .hash = GenericWatcher.getHash(cloned_path), .refs = 1, }; _ = try this.file_paths.put(cloned_path, result); @@ -110,7 +110,7 @@ pub const PathWatcherManager = struct { .path = cloned_path, // if is really a file we need to get the dirname .dirname = std.fs.path.dirname(cloned_path) orelse cloned_path, - .hash = Watcher.getHash(cloned_path), + .hash = GenericWatcher.getHash(cloned_path), .refs = 1, }; _ = try this.file_paths.put(cloned_path, result); @@ -154,7 +154,7 @@ pub const PathWatcherManager = struct { this: *PathWatcherManager, events: []GenericWatcher.WatchEvent, changed_files: []?[:0]u8, - watchlist: GenericWatcher.Watchlist, + watchlist: GenericWatcher.WatchList, ) void { var slice = watchlist.slice(); const file_paths = slice.items(.file_path); @@ -197,7 +197,7 @@ pub const PathWatcherManager = struct { if (event.op.write or event.op.delete or event.op.rename) { const event_type: PathWatcher.EventType = if (event.op.delete or event.op.rename or event.op.move_to) .rename else .change; - const hash = Watcher.getHash(file_path); + const hash = GenericWatcher.getHash(file_path); for (watchers) |w| { if (w) |watcher| { @@ -268,7 +268,7 @@ pub const PathWatcherManager = struct { const len = file_path_without_trailing_slash.len + changed_name.len; const path_slice = _on_file_update_path_buf[0 .. len + 1]; - const hash = Watcher.getHash(path_slice); + const hash = GenericWatcher.getHash(path_slice); // skip consecutive duplicates const event_type: PathWatcher.EventType = .rename; // renaming folders, creating folder or files will be always be rename @@ -688,7 +688,7 @@ pub const PathWatcher = struct { has_pending_directories: std.atomic.Value(bool) = std.atomic.Value(bool).init(false), closed: std.atomic.Value(bool) = std.atomic.Value(bool).init(false), pub const ChangeEvent = struct { - hash: PathWatcherManager.Watcher.HashType = 0, + hash: GenericWatcher.HashType = 0, event_type: EventType = .change, time_stamp: i64 = 0, }; @@ -805,7 +805,7 @@ pub const PathWatcher = struct { } } - pub fn emit(this: *PathWatcher, path: string, hash: PathWatcherManager.Watcher.HashType, time_stamp: i64, is_file: bool, event_type: EventType) void { + pub fn emit(this: *PathWatcher, path: string, hash: GenericWatcher.HashType, time_stamp: i64, is_file: bool, event_type: EventType) void { const time_diff = time_stamp - this.last_change_event.time_stamp; // skip consecutive duplicates if ((this.last_change_event.time_stamp == 0 or time_diff > 1) or this.last_change_event.event_type != event_type and this.last_change_event.hash != hash) { diff --git a/src/bun.js/node/win_watcher.zig b/src/bun.js/node/win_watcher.zig index c94608d3ef..bcff289446 100644 --- a/src/bun.js/node/win_watcher.zig +++ b/src/bun.js/node/win_watcher.zig @@ -9,17 +9,15 @@ const JSC = bun.JSC; const VirtualMachine = JSC.VirtualMachine; const StoredFileDescriptorType = bun.StoredFileDescriptorType; const Output = bun.Output; +const Watcher = @import("../../watcher.zig"); var default_manager: ?*PathWatcherManager = null; // TODO: make this a generic so we can reuse code with path_watcher // TODO: we probably should use native instead of libuv abstraction here for better performance pub const PathWatcherManager = struct { - const GenericWatcher = @import("../../watcher.zig"); const options = @import("../../options.zig"); - pub const Watcher = GenericWatcher.NewWatcher(*PathWatcherManager); const log = Output.scoped(.PathWatcherManager, false); - main_watcher: *Watcher, watchers: bun.BabyList(?*PathWatcher) = .{}, watcher_count: u32 = 0, @@ -85,55 +83,31 @@ pub const PathWatcherManager = struct { var this = PathWatcherManager.new(.{ .file_paths = bun.StringHashMap(PathInfo).init(bun.default_allocator), .watchers = watchers, - .main_watcher = undefined, .vm = vm, .watcher_count = 0, }); errdefer this.destroy(); - this.main_watcher = try Watcher.init( - this, - vm.bundler.fs, - bun.default_allocator, - ); - - errdefer this.main_watcher.deinit(false); - - try this.main_watcher.start(); return this; } - fn _addDirectory(this: *PathWatcherManager, _: *PathWatcher, path: PathInfo) !void { - const fd = path.fd; - try this.main_watcher.addDirectory(fd, path.path, path.hash, false); - } - fn registerWatcher(this: *PathWatcherManager, watcher: *PathWatcher) !void { - { - if (this.watcher_count == this.watchers.len) { - this.watcher_count += 1; - this.watchers.push(bun.default_allocator, watcher) catch |err| { - this.watcher_count -= 1; - return err; - }; - } else { - var watchers = this.watchers.slice(); - for (watchers, 0..) |w, i| { - if (w == null) { - watchers[i] = watcher; - this.watcher_count += 1; - break; - } + if (this.watcher_count == this.watchers.len) { + this.watcher_count += 1; + this.watchers.push(bun.default_allocator, watcher) catch |err| { + this.watcher_count -= 1; + return err; + }; + } else { + var watchers = this.watchers.slice(); + for (watchers, 0..) |w, i| { + if (w == null) { + watchers[i] = watcher; + this.watcher_count += 1; + break; } } } - - const path = watcher.path; - if (path.is_file) { - try this.main_watcher.addFile(path.fd, path.path, path.hash, options.Loader.file, .zero, null, false); - } else { - try this._addDirectory(watcher, path); - } } fn _incrementPathRef(this: *PathWatcherManager, file_path: [:0]const u8) void { @@ -152,7 +126,6 @@ pub const PathWatcherManager = struct { path.refs -= 1; if (path.refs == 0) { const path_ = path.path; - this.main_watcher.remove(path.hash); _ = this.file_paths.remove(path_); bun.default_allocator.free(path_); } @@ -198,8 +171,6 @@ pub const PathWatcherManager = struct { return; } - this.main_watcher.deinit(false); - if (this.watcher_count > 0) { while (this.watchers.popOrNull()) |watcher| { if (watcher) |w| { @@ -242,7 +213,7 @@ pub const PathWatcher = struct { const log = Output.scoped(.PathWatcher, false); pub const ChangeEvent = struct { - hash: PathWatcherManager.Watcher.HashType = 0, + hash: Watcher.HashType = 0, event_type: EventType = .change, time_stamp: i64 = 0, }; @@ -330,7 +301,7 @@ pub const PathWatcher = struct { return this; } - pub fn emit(this: *PathWatcher, path: string, hash: PathWatcherManager.Watcher.HashType, time_stamp: i64, is_file: bool, event_type: EventType) void { + pub fn emit(this: *PathWatcher, path: string, hash: Watcher.HashType, time_stamp: i64, is_file: bool, event_type: EventType) void { const time_diff = time_stamp - this.last_change_event.time_stamp; // skip consecutive duplicates if ((this.last_change_event.time_stamp == 0 or time_diff > 1) or this.last_change_event.event_type != event_type and this.last_change_event.hash != hash) { diff --git a/src/bun.zig b/src/bun.zig index d819465324..353c466b62 100644 --- a/src/bun.zig +++ b/src/bun.zig @@ -1397,9 +1397,31 @@ pub const failing_allocator = std.mem.Allocator{ .ptr = undefined, .vtable = &.{ pub fn reloadProcess( allocator: std.mem.Allocator, clear_terminal: bool, -) void { - const PosixSpawn = posix.spawn; +) noreturn { + if (clear_terminal) { + Output.flush(); + Output.disableBuffering(); + Output.resetTerminalAll(); + } const bun = @This(); + + if (comptime Environment.isWindows) { + // this assumes that our parent process assigned us to a job object (see runWatcherManager) + var procinfo: std.os.windows.PROCESS_INFORMATION = undefined; + win32.spawnProcessCopy(allocator, &procinfo, false, false) catch |err| { + Output.panic("Error while reloading process: {s}", .{@errorName(err)}); + }; + + // terminate the current process + const rc = bun.windows.TerminateProcess(@ptrFromInt(std.math.maxInt(usize)), 0); + if (rc == 0) { + const err = bun.windows.GetLastError(); + Output.panic("Error while reloading process: {s}", .{@tagName(err)}); + } else { + Output.panic("Unexpected error while reloading process\n", .{}); + } + } + const PosixSpawn = posix.spawn; const dupe_argv = allocator.allocSentinel(?[*:0]const u8, bun.argv().len, null) catch unreachable; for (bun.argv(), dupe_argv) |src, *dest| { dest.* = (allocator.dupeZ(u8, src) catch unreachable).ptr; @@ -1424,13 +1446,6 @@ pub fn reloadProcess( // we clone envp so that the memory address of environment variables isn't the same as the libc one const envp = @as([*:null]?[*:0]const u8, @ptrCast(environ.ptr)); - // Clear the terminal - if (clear_terminal) { - Output.flush(); - Output.disableBuffering(); - Output.resetTerminalAll(); - } - // macOS doesn't have CLOEXEC, so we must go through posix_spawn if (comptime Environment.isMac) { var actions = PosixSpawn.Actions.init() catch unreachable; @@ -1454,7 +1469,9 @@ pub fn reloadProcess( .err => |err| { Output.panic("Unexpected error while reloading: {d} {s}", .{ err.errno, @tagName(err.getErrno()) }); }, - .result => |_| {}, + .result => |_| { + Output.panic("Unexpected error while reloading: posix_spawn returned a result", .{}); + }, } } else if (comptime Environment.isPosix) { const on_before_reload_process_linux = struct { @@ -1468,10 +1485,8 @@ pub fn reloadProcess( envp, ); Output.panic("Unexpected error while reloading: {s}", .{@errorName(err)}); - } else if (comptime Environment.isWindows) { - @panic("TODO on Windows!"); } else { - @panic("Unsupported platform"); + @compileError("unsupported platform for reloadProcess"); } } pub var auto_reload_on_crash = false; @@ -1879,10 +1894,13 @@ pub const posix = struct { }; pub const win32 = struct { + const w = std.os.windows; pub var STDOUT_FD: FileDescriptor = undefined; pub var STDERR_FD: FileDescriptor = undefined; pub var STDIN_FD: FileDescriptor = undefined; + const watcherChildEnv: [:0]const u16 = strings.toUTF16LiteralZ("_BUN_WATCHER_CHILD"); + pub fn stdio(i: anytype) FileDescriptor { return switch (i) { 0 => STDIN_FD, @@ -1891,6 +1909,158 @@ pub const win32 = struct { else => @panic("Invalid stdio fd"), }; } + + pub fn isWatcherChild() bool { + var buf: [1]u16 = undefined; + return windows.GetEnvironmentVariableW(@constCast(watcherChildEnv.ptr), &buf, 1) > 0; + } + + pub fn becomeWatcherManager(allocator: std.mem.Allocator) noreturn { + // this process will be the parent of the child process that actually runs the script + // based on https://devblogs.microsoft.com/oldnewthing/20130405-00/?p=4743 + const job = windows.CreateJobObjectA(null, null); + const iocp = windows.CreateIoCompletionPort(windows.INVALID_HANDLE_VALUE, null, 0, 1) orelse { + Output.panic("Failed to create IOCP\n", .{}); + }; + var assoc = windows.JOBOBJECT_ASSOCIATE_COMPLETION_PORT{ + .CompletionKey = job, + .CompletionPort = iocp, + }; + if (windows.SetInformationJobObject(job, windows.JobObjectAssociateCompletionPortInformation, &assoc, @sizeOf(windows.JOBOBJECT_ASSOCIATE_COMPLETION_PORT)) == 0) { + const err = windows.GetLastError(); + Output.panic("Failed to associate completion port: {s}\n", .{@tagName(err)}); + } + + var procinfo: std.os.windows.PROCESS_INFORMATION = undefined; + spawnProcessCopy(allocator, &procinfo, true, true) catch |err| { + Output.panic("Failed to spawn process: {s}\n", .{@errorName(err)}); + }; + if (windows.AssignProcessToJobObject(job, procinfo.hProcess) == 0) { + const err = windows.GetLastError(); + Output.panic("Failed to assign process to job object: {s}\n", .{@tagName(err)}); + } + if (windows.ResumeThread(procinfo.hThread) == 0) { + const err = windows.GetLastError(); + Output.panic("Failed to resume child process: {s}\n", .{@tagName(err)}); + } + + var completion_code: w.DWORD = 0; + var completion_key: w.ULONG_PTR = 0; + var overlapped: ?*w.OVERLAPPED = null; + var last_pid: w.DWORD = 0; + while (true) { + if (w.kernel32.GetQueuedCompletionStatus(iocp, &completion_code, &completion_key, &overlapped, w.INFINITE) == 0) { + const err = windows.GetLastError(); + Output.panic("Failed to query completion status: {s}\n", .{@tagName(err)}); + } + // only care about events concerning our job object (theoretically unnecessary) + if (completion_key != @intFromPtr(job)) { + continue; + } + if (completion_code == windows.JOB_OBJECT_MSG_EXIT_PROCESS) { + last_pid = @truncate(@intFromPtr(overlapped)); + } else if (completion_code == windows.JOB_OBJECT_MSG_ACTIVE_PROCESS_ZERO) { + break; + } + } + // NOTE: for now we always exit with a zero exit code. + // This is because there's no straightforward way to communicate the exit code + // of subsequently spawned child processes to the original parent process. + Global.exit(0); + } + + pub fn spawnProcessCopy( + allocator: std.mem.Allocator, + procinfo: *std.os.windows.PROCESS_INFORMATION, + suspended: bool, + setChild: bool, + ) !void { + var flags: std.os.windows.DWORD = w.CREATE_UNICODE_ENVIRONMENT; + if (suspended) { + // see CREATE_SUSPENDED at + // https://learn.microsoft.com/en-us/windows/win32/procthread/process-creation-flags + flags |= 0x00000004; + } + + const image_path = &w.peb().ProcessParameters.ImagePathName; + var wbuf: WPathBuffer = undefined; + @memcpy(wbuf[0..image_path.Length], image_path.Buffer); + wbuf[image_path.Length] = 0; + + const image_pathZ = wbuf[0..image_path.Length :0]; + + const kernelenv = w.kernel32.GetEnvironmentStringsW(); + var newenv: ?[]u16 = null; + defer { + if (kernelenv) |envptr| { + _ = w.kernel32.FreeEnvironmentStringsW(envptr); + } + if (newenv) |ptr| { + allocator.free(ptr); + } + } + + if (setChild) { + var size: usize = 0; + if (kernelenv) |ptr| { + // check that env is non-empty + if (ptr[0] != 0 or ptr[1] != 0) { + // array is terminated by two nulls + while (ptr[size] != 0 or ptr[size + 1] != 0) size += 1; + size += 1; + } + } + // now ptr[size] is the first null + const buf = try allocator.alloc(u16, size + watcherChildEnv.len + 4); + if (kernelenv) |ptr| { + @memcpy(buf[0..size], ptr); + } + @memcpy(buf[size .. size + watcherChildEnv.len], watcherChildEnv); + buf[size + watcherChildEnv.len] = '='; + buf[size + watcherChildEnv.len + 1] = '1'; + buf[size + watcherChildEnv.len + 2] = 0; + buf[size + watcherChildEnv.len + 3] = 0; + newenv = buf; + } + + const env: ?[*]u16 = if (newenv) |e| e.ptr else kernelenv; + + var startupinfo = w.STARTUPINFOW{ + .cb = @sizeOf(w.STARTUPINFOW), + .lpReserved = null, + .lpDesktop = null, + .lpTitle = null, + .dwX = 0, + .dwY = 0, + .dwXSize = 0, + .dwYSize = 0, + .dwXCountChars = 0, + .dwYCountChars = 0, + .dwFillAttribute = 0, + .dwFlags = w.STARTF_USESTDHANDLES, + .wShowWindow = 0, + .cbReserved2 = 0, + .lpReserved2 = null, + .hStdInput = std.io.getStdIn().handle, + .hStdOutput = std.io.getStdOut().handle, + .hStdError = std.io.getStdErr().handle, + }; + const rc = w.kernel32.CreateProcessW( + image_pathZ, + w.kernel32.GetCommandLineW(), + null, + null, + 1, + flags, + env, + null, + &startupinfo, + procinfo, + ); + if (rc == 0) { + Output.panic("Unexpected error while reloading process\n", .{}); + } + } }; pub usingnamespace if (@import("builtin").target.os.tag != .windows) posix else win32; diff --git a/src/cli.zig b/src/cli.zig index 12e726d3e6..0bb6dbf966 100644 --- a/src/cli.zig +++ b/src/cli.zig @@ -1137,6 +1137,14 @@ pub const Command = struct { if (comptime Command.Tag.uses_global_options.get(command)) { ctx.args = try Arguments.parse(allocator, &ctx, command); } + + if (comptime Environment.isWindows) { + if (ctx.debug.hot_reload == .watch and !bun.isWatcherChild()) { + // this is noreturn + bun.becomeWatcherManager(allocator); + } + } + return ctx; } }; diff --git a/src/resolver/resolve_path.zig b/src/resolver/resolve_path.zig index f8859c3172..d644b3ad9e 100644 --- a/src/resolver/resolve_path.zig +++ b/src/resolver/resolve_path.zig @@ -48,6 +48,23 @@ inline fn @"is ../"(slice: []const u8) bool { return strings.hasPrefixComptime(slice, "../"); } +const ParentEqual = enum { + parent, + equal, + unrelated, +}; + +pub fn isParentOrEqual(parent_: []const u8, child: []const u8) ParentEqual { + var parent = parent_; + while (parent.len > 0 and isSepAny(parent[parent.len - 1])) { + parent = parent[0 .. parent.len - 1]; + } + if (std.mem.indexOf(u8, child, parent) != 0) return .unrelated; + if (child.len == parent.len) return .equal; + if (isSepAny(child[parent.len])) return .parent; + return .unrelated; +} + pub fn getIfExistsLongestCommonPathGeneric(input: []const []const u8, comptime platform: Platform) ?[]const u8 { const separator = comptime platform.separator(); const isPathSeparator = comptime platform.getSeparatorFunc(); diff --git a/src/string_immutable.zig b/src/string_immutable.zig index 166b49a378..c51436b00c 100644 --- a/src/string_immutable.zig +++ b/src/string_immutable.zig @@ -54,6 +54,22 @@ pub fn toUTF16Literal(comptime str: []const u8) []const u16 { }; } +pub fn toUTF16LiteralZ(comptime str: []const u8) [:0]const u16 { + return comptime brk: { + comptime var output: [str.len + 1]u16 = undefined; + + for (str, 0..) |c, i| { + output[i] = c; + } + output[str.len] = 0; + + const Static = struct { + pub const literal: [:0]const u16 = output[0..str.len :0]; + }; + break :brk Static.literal; + }; +} + pub const OptionalUsize = std.meta.Int(.unsigned, @bitSizeOf(usize) - 1); pub fn indexOfAny(slice: string, comptime str: anytype) ?OptionalUsize { switch (comptime str.len) { diff --git a/src/watcher.zig b/src/watcher.zig index 55d160ae9e..360a102895 100644 --- a/src/watcher.zig +++ b/src/watcher.zig @@ -1,4 +1,3 @@ -const Fs = @import("./fs.zig"); const std = @import("std"); const bun = @import("root").bun; const string = bun.string; @@ -6,59 +5,31 @@ const Output = bun.Output; const Global = bun.Global; const Environment = bun.Environment; const strings = bun.strings; -const MutableString = bun.MutableString; const stringZ = bun.stringZ; -const StoredFileDescriptorType = bun.StoredFileDescriptorType; const FeatureFlags = bun.FeatureFlags; -const default_allocator = bun.default_allocator; -const C = bun.C; -const c = std.c; const options = @import("./options.zig"); -const IndexType = @import("./allocators.zig").IndexType; - -const os = std.os; const Mutex = @import("./lock.zig").Lock; const Futex = @import("./futex.zig"); pub const WatchItemIndex = u16; -const NoWatchItem: WatchItemIndex = std.math.maxInt(WatchItemIndex); const PackageJSON = @import("./resolver/package_json.zig").PackageJSON; +const log = bun.Output.scoped(.watcher, false); + const WATCHER_MAX_LIST = 8096; -pub const INotify = struct { - pub const IN_CLOEXEC = std.os.O.CLOEXEC; - pub const IN_NONBLOCK = std.os.O.NONBLOCK; +const INotify = struct { + loaded_inotify: bool = false, + inotify_fd: EventListIndex = 0, - pub const IN_ACCESS = 0x00000001; - pub const IN_MODIFY = 0x00000002; - pub const IN_ATTRIB = 0x00000004; - pub const IN_CLOSE_WRITE = 0x00000008; - pub const IN_CLOSE_NOWRITE = 0x00000010; - pub const IN_CLOSE = IN_CLOSE_WRITE | IN_CLOSE_NOWRITE; - pub const IN_OPEN = 0x00000020; - pub const IN_MOVED_FROM = 0x00000040; - pub const IN_MOVED_TO = 0x00000080; - pub const IN_MOVE = IN_MOVED_FROM | IN_MOVED_TO; - pub const IN_CREATE = 0x00000100; - pub const IN_DELETE = 0x00000200; - pub const IN_DELETE_SELF = 0x00000400; - pub const IN_MOVE_SELF = 0x00000800; - pub const IN_ALL_EVENTS = 0x00000fff; + eventlist: EventListBuffer = undefined, + eventlist_ptrs: [128]*const INotifyEvent = undefined, - pub const IN_UNMOUNT = 0x00002000; - pub const IN_Q_OVERFLOW = 0x00004000; - pub const IN_IGNORED = 0x00008000; - - pub const IN_ONLYDIR = 0x01000000; - pub const IN_DONT_FOLLOW = 0x02000000; - pub const IN_EXCL_UNLINK = 0x04000000; - pub const IN_MASK_ADD = 0x20000000; - - pub const IN_ISDIR = 0x40000000; - pub const IN_ONESHOT = 0x80000000; + watch_count: std.atomic.Value(u32) = std.atomic.Value(u32).init(0), + coalesce_interval: isize = 100_000, pub const EventListIndex = c_int; + const EventListBuffer = [@sizeOf([128]INotifyEvent) + (128 * bun.MAX_PATH_BYTES + (128 * @alignOf(INotifyEvent)))]u8; pub const INotifyEvent = extern struct { watch_descriptor: c_int, @@ -76,62 +47,48 @@ pub const INotify = struct { return bun.sliceTo(@as([*:0]u8, @ptrFromInt(@intFromPtr(&this.name_len) + @sizeOf(u32))), 0)[0.. :0]; } }; - pub var inotify_fd: EventListIndex = 0; - pub var loaded_inotify = false; - const EventListBuffer = [@sizeOf([128]INotifyEvent) + (128 * bun.MAX_PATH_BYTES + (128 * @alignOf(INotifyEvent)))]u8; - var eventlist: EventListBuffer = undefined; - var eventlist_ptrs: [128]*const INotifyEvent = undefined; - - var watch_count: std.atomic.Value(u32) = std.atomic.Value(u32).init(0); - - const watch_file_mask = std.os.linux.IN.EXCL_UNLINK | std.os.linux.IN.MOVE_SELF | std.os.linux.IN.DELETE_SELF | std.os.linux.IN.MOVED_TO | std.os.linux.IN.MODIFY; - const watch_dir_mask = std.os.linux.IN.EXCL_UNLINK | std.os.linux.IN.DELETE | std.os.linux.IN.DELETE_SELF | std.os.linux.IN.CREATE | std.os.linux.IN.MOVE_SELF | std.os.linux.IN.ONLYDIR | std.os.linux.IN.MOVED_TO; - - pub fn watchPath(pathname: [:0]const u8) !EventListIndex { - std.debug.assert(loaded_inotify); - const old_count = watch_count.fetchAdd(1, .Release); - defer if (old_count == 0) Futex.wake(&watch_count, 10); - return std.os.inotify_add_watchZ(inotify_fd, pathname, watch_file_mask); + pub fn watchPath(this: *INotify, pathname: [:0]const u8) !EventListIndex { + std.debug.assert(this.loaded_inotify); + const old_count = this.watch_count.fetchAdd(1, .Release); + defer if (old_count == 0) Futex.wake(&this.watch_count, 10); + const watch_file_mask = std.os.linux.IN.EXCL_UNLINK | std.os.linux.IN.MOVE_SELF | std.os.linux.IN.DELETE_SELF | std.os.linux.IN.MOVED_TO | std.os.linux.IN.MODIFY; + return std.os.inotify_add_watchZ(this.inotify_fd, pathname, watch_file_mask); } - pub fn watchDir(pathname: [:0]const u8) !EventListIndex { - std.debug.assert(loaded_inotify); - const old_count = watch_count.fetchAdd(1, .Release); - defer if (old_count == 0) Futex.wake(&watch_count, 10); - return std.os.inotify_add_watchZ(inotify_fd, pathname, watch_dir_mask); + pub fn watchDir(this: *INotify, pathname: [:0]const u8) !EventListIndex { + std.debug.assert(this.loaded_inotify); + const old_count = this.watch_count.fetchAdd(1, .Release); + defer if (old_count == 0) Futex.wake(&this.watch_count, 10); + const watch_dir_mask = std.os.linux.IN.EXCL_UNLINK | std.os.linux.IN.DELETE | std.os.linux.IN.DELETE_SELF | std.os.linux.IN.CREATE | std.os.linux.IN.MOVE_SELF | std.os.linux.IN.ONLYDIR | std.os.linux.IN.MOVED_TO; + return std.os.inotify_add_watchZ(this.inotify_fd, pathname, watch_dir_mask); } - pub fn unwatch(wd: EventListIndex) void { - std.debug.assert(loaded_inotify); - _ = watch_count.fetchSub(1, .Release); - std.os.inotify_rm_watch(inotify_fd, wd); + pub fn unwatch(this: *INotify, wd: EventListIndex) void { + std.debug.assert(this.loaded_inotify); + _ = this.watch_count.fetchSub(1, .Release); + std.os.inotify_rm_watch(this.inotify_fd, wd); } - pub fn isRunning() bool { - return loaded_inotify; - } - - var coalesce_interval: isize = 100_000; - pub fn init() !void { - std.debug.assert(!loaded_inotify); - loaded_inotify = true; + pub fn init(this: *INotify, _: []const u8) !void { + std.debug.assert(!this.loaded_inotify); + this.loaded_inotify = true; if (bun.getenvZ("BUN_INOTIFY_COALESCE_INTERVAL")) |env| { - coalesce_interval = std.fmt.parseInt(isize, env, 10) catch 100_000; + this.coalesce_interval = std.fmt.parseInt(isize, env, 10) catch 100_000; } - inotify_fd = try std.os.inotify_init1(IN_CLOEXEC); + this.inotify_fd = try std.os.inotify_init1(std.os.linux.IN.CLOEXEC); } - pub fn read() ![]*const INotifyEvent { - std.debug.assert(loaded_inotify); + pub fn read(this: *INotify) ![]*const INotifyEvent { + std.debug.assert(this.loaded_inotify); restart: while (true) { - Futex.wait(&watch_count, 0, null) catch unreachable; + Futex.wait(&this.watch_count, 0, null) catch unreachable; const rc = std.os.system.read( - inotify_fd, - @as([*]u8, @ptrCast(@alignCast(&eventlist))), + this.inotify_fd, + @as([*]u8, @ptrCast(@alignCast(&this.eventlist))), @sizeOf(EventListBuffer), ); @@ -145,16 +102,16 @@ pub const INotify = struct { // we do a 0.1ms sleep to try to coalesce events better if (len < (@sizeOf(EventListBuffer) / 2)) { var fds = [_]std.os.pollfd{.{ - .fd = inotify_fd, + .fd = this.inotify_fd, .events = std.os.POLL.IN | std.os.POLL.ERR, .revents = 0, }}; - var timespec = std.os.timespec{ .tv_sec = 0, .tv_nsec = coalesce_interval }; + var timespec = std.os.timespec{ .tv_sec = 0, .tv_nsec = this.coalesce_interval }; if ((std.os.ppoll(&fds, ×pec, null) catch 0) > 0) { while (true) { const new_rc = std.os.system.read( - inotify_fd, - @as([*]u8, @ptrCast(@alignCast(&eventlist))) + len, + this.inotify_fd, + @as([*]u8, @ptrCast(@alignCast(&this.eventlist))) + len, @sizeOf(EventListBuffer) - len, ); switch (std.os.errno(new_rc)) { @@ -186,14 +143,14 @@ pub const INotify = struct { var i: u32 = 0; while (i < len) : (i += @sizeOf(INotifyEvent)) { @setRuntimeSafety(false); - const event = @as(*INotifyEvent, @ptrCast(@alignCast(eventlist[i..][0..@sizeOf(INotifyEvent)]))); + const event = @as(*INotifyEvent, @ptrCast(@alignCast(this.eventlist[i..][0..@sizeOf(INotifyEvent)]))); i += event.name_len; - eventlist_ptrs[count] = event; + this.eventlist_ptrs[count] = event; count += 1; } - return eventlist_ptrs[0..count]; + return this.eventlist_ptrs[0..count]; }, .AGAIN => continue :restart, .INVAL => return error.ShortRead, @@ -205,10 +162,10 @@ pub const INotify = struct { unreachable; } - pub fn stop() void { - if (inotify_fd != 0) { - _ = bun.sys.close(bun.toFD(inotify_fd)); - inotify_fd = 0; + pub fn stop(this: *INotify) void { + if (this.inotify_fd != 0) { + _ = bun.sys.close(bun.toFD(this.inotify_fd)); + this.inotify_fd = 0; } } }; @@ -217,69 +174,205 @@ const DarwinWatcher = struct { pub const EventListIndex = u32; const KEvent = std.c.Kevent; + // Internal - pub var changelist: [128]KEvent = undefined; + changelist: [128]KEvent = undefined, // Everything being watched - pub var eventlist: [WATCHER_MAX_LIST]KEvent = undefined; - pub var eventlist_index: EventListIndex = 0; + eventlist: [WATCHER_MAX_LIST]KEvent = undefined, + eventlist_index: EventListIndex = 0, - pub var fd: i32 = 0; + fd: i32 = 0, - pub fn init() !void { - std.debug.assert(fd == 0); - - fd = try std.os.kqueue(); - if (fd == 0) return error.KQueueError; + pub fn init(this: *DarwinWatcher, _: []const u8) !void { + this.fd = try std.os.kqueue(); + if (this.fd == 0) return error.KQueueError; } - pub fn isRunning() bool { - return fd != 0; - } - - pub fn stop() void { - if (fd != 0) { - _ = bun.sys.close(fd); + pub fn stop(this: *DarwinWatcher) void { + if (this.fd != 0) { + _ = bun.sys.close(this.fd); } - - fd = 0; + this.fd = 0; } }; -pub const Placeholder = struct { - pub const EventListIndex = u32; +const WindowsWatcher = struct { + mutex: Mutex = Mutex.init(), + iocp: w.HANDLE = undefined, + watcher: DirWatcher = undefined, - pub var eventlist: [WATCHER_MAX_LIST]EventListIndex = undefined; - pub var eventlist_index: EventListIndex = 0; + const w = std.os.windows; + pub const EventListIndex = c_int; - pub fn isRunning() bool { - return true; + const Error = error{ + IocpFailed, + ReadDirectoryChangesFailed, + CreateFileFailed, + InvalidPath, + }; + + const Action = enum(w.DWORD) { + Added = w.FILE_ACTION_ADDED, + Removed = w.FILE_ACTION_REMOVED, + Modified = w.FILE_ACTION_MODIFIED, + RenamedOld = w.FILE_ACTION_RENAMED_OLD_NAME, + RenamedNew = w.FILE_ACTION_RENAMED_NEW_NAME, + }; + + const FileEvent = struct { + action: Action, + filename: []u16 = undefined, + }; + + const DirWatcher = struct { + // must be initialized to zero (even though it's never read or written in our code), + // otherwise ReadDirectoryChangesW will fail with INVALID_HANDLE + overlapped: w.OVERLAPPED = std.mem.zeroes(w.OVERLAPPED), + buf: [64 * 1024]u8 align(@alignOf(w.FILE_NOTIFY_INFORMATION)) = undefined, + dirHandle: w.HANDLE, + + // invalidates any EventIterators + fn prepare(this: *DirWatcher) Error!void { + const filter = w.FILE_NOTIFY_CHANGE_FILE_NAME | w.FILE_NOTIFY_CHANGE_DIR_NAME | w.FILE_NOTIFY_CHANGE_LAST_WRITE | w.FILE_NOTIFY_CHANGE_CREATION; + if (w.kernel32.ReadDirectoryChangesW(this.dirHandle, &this.buf, this.buf.len, 1, filter, null, &this.overlapped, null) == 0) { + const err = w.kernel32.GetLastError(); + log("failed to start watching directory: {s}", .{@tagName(err)}); + return Error.ReadDirectoryChangesFailed; + } + } + }; + + const EventIterator = struct { + watcher: *DirWatcher, + offset: usize = 0, + hasNext: bool = true, + + pub fn next(this: *EventIterator) ?FileEvent { + if (!this.hasNext) return null; + const info_size = @sizeOf(w.FILE_NOTIFY_INFORMATION); + const info: *w.FILE_NOTIFY_INFORMATION = @alignCast(@ptrCast(this.watcher.buf[this.offset..].ptr)); + const name_ptr: [*]u16 = @alignCast(@ptrCast(this.watcher.buf[this.offset + info_size ..])); + const filename: []u16 = name_ptr[0 .. info.FileNameLength / @sizeOf(u16)]; + + const action: Action = @enumFromInt(info.Action); + + if (info.NextEntryOffset == 0) { + this.hasNext = false; + } else { + this.offset += @as(usize, info.NextEntryOffset); + } + + return FileEvent{ + .action = action, + .filename = filename, + }; + } + }; + + pub fn init(this: *WindowsWatcher, root: []const u8) !void { + var pathbuf: bun.WPathBuffer = undefined; + const wpath = bun.strings.toNTPath(&pathbuf, root); + const path_len_bytes: u16 = @truncate(wpath.len * 2); + var nt_name = w.UNICODE_STRING{ + .Length = path_len_bytes, + .MaximumLength = path_len_bytes, + .Buffer = @constCast(wpath.ptr), + }; + var attr = w.OBJECT_ATTRIBUTES{ + .Length = @sizeOf(w.OBJECT_ATTRIBUTES), + .RootDirectory = null, + .Attributes = 0, // Note we do not use OBJ_CASE_INSENSITIVE here. + .ObjectName = &nt_name, + .SecurityDescriptor = null, + .SecurityQualityOfService = null, + }; + var handle: w.HANDLE = w.INVALID_HANDLE_VALUE; + var io: w.IO_STATUS_BLOCK = undefined; + const rc = w.ntdll.NtCreateFile( + &handle, + w.FILE_LIST_DIRECTORY, + &attr, + &io, + null, + 0, + w.FILE_SHARE_READ | w.FILE_SHARE_WRITE | w.FILE_SHARE_DELETE, + w.FILE_OPEN, + w.FILE_DIRECTORY_FILE | w.FILE_OPEN_FOR_BACKUP_INTENT, + null, + 0, + ); + + if (rc != .SUCCESS) { + const err = bun.windows.Win32Error.fromNTStatus(rc); + log("failed to open directory for watching: {s}", .{@tagName(err)}); + return Error.CreateFileFailed; + } + errdefer _ = w.kernel32.CloseHandle(handle); + + this.iocp = try w.CreateIoCompletionPort(handle, null, 0, 1); + errdefer _ = w.kernel32.CloseHandle(this.iocp); + + this.watcher = .{ .dirHandle = handle }; } - pub fn init() !void {} + const Timeout = enum(w.DWORD) { + infinite = w.INFINITE, + minimal = 1, + none = 0, + }; + + // wait until new events are available + pub fn next(this: *WindowsWatcher, timeout: Timeout) !?EventIterator { + try this.watcher.prepare(); + + var nbytes: w.DWORD = 0; + var key: w.ULONG_PTR = 0; + var overlapped: ?*w.OVERLAPPED = null; + while (true) { + const rc = w.kernel32.GetQueuedCompletionStatus(this.iocp, &nbytes, &key, &overlapped, @intFromEnum(timeout)); + if (rc == 0) { + const err = w.kernel32.GetLastError(); + if (err == w.Win32Error.IMEOUT) { + return null; + } else { + log("GetQueuedCompletionStatus failed: {s}", .{@tagName(err)}); + return Error.IocpFailed; + } + } + + if (overlapped) |ptr| { + // ignore possible spurious events + if (ptr != &this.watcher.overlapped) { + continue; + } + if (nbytes == 0) { + // shutdown notification + // TODO close handles? + return Error.IocpFailed; + } + return EventIterator{ .watcher = &this.watcher }; + } else { + log("GetQueuedCompletionStatus returned no overlapped event", .{}); + return Error.IocpFailed; + } + } + } + + pub fn stop(this: *WindowsWatcher) void { + w.CloseHandle(this.watcher.dirHandle); + w.CloseHandle(this.iocp); + } }; const PlatformWatcher = if (Environment.isMac) DarwinWatcher else if (Environment.isLinux) INotify +else if (Environment.isWindows) + WindowsWatcher else - Placeholder; - -pub const WatchItem = struct { - file_path: string, - // filepath hash for quick comparison - hash: u32, - eventlist_index: PlatformWatcher.EventListIndex, - loader: options.Loader, - fd: StoredFileDescriptorType, - count: u32, - parent_hash: u32, - kind: Kind, - package_json: ?*PackageJSON, - - pub const Kind = enum { file, directory }; -}; + @compileError("Unsupported platform"); pub const WatchEvent = struct { index: WatchItemIndex, @@ -332,11 +425,21 @@ pub const WatchEvent = struct { pub fn fromINotify(this: *WatchEvent, event: INotify.INotifyEvent, index: WatchItemIndex) void { this.* = WatchEvent{ .op = Op{ - .delete = (event.mask & INotify.IN_DELETE_SELF) > 0 or (event.mask & INotify.IN_DELETE) > 0, - .metadata = false, - .rename = (event.mask & INotify.IN_MOVE_SELF) > 0, - .move_to = (event.mask & INotify.IN_MOVED_TO) > 0, - .write = (event.mask & INotify.IN_MODIFY) > 0, + .delete = (event.mask & std.os.linux.IN.DELETE_SELF) > 0 or (event.mask & std.os.linux.IN.DELETE) > 0, + .rename = (event.mask & std.os.linux.IN.MOVE_SELF) > 0, + .move_to = (event.mask & std.os.linux.IN.MOVED_TO) > 0, + .write = (event.mask & std.os.linux.IN.MODIFY) > 0, + }, + .index = index, + }; + } + + pub fn fromFileNotify(this: *WatchEvent, event: WindowsWatcher.FileEvent, index: WatchItemIndex) void { + this.* = WatchEvent{ + .op = Op{ + .delete = event.action == .Removed, + .rename = event.action == .RenamedOld, + .write = event.action == .Modified, }, .index = index, }; @@ -351,13 +454,33 @@ pub const WatchEvent = struct { }; }; -pub const Watchlist = std.MultiArrayList(WatchItem); +pub const WatchItem = struct { + file_path: string, + // filepath hash for quick comparison + hash: u32, + loader: options.Loader, + fd: bun.FileDescriptor, + count: u32, + parent_hash: u32, + kind: Kind, + package_json: ?*PackageJSON, + eventlist_index: if (Environment.isLinux) PlatformWatcher.EventListIndex else u0 = 0, + + pub const Kind = enum { file, directory }; +}; + +pub const WatchList = std.MultiArrayList(WatchItem); +pub const HashType = u32; + +pub fn getHash(filepath: string) HashType { + return @as(HashType, @truncate(bun.hash(filepath))); +} pub fn NewWatcher(comptime ContextType: type) type { return struct { const Watcher = @This(); - watchlist: Watchlist, + watchlist: WatchList, watched_count: usize = 0, mutex: Mutex, @@ -365,12 +488,10 @@ pub fn NewWatcher(comptime ContextType: type) type { // User-facing watch_events: [128]WatchEvent = undefined, - changed_filepaths: [128]?[:0]u8 = std.mem.zeroes([128]?[:0]u8), + changed_filepaths: [128]?[:0]u8 = [_]?[:0]u8{null} ** 128, - fs: *Fs.FileSystem, - // this is what kqueue knows about - fd: StoredFileDescriptorType, ctx: ContextType, + fs: *bun.fs.FileSystem, allocator: std.mem.Allocator, watchloop_handle: ?std.Thread.Id = null, cwd: string, @@ -378,42 +499,33 @@ pub fn NewWatcher(comptime ContextType: type) type { running: bool = true, close_descriptors: bool = false, - pub const HashType = u32; - pub const WatchListArray = Watchlist; + evict_list: [WATCHER_MAX_LIST]WatchItemIndex = undefined, + evict_list_i: WatchItemIndex = 0, - var evict_list: [WATCHER_MAX_LIST]WatchItemIndex = undefined; + const no_watch_item: WatchItemIndex = std.math.maxInt(WatchItemIndex); - pub fn getHash(filepath: string) HashType { - return @as(HashType, @truncate(bun.hash(filepath))); - } - - pub fn init(ctx: ContextType, fs: *Fs.FileSystem, allocator: std.mem.Allocator) !*Watcher { + pub fn init(ctx: ContextType, fs: *bun.fs.FileSystem, allocator: std.mem.Allocator) !*Watcher { const watcher = try allocator.create(Watcher); errdefer allocator.destroy(watcher); - if (!PlatformWatcher.isRunning()) { - try PlatformWatcher.init(); - } - watcher.* = Watcher{ .fs = fs, - .fd = .zero, .allocator = allocator, .watched_count = 0, .ctx = ctx, - .watchlist = Watchlist{}, + .watchlist = WatchList{}, .mutex = Mutex.init(), .cwd = fs.top_level_dir, }; + try PlatformWatcher.init(&watcher.platform, fs.top_level_dir); + return watcher; } pub fn start(this: *Watcher) !void { - if (!Environment.isWindows) { - std.debug.assert(this.watchloop_handle == null); - this.thread = try std.Thread.spawn(.{}, Watcher.watchLoop, .{this}); - } + std.debug.assert(this.watchloop_handle == null); + this.thread = try std.Thread.spawn(.{}, Watcher.watchLoop, .{this}); } pub fn deinit(this: *Watcher, close_descriptors: bool) void { @@ -440,10 +552,6 @@ pub fn NewWatcher(comptime ContextType: type) type { // This must only be called from the watcher thread pub fn watchLoop(this: *Watcher) !void { - if (Environment.isWindows) { - @compileError("watchLoop should not be used on Windows"); - } - this.watchloop_handle = std.Thread.getCurrentId(); Output.Source.configureNamedThread("File Watcher"); @@ -452,7 +560,7 @@ pub fn NewWatcher(comptime ContextType: type) type { this._watchLoop() catch |err| { this.watchloop_handle = null; - PlatformWatcher.stop(); + this.platform.stop(); if (this.running) { this.ctx.onError(err); } @@ -471,70 +579,39 @@ pub fn NewWatcher(comptime ContextType: type) type { allocator.destroy(this); } - pub fn remove(this: *Watcher, hash: HashType) void { - this.mutex.lock(); - defer this.mutex.unlock(); - if (this.indexOf(hash)) |index| { - const fds = this.watchlist.items(.fd); - const fd = fds[index]; - _ = bun.sys.close(fd); - this.watchlist.swapRemove(index); - } - } - - var evict_list_i: WatchItemIndex = 0; - - pub fn removeAtIndex(_: *Watcher, index: WatchItemIndex, hash: HashType, parents: []HashType, comptime kind: WatchItem.Kind) void { - std.debug.assert(index != NoWatchItem); - - evict_list[evict_list_i] = index; - evict_list_i += 1; - - if (comptime kind == .directory) { - for (parents) |parent| { - if (parent == hash) { - evict_list[evict_list_i] = @as(WatchItemIndex, @truncate(parent)); - evict_list_i += 1; - } - } - } - } - pub fn flushEvictions(this: *Watcher) void { - if (evict_list_i == 0) return; - defer evict_list_i = 0; + if (this.evict_list_i == 0) return; + defer this.evict_list_i = 0; // swapRemove messes up the order // But, it only messes up the order if any elements in the list appear after the item being removed // So if we just sort the list by the biggest index first, that should be fine std.sort.pdq( WatchItemIndex, - evict_list[0..evict_list_i], + this.evict_list[0..this.evict_list_i], {}, comptime std.sort.desc(WatchItemIndex), ); var slice = this.watchlist.slice(); const fds = slice.items(.fd); - var last_item = NoWatchItem; + var last_item = no_watch_item; - for (evict_list[0..evict_list_i]) |item| { + for (this.evict_list[0..this.evict_list_i]) |item| { // catch duplicates, since the list is sorted, duplicates will appear right after each other if (item == last_item) continue; - // close the file descriptors here. this should automatically remove it from being watched too. - _ = bun.sys.close(fds[item]); - - // if (Environment.isLinux) { - // INotify.unwatch(event_list_ids[item]); - // } - + if (!Environment.isWindows) { + // on mac and linux we can just close the file descriptor + // TODO do we need to call inotify_rm_watch on linux? + _ = bun.sys.close(fds[item]); + } last_item = item; } - last_item = NoWatchItem; + last_item = no_watch_item; // This is split into two passes because reading the slice while modified is potentially unsafe. - for (evict_list[0..evict_list_i]) |item| { + for (this.evict_list[0..this.evict_list_i]) |item| { if (item == last_item) continue; this.watchlist.swapRemove(item); last_item = item; @@ -543,7 +620,7 @@ pub fn NewWatcher(comptime ContextType: type) type { fn _watchLoop(this: *Watcher) !void { if (Environment.isMac) { - std.debug.assert(DarwinWatcher.fd > 0); + std.debug.assert(this.platform.fd > 0); const KEvent = std.c.Kevent; var changelist_array: [128]KEvent = std.mem.zeroes([128]KEvent); @@ -552,7 +629,7 @@ pub fn NewWatcher(comptime ContextType: type) type { defer Output.flush(); var count_ = std.os.system.kevent( - DarwinWatcher.fd, + this.platform.fd, @as([*]KEvent, changelist), 0, @as([*]KEvent, changelist), @@ -566,7 +643,7 @@ pub fn NewWatcher(comptime ContextType: type) type { const remain = 128 - count_; var timespec = std.os.timespec{ .tv_sec = 0, .tv_nsec = 100_000 }; const extra = std.os.system.kevent( - DarwinWatcher.fd, + this.platform.fd, @as([*]KEvent, changelist[@as(usize, @intCast(count_))..].ptr), 0, @as([*]KEvent, changelist[@as(usize, @intCast(count_))..].ptr), @@ -613,7 +690,7 @@ pub fn NewWatcher(comptime ContextType: type) type { restart: while (true) { defer Output.flush(); - var events = try INotify.read(); + var events = try this.platform.read(); if (events.len == 0) continue :restart; // TODO: is this thread safe? @@ -685,50 +762,96 @@ pub fn NewWatcher(comptime ContextType: type) type { } } } else if (Environment.isWindows) { - @compileError("watchLoop should not be used on Windows"); - } - } - - pub fn indexOf(this: *Watcher, hash: HashType) ?u32 { - for (this.watchlist.items(.hash), 0..) |other, i| { - if (hash == other) { - return @as(u32, @truncate(i)); + var buf: bun.PathBuffer = undefined; + const root = this.fs.top_level_dir; + @memcpy(buf[0..root.len], root); + const needs_slash = root.len == 0 or !bun.strings.charIsAnySlash(root[root.len - 1]); + if (needs_slash) { + buf[root.len] = '\\'; } - } - return null; - } + const baseidx = if (needs_slash) root.len + 1 else root.len; + restart: while (true) { + var event_id: usize = 0; - pub fn addFile( - this: *Watcher, - fd: StoredFileDescriptorType, - file_path: string, - hash: HashType, - loader: options.Loader, - dir_fd: StoredFileDescriptorType, - package_json: ?*PackageJSON, - comptime copy_file_path: bool, - ) !void { - // This must lock due to concurrent transpiler - this.mutex.lock(); - defer this.mutex.unlock(); + // first wait has infinite timeout - we're waiting for the next event and don't want to spin + var timeout = WindowsWatcher.Timeout.infinite; + while (true) { + var iter = try this.platform.next(timeout) orelse break; + // after the first wait, we want to start coalescing events, so we wait for a minimal amount of time + timeout = WindowsWatcher.Timeout.minimal; + const item_paths = this.watchlist.items(.file_path); + log("number of watched items: {d}", .{item_paths.len}); + while (iter.next()) |event| { + const convert_res = bun.strings.copyUTF16IntoUTF8(buf[baseidx..], []const u16, event.filename, false); + const eventpath = buf[0 .. baseidx + convert_res.written]; - if (this.indexOf(hash)) |index| { - if (comptime FeatureFlags.atomic_file_watcher) { - // On Linux, the file descriptor might be out of date. - if (fd.int() > 0) { - var fds = this.watchlist.items(.fd); - fds[index] = fd; + log("watcher update event: (filename: {s}, action: {s}", .{ eventpath, @tagName(event.action) }); + + // TODO this probably needs a more sophisticated search algorithm in the future + // Possible approaches: + // - Keep a sorted list of the watched paths and perform a binary search. We could use a bool to keep + // track of whether the list is sorted and only sort it when we detect a change. + // - Use a prefix tree. Potentially more efficient for large numbers of watched paths, but complicated + // to implement and maintain. + // - others that i'm not thinking of + + for (item_paths, 0..) |path_, item_idx| { + var path = path_; + if (path.len > 0 and bun.strings.charIsAnySlash(path[path.len - 1])) { + path = path[0 .. path.len - 1]; + } + // log("checking path: {s}\n", .{path}); + // check if the current change applies to this item + // if so, add it to the eventlist + const rel = bun.path.isParentOrEqual(eventpath, path); + // skip unrelated items + if (rel == .unrelated) continue; + // if the event is for a parent dir of the item, only emit it if it's a delete or rename + if (rel == .parent and (event.action != .Removed or event.action != .RenamedOld)) continue; + this.watch_events[event_id].fromFileNotify(event, @truncate(item_idx)); + event_id += 1; + } + } + } + if (event_id == 0) { + continue :restart; } - } - return; - } - try this.appendFileMaybeLock(fd, file_path, hash, loader, dir_fd, package_json, copy_file_path, false); + // log("event_id: {d}\n", .{event_id}); + + var all_events = this.watch_events[0..event_id]; + std.sort.pdq(WatchEvent, all_events, {}, WatchEvent.sortByIndex); + + var last_event_index: usize = 0; + var last_event_id: INotify.EventListIndex = std.math.maxInt(INotify.EventListIndex); + + for (all_events, 0..) |_, i| { + // if (all_events[i].name_len > 0) { + // this.changed_filepaths[name_off] = temp_name_list[all_events[i].name_off]; + // all_events[i].name_off = name_off; + // name_off += 1; + // } + + if (all_events[i].index == last_event_id) { + all_events[last_event_index].merge(all_events[i]); + continue; + } + last_event_index = i; + last_event_id = all_events[i].index; + } + if (all_events.len == 0) continue :restart; + all_events = all_events[0 .. last_event_index + 1]; + + log("calling onFileUpdate (all_events.len = {d})", .{all_events.len}); + + this.ctx.onFileUpdate(all_events, this.changed_filepaths[0 .. last_event_index + 1], this.watchlist); + } + } } fn appendFileAssumeCapacity( this: *Watcher, - fd: StoredFileDescriptorType, + fd: bun.FileDescriptor, file_path: string, hash: HashType, loader: options.Loader, @@ -736,7 +859,15 @@ pub fn NewWatcher(comptime ContextType: type) type { package_json: ?*PackageJSON, comptime copy_file_path: bool, ) !void { - var index: PlatformWatcher.EventListIndex = std.math.maxInt(PlatformWatcher.EventListIndex); + if (comptime Environment.isWindows) { + // on windows we can only watch items that are in the directory tree of the top level dir + const rel = bun.path.isParentOrEqual(this.fs.top_level_dir, file_path); + if (rel == .unrelated) { + Output.warn("File {s} is not in the project directory and will not be watched\n", .{file_path}); + return; + } + } + const watchlist_id = this.watchlist.len; const file_path_: string = if (comptime copy_file_path) @@ -744,13 +875,24 @@ pub fn NewWatcher(comptime ContextType: type) type { else file_path; + var item = WatchItem{ + .file_path = file_path_, + .fd = fd, + .hash = hash, + .count = 0, + .loader = loader, + .parent_hash = parent_hash, + .package_json = package_json, + .kind = .file, + }; + if (comptime Environment.isMac) { const KEvent = std.c.Kevent; // https://developer.apple.com/library/archive/documentation/System/Conceptual/ManPages_iPhoneOS/man2/kqueue.2.html var event = std.mem.zeroes(KEvent); - event.flags = c.EV_ADD | c.EV_CLEAR | c.EV_ENABLE; + event.flags = std.c.EV_ADD | std.c.EV_CLEAR | std.c.EV_ENABLE; // we want to know about the vnode event.filter = std.c.EVFILT_VNODE; @@ -768,7 +910,7 @@ pub fn NewWatcher(comptime ContextType: type) type { // - We register the event here. // our while(true) loop above receives notification of changes to any of the events created here. _ = std.os.system.kevent( - DarwinWatcher.fd, + this.platform.fd, @as([]KEvent, events[0..1]).ptr, 1, @as([]KEvent, events[0..1]).ptr, @@ -782,37 +924,35 @@ pub fn NewWatcher(comptime ContextType: type) type { // buf[file_path_to_use_.len] = 0; var buf = file_path_.ptr; const slice: [:0]const u8 = buf[0..file_path_.len :0]; - index = try INotify.watchPath(slice); + item.eventlist_index = try this.platform.watchPath(slice); } - this.watchlist.appendAssumeCapacity(.{ - .file_path = file_path_, - .fd = fd, - .hash = hash, - .count = 0, - .eventlist_index = index, - .loader = loader, - .parent_hash = parent_hash, - .package_json = package_json, - .kind = .file, - }); + this.watchlist.appendAssumeCapacity(item); } fn appendDirectoryAssumeCapacity( this: *Watcher, - stored_fd: StoredFileDescriptorType, + stored_fd: bun.FileDescriptor, file_path: string, hash: HashType, comptime copy_file_path: bool, ) !WatchItemIndex { + if (comptime Environment.isWindows) { + // on windows we can only watch items that are in the directory tree of the top level dir + const rel = bun.path.isParentOrEqual(this.fs.top_level_dir, file_path); + if (rel == .unrelated) { + Output.warn("Directory {s} is not in the project directory and will not be watched\n", .{file_path}); + return no_watch_item; + } + } + const fd = brk: { if (stored_fd.int() > 0) break :brk stored_fd; const dir = try std.fs.cwd().openDir(file_path, .{}); break :brk bun.toFD(dir.fd); }; - const parent_hash = Watcher.getHash(Fs.PathName.init(file_path).dirWithTrailingSlash()); - var index: PlatformWatcher.EventListIndex = std.math.maxInt(PlatformWatcher.EventListIndex); + const parent_hash = getHash(bun.fs.PathName.init(file_path).dirWithTrailingSlash()); const file_path_: string = if (comptime copy_file_path) bun.asByteSlice(try this.allocator.dupeZ(u8, file_path)) @@ -821,13 +961,24 @@ pub fn NewWatcher(comptime ContextType: type) type { const watchlist_id = this.watchlist.len; + var item = WatchItem{ + .file_path = file_path_, + .fd = fd, + .hash = hash, + .count = 0, + .loader = options.Loader.file, + .parent_hash = parent_hash, + .kind = .directory, + .package_json = null, + }; + if (Environment.isMac) { const KEvent = std.c.Kevent; // https://developer.apple.com/library/archive/documentation/System/Conceptual/ManPages_iPhoneOS/man2/kqueue.2.html var event = std.mem.zeroes(KEvent); - event.flags = c.EV_ADD | c.EV_CLEAR | c.EV_ENABLE; + event.flags = std.c.EV_ADD | std.c.EV_CLEAR | std.c.EV_ENABLE; // we want to know about the vnode event.filter = std.c.EVFILT_VNODE; @@ -849,7 +1000,7 @@ pub fn NewWatcher(comptime ContextType: type) type { // - We register the event here. // our while(true) loop above receives notification of changes to any of the events created here. _ = std.os.system.kevent( - DarwinWatcher.fd, + this.platform.fd, @as([]KEvent, events[0..1]).ptr, 1, @as([]KEvent, events[0..1]).ptr, @@ -862,53 +1013,22 @@ pub fn NewWatcher(comptime ContextType: type) type { bun.copy(u8, &buf, file_path_to_use_); buf[file_path_to_use_.len] = 0; const slice: [:0]u8 = buf[0..file_path_to_use_.len :0]; - index = try INotify.watchDir(slice); + item.eventlist_index = try this.platform.watchDir(slice); } - this.watchlist.appendAssumeCapacity(.{ - .file_path = file_path_, - .fd = fd, - .hash = hash, - .count = 0, - .eventlist_index = index, - .loader = options.Loader.file, - .parent_hash = parent_hash, - .kind = .directory, - .package_json = null, - }); + this.watchlist.appendAssumeCapacity(item); return @as(WatchItemIndex, @truncate(this.watchlist.len - 1)); } - pub inline fn isEligibleDirectory(this: *Watcher, dir: string) bool { - return strings.indexOf(dir, this.fs.top_level_dir) != null and strings.indexOf(dir, "node_modules") == null; - } - - pub fn addDirectory( - this: *Watcher, - fd: StoredFileDescriptorType, - file_path: string, - hash: HashType, - comptime copy_file_path: bool, - ) !void { - this.mutex.lock(); - defer this.mutex.unlock(); - - if (this.indexOf(hash) != null) { - return; - } - - try this.watchlist.ensureUnusedCapacity(this.allocator, 1); - - _ = try this.appendDirectoryAssumeCapacity(fd, file_path, hash, copy_file_path); - } + // Below is platform-independent pub fn appendFileMaybeLock( this: *Watcher, - fd: StoredFileDescriptorType, + fd: bun.FileDescriptor, file_path: string, hash: HashType, loader: options.Loader, - dir_fd: StoredFileDescriptorType, + dir_fd: bun.FileDescriptor, package_json: ?*PackageJSON, comptime copy_file_path: bool, comptime lock: bool, @@ -916,10 +1036,10 @@ pub fn NewWatcher(comptime ContextType: type) type { if (comptime lock) this.mutex.lock(); defer if (comptime lock) this.mutex.unlock(); std.debug.assert(file_path.len > 1); - const pathname = Fs.PathName.init(file_path); + const pathname = bun.fs.PathName.init(file_path); const parent_dir = pathname.dirWithTrailingSlash(); - const parent_dir_hash: HashType = Watcher.getHash(parent_dir); + const parent_dir_hash: HashType = getHash(parent_dir); var parent_watch_item: ?WatchItemIndex = null; const autowatch_parent_dir = (comptime FeatureFlags.watch_directories) and this.isEligibleDirectory(parent_dir); @@ -928,7 +1048,7 @@ pub fn NewWatcher(comptime ContextType: type) type { if (dir_fd.int() > 0) { const fds = watchlist_slice.items(.fd); - if (std.mem.indexOfScalar(StoredFileDescriptorType, fds, dir_fd)) |i| { + if (std.mem.indexOfScalar(bun.FileDescriptor, fds, dir_fd)) |i| { parent_watch_item = @as(WatchItemIndex, @truncate(i)); } } @@ -965,17 +1085,101 @@ pub fn NewWatcher(comptime ContextType: type) type { } } + inline fn isEligibleDirectory(this: *Watcher, dir: string) bool { + return strings.contains(dir, this.fs.top_level_dir) and !strings.contains(dir, "node_modules"); + } + pub fn appendFile( this: *Watcher, - fd: StoredFileDescriptorType, + fd: bun.FileDescriptor, file_path: string, hash: HashType, loader: options.Loader, - dir_fd: StoredFileDescriptorType, + dir_fd: bun.FileDescriptor, package_json: ?*PackageJSON, comptime copy_file_path: bool, ) !void { return appendFileMaybeLock(this, fd, file_path, hash, loader, dir_fd, package_json, copy_file_path, true); } + + pub fn addDirectory( + this: *Watcher, + fd: bun.FileDescriptor, + file_path: string, + hash: HashType, + comptime copy_file_path: bool, + ) !void { + this.mutex.lock(); + defer this.mutex.unlock(); + + if (this.indexOf(hash) != null) { + return; + } + + try this.watchlist.ensureUnusedCapacity(this.allocator, 1); + + _ = try this.appendDirectoryAssumeCapacity(fd, file_path, hash, copy_file_path); + } + + pub fn addFile( + this: *Watcher, + fd: bun.FileDescriptor, + file_path: string, + hash: HashType, + loader: options.Loader, + dir_fd: bun.FileDescriptor, + package_json: ?*PackageJSON, + comptime copy_file_path: bool, + ) !void { + // This must lock due to concurrent transpiler + this.mutex.lock(); + defer this.mutex.unlock(); + + if (this.indexOf(hash)) |index| { + if (comptime FeatureFlags.atomic_file_watcher) { + // On Linux, the file descriptor might be out of date. + if (fd.int() > 0) { + var fds = this.watchlist.items(.fd); + fds[index] = fd; + } + } + return; + } + + try this.appendFileMaybeLock(fd, file_path, hash, loader, dir_fd, package_json, copy_file_path, false); + } + + pub fn indexOf(this: *Watcher, hash: HashType) ?u32 { + for (this.watchlist.items(.hash), 0..) |other, i| { + if (hash == other) { + return @as(u32, @truncate(i)); + } + } + return null; + } + + pub fn remove(this: *Watcher, hash: HashType) void { + this.mutex.lock(); + defer this.mutex.unlock(); + if (this.indexOf(hash)) |index| { + this.removeAtIndex(@truncate(index), hash, &[_]HashType{}, .file); + } + } + + pub fn removeAtIndex(this: *Watcher, index: WatchItemIndex, hash: HashType, parents: []HashType, comptime kind: WatchItem.Kind) void { + std.debug.assert(index != no_watch_item); + + this.evict_list[this.evict_list_i] = index; + this.evict_list_i += 1; + + if (comptime kind == .directory) { + for (parents) |parent| { + if (parent == hash) { + this.evict_list[this.evict_list_i] = @as(WatchItemIndex, @truncate(parent)); + this.evict_list_i += 1; + } + } + } + } }; } diff --git a/src/windows.zig b/src/windows.zig index 054994619f..ab4da2601b 100644 --- a/src/windows.zig +++ b/src/windows.zig @@ -3011,10 +3011,62 @@ pub fn translateNTStatusToErrno(err: win32.NTSTATUS) bun.C.E { pub extern "kernel32" fn GetHostNameW( lpBuffer: PWSTR, nSize: c_int, -) BOOL; +) callconv(windows.WINAPI) BOOL; /// https://learn.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-gettemppathw pub extern "kernel32" fn GetTempPathW( nBufferLength: DWORD, // [in] lpBuffer: LPCWSTR, // [out] ) DWORD; + +pub extern "kernel32" fn CreateJobObjectA( + lpJobAttributes: ?*anyopaque, // [in, optional] + lpName: ?LPCSTR, // [in, optional] +) callconv(windows.WINAPI) HANDLE; + +pub extern "kernel32" fn AssignProcessToJobObject( + hJob: HANDLE, // [in] + hProcess: HANDLE, // [in] +) callconv(windows.WINAPI) BOOL; + +pub extern "kernel32" fn ResumeThread( + hJob: HANDLE, // [in] +) callconv(windows.WINAPI) DWORD; + +pub const JOBOBJECT_ASSOCIATE_COMPLETION_PORT = extern struct { + CompletionKey: windows.PVOID, + CompletionPort: HANDLE, +}; + +pub const JobObjectAssociateCompletionPortInformation: DWORD = 7; + +pub extern "kernel32" fn SetInformationJobObject( + hJob: HANDLE, + JobObjectInformationClass: DWORD, + lpJobObjectInformation: LPVOID, + cbJobObjectInformationLength: DWORD, +) callconv(windows.WINAPI) BOOL; + +// Found experimentally: +// #include +// #include +// +// int main() { +// printf("%ld\n", JOB_OBJECT_MSG_ACTIVE_PROCESS_ZERO); +// printf("%ld\n", JOB_OBJECT_MSG_EXIT_PROCESS); +// } +// +// Output: +// 4 +// 7 +pub const JOB_OBJECT_MSG_ACTIVE_PROCESS_ZERO = 4; +pub const JOB_OBJECT_MSG_EXIT_PROCESS = 7; + +pub extern "kernel32" fn OpenProcess( + dwDesiredAccess: DWORD, + bInheritHandle: BOOL, + dwProcessId: DWORD, +) callconv(windows.WINAPI) ?HANDLE; + +// https://learn.microsoft.com/en-us/windows/win32/procthread/process-security-and-access-rights +pub const PROCESS_QUERY_LIMITED_INFORMATION: DWORD = 0x1000; diff --git a/test/cli/hot/hot.test.ts b/test/cli/hot/hot.test.ts index facffa042b..c3c97fc782 100644 --- a/test/cli/hot/hot.test.ts +++ b/test/cli/hot/hot.test.ts @@ -2,10 +2,10 @@ import { spawn } from "bun"; import { expect, it } from "bun:test"; import { bunExe, bunEnv, tempDirWithFiles, bunRun, bunRunAsScript } from "harness"; -import { readFileSync, renameSync, rmSync, unlinkSync, writeFileSync } from "fs"; +import { readFileSync, renameSync, rmSync, unlinkSync, writeFileSync, copyFileSync } from "fs"; import { join } from "path"; -const hotRunnerRoot = join(import.meta.dir, "/hot-runner-root.js"); +const hotRunnerRoot = join(import.meta.dir, "hot-runner-root.js"); it("should hot reload when file is overwritten", async () => { const root = hotRunnerRoot; @@ -169,7 +169,8 @@ it("should not hot reload when a random file is written", async () => { }); it("should hot reload when a file is deleted and rewritten", async () => { - const root = hotRunnerRoot; + const root = hotRunnerRoot + ".tmp.js"; + copyFileSync(hotRunnerRoot, root); const runner = spawn({ cmd: [bunExe(), "--hot", "run", root], env: bunEnv, @@ -182,7 +183,7 @@ it("should hot reload when a file is deleted and rewritten", async () => { async function onReload() { const contents = readFileSync(root, "utf-8"); - unlinkSync(root); + rmSync(root); writeFileSync(root, contents); } @@ -205,12 +206,13 @@ it("should hot reload when a file is deleted and rewritten", async () => { if (any) await onReload(); } - + rmSync(root); expect(reloadCounter).toBe(3); }); it("should hot reload when a file is renamed() into place", async () => { - const root = hotRunnerRoot; + const root = hotRunnerRoot + ".tmp.js"; + copyFileSync(hotRunnerRoot, root); const runner = spawn({ cmd: [bunExe(), "--hot", "run", root], env: bunEnv, @@ -227,6 +229,8 @@ it("should hot reload when a file is renamed() into place", async () => { await 1; writeFileSync(root + ".tmpfile", contents); await 1; + rmSync(root); + await 1; renameSync(root + ".tmpfile", root); await 1; } @@ -250,6 +254,6 @@ it("should hot reload when a file is renamed() into place", async () => { if (any) await onReload(); } - + rmSync(root); expect(reloadCounter).toBe(3); }); diff --git a/test/cli/watch/watch.test.ts b/test/cli/watch/watch.test.ts index fc4d65a1da..a4cf98aac5 100644 --- a/test/cli/watch/watch.test.ts +++ b/test/cli/watch/watch.test.ts @@ -1,14 +1,14 @@ -import { describe, test, expect, afterEach } from "bun:test"; +import { it, expect, afterEach } from "bun:test"; import type { Subprocess } from "bun"; import { spawn } from "bun"; import { join } from "node:path"; import { tmpdir } from "node:os"; -import { mkdtempSync, writeFileSync } from "node:fs"; +import { mkdtempSync, writeFileSync, rmSync } from "node:fs"; import { bunExe, bunEnv } from "harness"; let watchee: Subprocess; -describe("bun --watch", () => { +it("should watch files", async () => { const cwd = mkdtempSync(join(tmpdir(), "bun-test-")); const path = join(cwd, "watchee.js"); @@ -16,16 +16,25 @@ describe("bun --watch", () => { writeFileSync(path, `console.log(${i});`); }; - test("should watch files", async () => { - watchee = spawn({ - cwd, - cmd: [bunExe(), "--watch", "watchee.js"], - env: bunEnv, - stdout: "inherit", - stderr: "inherit", - }); - await Bun.sleep(2000); + let i = 0; + updateFile(i); + watchee = spawn({ + cwd, + cmd: [bunExe(), "--watch", "watchee.js"], + env: bunEnv, + stdout: "pipe", + stderr: "inherit", + stdin: "ignore", }); + + for await (const line of watchee.stdout) { + if (i == 10) break; + var str = new TextDecoder().decode(line); + expect(str).toContain(`${i}`); + i++; + updateFile(i); + } + rmSync(path); }); afterEach(() => { diff --git a/test/js/node/fs/fs-stream.link.js b/test/js/node/fs/fs-stream.link.js deleted file mode 120000 index 0cadae0e54..0000000000 --- a/test/js/node/fs/fs-stream.link.js +++ /dev/null @@ -1 +0,0 @@ -./test/bun.js/fs-stream.js \ No newline at end of file From dbe0a4a978b81e0e747075bfb850832bcd3ff73f Mon Sep 17 00:00:00 2001 From: dave caruso Date: Wed, 31 Jan 2024 22:14:03 -0800 Subject: [PATCH 30/45] do more assertions (#8610) --- src/string_immutable.zig | 56 ++++++++++++++++++++++++++++++++-------- src/sys.zig | 13 +++++++++- src/sys_uv.zig | 18 +++++++++++++ 3 files changed, 75 insertions(+), 12 deletions(-) diff --git a/src/string_immutable.zig b/src/string_immutable.zig index c51436b00c..c73bcc83b8 100644 --- a/src/string_immutable.zig +++ b/src/string_immutable.zig @@ -657,6 +657,14 @@ pub fn startsWith(self: string, str: string) bool { return eqlLong(self[0..str.len], str, false); } +pub fn startsWithGeneric(comptime T: type, self: []const T, str: []const T) bool { + if (str.len > self.len) { + return false; + } + + return eqlLong(bun.reinterpretSlice(u8, self[0..str.len]), str, false); +} + pub inline fn endsWith(self: string, str: string) bool { return str.len == 0 or @call(.always_inline, std.mem.endsWith, .{ u8, self, str }); } @@ -848,8 +856,19 @@ pub fn hasPrefixComptimeUTF16(self: []const u16, comptime alt: []const u8) bool return self.len >= alt.len and eqlComptimeCheckLenWithType(u16, self[0..alt.len], comptime toUTF16Literal(alt), false); } -pub fn hasPrefixComptimeType(comptime T: type, self: []const T, comptime alt: []const T) bool { - return self.len >= alt.len and eqlComptimeCheckLenWithType(u16, self[0..alt.len], alt, false); +pub fn hasPrefixComptimeType(comptime T: type, self: []const T, comptime alt: anytype) bool { + const rhs = comptime switch (T) { + u8 => alt, + u16 => switch (std.meta.Child(@TypeOf(alt))) { + u8 => w(alt), + else => |t| switch (std.meta.Child(t)) { + u8 => w(alt), + else => alt, + }, + }, + else => unreachable, + }; + return self.len >= alt.len and eqlComptimeCheckLenWithType(T, self[0..rhs.len], rhs, false); } pub fn hasSuffixComptime(self: string, comptime alt: anytype) bool { @@ -1650,10 +1669,22 @@ pub fn utf16Codepoint(comptime Type: type, input: Type) UTF16Replacement { } } -fn windowsPathIsPosixAbsolute(utf8: []const u8) bool { - if (utf8.len == 0) return false; - if (!charIsAnySlash(utf8[0])) return false; - if (utf8.len > 1 and charIsAnySlash(utf8[1])) return false; +/// '/hello' -> true +/// '\hello' -> true +/// 'C:/hello' -> false +/// '\??\C:\hello' -> false +fn windowsPathIsPosixAbsolute(comptime T: type, chars: []const T) bool { + if (chars.len == 0) return false; + if (!(chars[0] == '/' or chars[0] == '\\')) return false; + if (chars.len > 1 and + (chars[1] == '/' or chars[1] == '\\')) return false; + if (chars.len > 2 and + chars[2] == ':') return false; + if (chars.len > 4 and + chars[1] == '?' and + chars[2] == '?' and + (chars[3] == '/' or chars[3] == '\\')) + return windowsPathIsPosixAbsolute(T, chars[4..]); return true; } @@ -1754,11 +1785,16 @@ pub fn toWDirPath(wbuf: []u16, utf8: []const u8) [:0]const u16 { return toWPathMaybeDir(wbuf, utf8, true); } -pub fn assertIsValidWindowsPath(utf8: []const u8) void { +pub fn assertIsValidWindowsPath(comptime T: type, path: []const T) void { if (Environment.allow_assert and Environment.isWindows) { - if (startsWith(utf8, ":/")) { + if (windowsPathIsPosixAbsolute(T, path)) { + std.debug.panic("Do not pass posix paths to windows APIs, was given '{s}' (missing a root like 'C:\\', see PosixToWinNormalizer for why this is an assertion)", .{ + if (T == u8) path else std.unicode.fmtUtf16le(path), + }); + } + if (hasPrefixComptimeType(T, path, ":/")) { std.debug.panic("Path passed to windows API '{s}' is almost certainly invalid. Where did the drive letter go?", .{ - utf8, + if (T == u8) path else std.unicode.fmtUtf16le(path), }); } } @@ -1767,8 +1803,6 @@ pub fn assertIsValidWindowsPath(utf8: []const u8) void { pub fn toWPathMaybeDir(wbuf: []u16, utf8: []const u8, comptime add_trailing_lash: bool) [:0]const u16 { std.debug.assert(wbuf.len > 0); - assertIsValidWindowsPath(utf8); - var result = bun.simdutf.convert.utf8.to.utf16.with_errors.le( utf8, wbuf[0..wbuf.len -| (1 + @as(usize, @intFromBool(add_trailing_lash)))], diff --git a/src/sys.zig b/src/sys.zig index 528909a172..537c126980 100644 --- a/src/sys.zig +++ b/src/sys.zig @@ -15,6 +15,7 @@ const C = @import("root").bun.C; const linux = os.linux; const Maybe = JSC.Maybe; const kernel32 = bun.windows; +const assertIsValidWindowsPath = bun.strings.assertIsValidWindowsPath; pub const sys_uv = if (Environment.isWindows) @import("./sys_uv.zig") else Syscall; @@ -169,6 +170,8 @@ pub fn fchmod(fd: bun.FileDescriptor, mode: bun.Mode) Maybe(void) { } pub fn chdirOSPath(destination: bun.OSPathSliceZ) Maybe(void) { + assertIsValidWindowsPath(bun.OSPathChar, destination); + if (comptime Environment.isPosix) { const rc = sys.chdir(destination); return Maybe(void).errnoSys(rc, .chdir) orelse Maybe(void).success; @@ -303,8 +306,10 @@ pub fn mkdirA(file_path: []const u8, flags: bun.Mode) Maybe(void) { if (comptime Environment.isWindows) { var wbuf: bun.WPathBuffer = undefined; + const wpath = bun.strings.toWPath(&wbuf, file_path); + assertIsValidWindowsPath(u16, wpath); return Maybe(void).errnoSysP( - kernel32.CreateDirectoryW(bun.strings.toWPath(&wbuf, file_path).ptr, null), + kernel32.CreateDirectoryW(wpath.ptr, null), .mkdir, file_path, ) orelse Maybe(void).success; @@ -315,6 +320,7 @@ pub fn mkdirOSPath(file_path: bun.OSPathSliceZ, flags: bun.Mode) Maybe(void) { return switch (Environment.os) { else => mkdir(file_path, flags), .windows => { + assertIsValidWindowsPath(bun.OSPathChar, file_path); return Maybe(void).errnoSys( kernel32.CreateDirectoryW(file_path, null), .mkdir, @@ -416,6 +422,7 @@ pub fn openDirAtWindows( iterable: bool, no_follow: bool, ) Maybe(bun.FileDescriptor) { + assertIsValidWindowsPath(u16, path); const base_flags = w.STANDARD_RIGHTS_READ | w.FILE_READ_ATTRIBUTES | w.FILE_READ_EA | w.SYNCHRONIZE | w.FILE_TRAVERSE; const flags: u32 = if (iterable) base_flags | w.FILE_LIST_DIRECTORY else base_flags; @@ -568,6 +575,7 @@ pub fn ntCreateFile( // this path is probably already backslash normalized so we're only going to check for '.\' const path = if (bun.strings.hasPrefixComptimeUTF16(path_maybe_leading_dot, ".\\")) path_maybe_leading_dot[2..] else path_maybe_leading_dot; std.debug.assert(!bun.strings.hasPrefixComptimeUTF16(path_maybe_leading_dot, "./")); + assertIsValidWindowsPath(u16, path); const path_len_bytes = std.math.cast(u16, path.len * 2) orelse return .{ .err = .{ @@ -1403,6 +1411,7 @@ pub fn mmap( } pub fn mmapFile(path: [:0]const u8, flags: u32, wanted_size: ?usize, offset: usize) Maybe([]align(mem.page_size) u8) { + assertIsValidWindowsPath(u8, path); const fd = switch (open(path, os.O.RDWR, 0)) { .result => |fd| fd, .err => |err| return .{ .err = err }, @@ -1658,6 +1667,7 @@ pub fn existsOSPath(path: bun.OSPathSliceZ) bool { } if (comptime Environment.isWindows) { + assertIsValidWindowsPath(bun.OSPathChar, path); const result = kernel32.GetFileAttributesW(path.ptr); if (Environment.isDebug) { log("GetFileAttributesW({}) = {d}", .{ bun.fmt.fmtUTF16(path), result }); @@ -1676,6 +1686,7 @@ pub fn exists(path: []const u8) bool { if (comptime Environment.isWindows) { var wbuf: bun.WPathBuffer = undefined; const path_to_use = bun.strings.toWPath(&wbuf, path); + assertIsValidWindowsPath(u16, path_to_use); return kernel32.GetFileAttributesW(path_to_use.ptr) != windows.INVALID_FILE_ATTRIBUTES; } diff --git a/src/sys_uv.zig b/src/sys_uv.zig index f1ef3de9b5..4e7c951213 100644 --- a/src/sys_uv.zig +++ b/src/sys_uv.zig @@ -15,6 +15,7 @@ const E = C.E; const linux = os.linux; const Maybe = JSC.Maybe; const kernel32 = bun.windows; +const assertIsValidWindowsPath = bun.strings.assertIsValidWindowsPath; const uv = bun.windows.libuv; @@ -38,6 +39,8 @@ pub const mkdirOSPath = bun.sys.mkdirOSPath; // Note: `req = undefined; req.deinit()` has a saftey-check in a debug build pub fn open(file_path: [:0]const u8, c_flags: bun.Mode, _perm: bun.Mode) Maybe(bun.FileDescriptor) { + assertIsValidWindowsPath(u8, file_path); + var req: uv.fs_t = uv.fs_t.uninitialized; defer req.deinit(); @@ -58,6 +61,7 @@ pub fn open(file_path: [:0]const u8, c_flags: bun.Mode, _perm: bun.Mode) Maybe(b } pub fn mkdir(file_path: [:0]const u8, flags: bun.Mode) Maybe(void) { + assertIsValidWindowsPath(u8, file_path); var req: uv.fs_t = uv.fs_t.uninitialized; defer req.deinit(); const rc = uv.uv_fs_mkdir(uv.Loop.get(), &req, file_path.ptr, flags, null); @@ -70,6 +74,7 @@ pub fn mkdir(file_path: [:0]const u8, flags: bun.Mode) Maybe(void) { } pub fn chmod(file_path: [:0]const u8, flags: bun.Mode) Maybe(void) { + assertIsValidWindowsPath(u8, file_path); var req: uv.fs_t = uv.fs_t.uninitialized; defer req.deinit(); const rc = uv.uv_fs_chmod(uv.Loop.get(), &req, file_path.ptr, flags, null); @@ -95,6 +100,7 @@ pub fn fchmod(fd: FileDescriptor, flags: bun.Mode) Maybe(void) { } pub fn chown(file_path: [:0]const u8, uid: uv.uv_uid_t, gid: uv.uv_uid_t) Maybe(void) { + assertIsValidWindowsPath(u8, file_path); var req: uv.fs_t = uv.fs_t.uninitialized; defer req.deinit(); const rc = uv.uv_fs_chown(uv.Loop.get(), &req, file_path.ptr, uid, gid, null); @@ -121,6 +127,7 @@ pub fn fchown(fd: FileDescriptor, uid: uv.uv_uid_t, gid: uv.uv_uid_t) Maybe(void } pub fn access(file_path: [:0]const u8, flags: bun.Mode) Maybe(void) { + assertIsValidWindowsPath(u8, file_path); var req: uv.fs_t = uv.fs_t.uninitialized; defer req.deinit(); const rc = uv.uv_fs_access(uv.Loop.get(), &req, file_path.ptr, flags, null); @@ -133,6 +140,7 @@ pub fn access(file_path: [:0]const u8, flags: bun.Mode) Maybe(void) { } pub fn rmdir(file_path: [:0]const u8) Maybe(void) { + assertIsValidWindowsPath(u8, file_path); var req: uv.fs_t = uv.fs_t.uninitialized; defer req.deinit(); const rc = uv.uv_fs_rmdir(uv.Loop.get(), &req, file_path.ptr, null); @@ -145,6 +153,7 @@ pub fn rmdir(file_path: [:0]const u8) Maybe(void) { } pub fn unlink(file_path: [:0]const u8) Maybe(void) { + assertIsValidWindowsPath(u8, file_path); var req: uv.fs_t = uv.fs_t.uninitialized; defer req.deinit(); const rc = uv.uv_fs_unlink(uv.Loop.get(), &req, file_path.ptr, null); @@ -157,6 +166,7 @@ pub fn unlink(file_path: [:0]const u8) Maybe(void) { } pub fn readlink(file_path: [:0]const u8, buf: []u8) Maybe(usize) { + assertIsValidWindowsPath(u8, file_path); var req: uv.fs_t = uv.fs_t.uninitialized; defer req.deinit(); // Edge cases: http://docs.libuv.org/en/v1.x/fs.html#c.uv_fs_realpath @@ -180,6 +190,8 @@ pub fn readlink(file_path: [:0]const u8, buf: []u8) Maybe(usize) { } pub fn rename(from: [:0]const u8, to: [:0]const u8) Maybe(void) { + assertIsValidWindowsPath(u8, from); + assertIsValidWindowsPath(u8, to); var req: uv.fs_t = uv.fs_t.uninitialized; defer req.deinit(); const rc = uv.uv_fs_rename(uv.Loop.get(), &req, from.ptr, to.ptr, null); @@ -192,6 +204,8 @@ pub fn rename(from: [:0]const u8, to: [:0]const u8) Maybe(void) { } pub fn link(from: [:0]const u8, to: [:0]const u8) Maybe(void) { + assertIsValidWindowsPath(u8, from); + assertIsValidWindowsPath(u8, to); var req: uv.fs_t = uv.fs_t.uninitialized; defer req.deinit(); const rc = uv.uv_fs_link(uv.Loop.get(), &req, from.ptr, to.ptr, null); @@ -204,6 +218,8 @@ pub fn link(from: [:0]const u8, to: [:0]const u8) Maybe(void) { } pub fn symlinkUV(from: [:0]const u8, to: [:0]const u8, flags: c_int) Maybe(void) { + assertIsValidWindowsPath(u8, from); + assertIsValidWindowsPath(u8, to); var req: uv.fs_t = uv.fs_t.uninitialized; defer req.deinit(); const rc = uv.uv_fs_symlink(uv.Loop.get(), &req, from.ptr, to.ptr, flags, null); @@ -268,6 +284,7 @@ pub fn fsync(fd: FileDescriptor) Maybe(void) { } pub fn stat(path: [:0]const u8) Maybe(bun.Stat) { + assertIsValidWindowsPath(u8, path); var req: uv.fs_t = uv.fs_t.uninitialized; defer req.deinit(); const rc = uv.uv_fs_stat(uv.Loop.get(), &req, path.ptr, null); @@ -280,6 +297,7 @@ pub fn stat(path: [:0]const u8) Maybe(bun.Stat) { } pub fn lstat(path: [:0]const u8) Maybe(bun.Stat) { + assertIsValidWindowsPath(u8, path); var req: uv.fs_t = uv.fs_t.uninitialized; defer req.deinit(); const rc = uv.uv_fs_lstat(uv.Loop.get(), &req, path.ptr, null); From 42e4cd8408f2cc808294e0df0738469671a61ae9 Mon Sep 17 00:00:00 2001 From: Meghan Denny Date: Wed, 31 Jan 2024 22:29:33 -0800 Subject: [PATCH 31/45] get more bundler tests passing on windows (#8560) * get test/bundler/bundler_naming.test.ts passing on windows * move platformToPosixInPlace to bun.path and use the vector'd version * only resolve rel_path if it contains '/./', most of the time './' needs to be preserved * fix another file too * move kernel32 extern to better location * [autofix.ci] apply automated fixes * use bun.path.posixToPlatformInPlace here * rewrite this whole section to stay in utf16 and handle errors * remove dead comments * fix a typo * undo these relative changes * preserve path.pretty from getting lost * use bun's instead of zig's resolve here * both side of this loop need the inplace normal * use existing generic dirname function * make path inplace functions generic * we might need to modify this so copy on windows Co-authored-by: Jarred Sumner * don't assume the prefix we get from this * more robust path traversal * don't mangle absolute paths * do this bit in a loop * this line is a u16 * dont forget to compile before pushing * this was wrong * look into this later * check pointer math first since its faster * posix syscalls support path traversal, don't do the work for them * its already inside stable, no need to change * used the online editor --------- Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> Co-authored-by: Jarred Sumner --- scripts/build.ps1 | 2 +- src/bun.js/node/node_fs.zig | 43 ++++++++++++++++++++++++-- src/bun.js/node/types.zig | 16 +++++----- src/bun.zig | 11 +++++++ src/bundler/bundle_v2.zig | 37 +++++++++++++++------- src/fs.zig | 8 ++--- src/http/url_path.zig | 7 +---- src/options.zig | 1 + src/resolver/resolve_path.zig | 16 ++++++++++ src/string_immutable.zig | 2 +- src/windows.zig | 1 + test/bundler/bundler_naming.test.ts | 34 +++++++++++++++++++- test/bundler/esbuild/splitting.test.ts | 8 +++-- test/bundler/expectBundled.ts | 10 +++--- 14 files changed, 154 insertions(+), 42 deletions(-) diff --git a/scripts/build.ps1 b/scripts/build.ps1 index facf34749c..f70328c71a 100644 --- a/scripts/build.ps1 +++ b/scripts/build.ps1 @@ -1,2 +1,2 @@ -.\scripts\env.sh +.\scripts\env.ps1 ninja -Cbuild diff --git a/src/bun.js/node/node_fs.zig b/src/bun.js/node/node_fs.zig index 6ae05bd220..ac9b00aef1 100644 --- a/src/bun.js/node/node_fs.zig +++ b/src/bun.js/node/node_fs.zig @@ -5273,13 +5273,52 @@ pub const NodeFS = struct { pub fn writeFileWithPathBuffer(pathbuf: *[bun.MAX_PATH_BYTES]u8, args: Arguments.WriteFile) Maybe(Return.WriteFile) { var path: [:0]const u8 = undefined; + var pathbuf2: [bun.MAX_PATH_BYTES]u8 = undefined; const fd = switch (args.file) { .path => brk: { - path = args.file.path.sliceZ(pathbuf); + // On Windows, we potentially mutate the path in posixToPlatformInPlace + // We cannot mutate JavaScript strings in-place. That will break many things. + // So we must always copy the path string on Windows. + path = args.file.path.sliceZWithForceCopy(pathbuf, Environment.isWindows); + bun.path.posixToPlatformInPlace(u8, @constCast(path)); + + var is_dirfd_different = false; + var dirfd = args.dirfd; + if (Environment.isWindows) { + while (std.mem.startsWith(u8, path, "..\\")) { + is_dirfd_different = true; + var buffer: bun.WPathBuffer = undefined; + const dirfd_path_len = std.os.windows.kernel32.GetFinalPathNameByHandleW(args.dirfd.cast(), &buffer, buffer.len, 0); + const dirfd_path = buffer[0..dirfd_path_len]; + const parent_path = bun.Dirname.dirname(u16, dirfd_path).?; + if (std.mem.startsWith(u16, parent_path, &bun.windows.nt_maxpath_prefix)) @constCast(parent_path)[1] = '?'; + const newdirfd = switch (bun.sys.openDirAtWindows(bun.invalid_fd, parent_path, false, true)) { + .result => |fd| fd, + .err => |err| { + return .{ .err = err.withPath(path) }; + }, + }; + path = path[3..]; + dirfd = newdirfd; + } + } + defer if (is_dirfd_different) { + var d = dirfd.asDir(); + d.close(); + }; + if (Environment.isWindows) { + // windows openat does not support path traversal, fix it here. + // use pathbuf2 here since without it 'panic: @memcpy arguments alias' triggers + if (std.mem.indexOf(u8, path, "\\.\\") != null or std.mem.indexOf(u8, path, "\\..\\") != null) { + const fixed_path = bun.path.normalizeStringWindows(path, &pathbuf2, false, false); + pathbuf2[fixed_path.len] = 0; + path = pathbuf2[0..fixed_path.len :0]; + } + } const open_result = Syscall.openat( - args.dirfd, + dirfd, path, @intFromEnum(args.flag) | os.O.NOCTTY, args.mode, diff --git a/src/bun.js/node/types.zig b/src/bun.js/node/types.zig index 07336d146d..aa1a247b4e 100644 --- a/src/bun.js/node/types.zig +++ b/src/bun.js/node/types.zig @@ -657,12 +657,17 @@ pub const PathLike = union(enum) { pub fn sliceZWithForceCopy(this: PathLike, buf: *[bun.MAX_PATH_BYTES]u8, comptime force: bool) [:0]const u8 { const sliced = this.slice(); + if (Environment.isWindows) { + if (std.fs.path.isAbsolute(sliced)) { + return resolve_path.PosixToWinNormalizer.resolveCWDWithExternalBufZ(buf, sliced) catch @panic("Error while resolving path."); + } + } + if (sliced.len == 0) return ""; if (comptime !force) { if (sliced[sliced.len - 1] == 0) { - var sliced_ptr = sliced.ptr; - return sliced_ptr[0 .. sliced.len - 1 :0]; + return sliced[0 .. sliced.len - 1 :0]; } } @@ -672,13 +677,6 @@ pub const PathLike = union(enum) { } pub inline fn sliceZ(this: PathLike, buf: *[bun.MAX_PATH_BYTES]u8) [:0]const u8 { - if (Environment.isWindows) { - const data = this.slice(); - if (!std.fs.path.isAbsolute(data)) { - return sliceZWithForceCopy(this, buf, false); - } - return resolve_path.PosixToWinNormalizer.resolveCWDWithExternalBufZ(buf, data) catch @panic("Error while resolving path."); - } return sliceZWithForceCopy(this, buf, false); } diff --git a/src/bun.zig b/src/bun.zig index 353c466b62..168f8cd9b5 100644 --- a/src/bun.zig +++ b/src/bun.zig @@ -42,6 +42,7 @@ 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"); +pub const allocators = @import("./allocators.zig"); pub const shell = struct { pub usingnamespace @import("./shell/shell.zig"); @@ -552,6 +553,16 @@ pub inline fn isSliceInBuffer(slice: []const u8, buffer: []const u8) bool { return slice.len > 0 and @intFromPtr(buffer.ptr) <= @intFromPtr(slice.ptr) and ((@intFromPtr(slice.ptr) + slice.len) <= (@intFromPtr(buffer.ptr) + buffer.len)); } +pub inline fn sliceInBuffer(stable: string, value: string) string { + if (allocators.sliceRange(stable, value)) |_| { + return value; + } + if (strings.indexOf(stable, value)) |index| { + return stable[index..][0..value.len]; + } + return value; +} + pub fn rangeOfSliceInBuffer(slice: []const u8, buffer: []const u8) ?[2]u32 { if (!isSliceInBuffer(slice, buffer)) return null; const r = [_]u32{ diff --git a/src/bundler/bundle_v2.zig b/src/bundler/bundle_v2.zig index 6e633099a2..e144ce5430 100644 --- a/src/bundler/bundle_v2.zig +++ b/src/bundler/bundle_v2.zig @@ -666,6 +666,7 @@ pub const BundleV2 = struct { } _ = @atomicRmw(usize, &this.graph.parse_pending, .Add, 1, .Monotonic); const source_index = Index.source(this.graph.input_files.len); + if (path.pretty.ptr == path.text.ptr) { // TODO: outbase const rel = bun.path.relative(this.bundler.fs.top_level_dir, path.text); @@ -674,6 +675,8 @@ pub const BundleV2 = struct { } } path.* = try path.dupeAlloc(this.graph.allocator); + // TODO: this shouldn't be necessary + path.pretty = bun.sliceInBuffer(path.text, path.pretty); entry.value_ptr.* = source_index.get(); this.graph.ast.append(bun.default_allocator, JSAst.empty) catch unreachable; @@ -1115,9 +1118,7 @@ pub const BundleV2 = struct { }, }, .size = source.contents.len, - .output_path = std.fmt.allocPrint(bun.default_allocator, "{}", .{ - template, - }) catch unreachable, + .output_path = std.fmt.allocPrint(bun.default_allocator, "{}", .{template}) catch unreachable, .input_path = bun.default_allocator.dupe(u8, source.path.text) catch unreachable, .input_loader = .file, .output_kind = .asset, @@ -2029,6 +2030,8 @@ pub const BundleV2 = struct { } path.* = path.dupeAlloc(this.graph.allocator) catch @panic("Ran out of memory"); + // TODO: this shouldn't be necessary + path.pretty = bun.sliceInBuffer(path.text, path.pretty); import_record.path = path.*; debug("created ParseTask: {s}", .{path.text}); @@ -9027,11 +9030,20 @@ const LinkerContext = struct { chunk.template.placeholder.hash = chunk.isolated_hash; const rel_path = std.fmt.allocPrint(c.allocator, "{any}", .{chunk.template}) catch unreachable; + bun.path.platformToPosixInPlace(u8, rel_path); + if ((try path_names_map.getOrPut(rel_path)).found_existing) { try c.log.addErrorFmt(null, Logger.Loc.Empty, bun.default_allocator, "Multiple files share the same output path: {s}", .{rel_path}); return error.DuplicateOutputPath; } - + // resolve any /./ and /../ occurrences + // use resolvePosix since we asserted above all seps are '/' + if (Environment.isWindows and std.mem.indexOf(u8, rel_path, "/./") != null) { + var buf: [std.fs.MAX_PATH_BYTES]u8 = undefined; + const rel_path_fixed = c.allocator.dupe(u8, bun.path.normalizeBuf(rel_path, &buf, .posix)) catch unreachable; + chunk.final_rel_path = rel_path_fixed; + continue; + } chunk.final_rel_path = rel_path; } } @@ -9377,7 +9389,7 @@ const LinkerContext = struct { defer max_heap_allocator.reset(); const rel_path = chunk.final_rel_path; - if (std.fs.path.dirname(rel_path)) |rel_parent| { + if (std.fs.path.dirnamePosix(rel_path)) |rel_parent| { if (rel_parent.len > 0) { root_dir.makePath(rel_parent) catch |err| { c.log.addErrorFmt(null, Logger.Loc.Empty, bun.default_allocator, "{s} creating outdir {} while saving chunk {}", .{ @@ -11082,7 +11094,7 @@ pub const Chunk = struct { shifts.appendAssumeCapacity(shift); var count: usize = 0; - var from_chunk_dir = std.fs.path.dirname(chunk.final_rel_path) orelse ""; + var from_chunk_dir = std.fs.path.dirnamePosix(chunk.final_rel_path) orelse ""; if (strings.eqlComptime(from_chunk_dir, ".")) from_chunk_dir = ""; @@ -11103,7 +11115,7 @@ pub const Chunk = struct { if (from_chunk_dir.len == 0) file_path else - bun.path.relative(from_chunk_dir, file_path), + bun.path.relativePlatform(from_chunk_dir, file_path, .posix, false), ); count += cheap_normalizer[0].len + cheap_normalizer[1].len; }, @@ -11160,7 +11172,7 @@ pub const Chunk = struct { if (from_chunk_dir.len == 0) file_path else - bun.path.relative(from_chunk_dir, file_path), + bun.path.relativePlatform(from_chunk_dir, file_path, .posix, false), ); if (cheap_normalizer[0].len > 0) { @@ -11250,7 +11262,7 @@ pub const Chunk = struct { var count: usize = 0; const file_path_buf: [4096]u8 = undefined; _ = file_path_buf; - var from_chunk_dir = std.fs.path.dirname(chunk.final_rel_path) orelse ""; + var from_chunk_dir = std.fs.path.dirnamePosix(chunk.final_rel_path) orelse ""; if (strings.eqlComptime(from_chunk_dir, ".")) from_chunk_dir = ""; @@ -11274,7 +11286,8 @@ pub const Chunk = struct { .chunk => chunks[index].final_rel_path, else => unreachable, }; - + // normalize windows paths to '/' + bun.path.platformToPosixInPlace(u8, @constCast(file_path)); const cheap_normalizer = cheapPrefixNormalizer( import_prefix, if (from_chunk_dir.len == 0) @@ -11315,12 +11328,14 @@ pub const Chunk = struct { .chunk => chunks[index].final_rel_path, else => unreachable, }; + // normalize windows paths to '/' + bun.path.platformToPosixInPlace(u8, @constCast(file_path)); const cheap_normalizer = cheapPrefixNormalizer( import_prefix, if (from_chunk_dir.len == 0) file_path else - bun.path.relative(from_chunk_dir, file_path), + bun.path.relativePlatform(from_chunk_dir, file_path, .posix, false), ); if (cheap_normalizer[0].len > 0) { diff --git a/src/fs.zig b/src/fs.zig index 973e54ce6a..f0ecdb18eb 100644 --- a/src/fs.zig +++ b/src/fs.zig @@ -18,7 +18,7 @@ const Semaphore = sync.Semaphore; const Fs = @This(); const path_handler = @import("./resolver/resolve_path.zig"); const PathString = bun.PathString; -const allocators = @import("./allocators.zig"); +const allocators = bun.allocators; const MAX_PATH_BYTES = bun.MAX_PATH_BYTES; const PathBuffer = bun.PathBuffer; @@ -1643,7 +1643,7 @@ pub const Path = struct { // This duplicates but only when strictly necessary // This will skip allocating if it's already in FilenameStore or DirnameStore pub fn dupeAlloc(this: *const Path, allocator: std.mem.Allocator) !Fs.Path { - if (this.text.ptr == this.pretty.ptr and this.text.len == this.text.len) { + if (this.text.ptr == this.pretty.ptr and this.text.len == this.pretty.len) { if (FileSystem.FilenameStore.instance.exists(this.text) or FileSystem.DirnameStore.instance.exists(this.text)) { return this.*; } @@ -1663,12 +1663,12 @@ pub const Path = struct { new_path.namespace = this.namespace; new_path.is_symlink = this.is_symlink; return new_path; - } else if (allocators.sliceRange(this.pretty, this.text)) |start_end| { + } else if (allocators.sliceRange(this.pretty, this.text)) |start_len| { if (FileSystem.FilenameStore.instance.exists(this.text) or FileSystem.DirnameStore.instance.exists(this.text)) { return this.*; } var new_path = Fs.Path.init(try FileSystem.FilenameStore.instance.append([]const u8, this.text)); - new_path.pretty = this.text[start_end[0]..start_end[1]]; + new_path.pretty = this.text[start_len[0]..][0..start_len[1]]; new_path.namespace = this.namespace; new_path.is_symlink = this.is_symlink; return new_path; diff --git a/src/http/url_path.zig b/src/http/url_path.zig index e511acf3ba..d1e50072b3 100644 --- a/src/http/url_path.zig +++ b/src/http/url_path.zig @@ -71,12 +71,7 @@ pub fn parse(possibly_encoded_pathname_: string) !URLPath { bun.copy(u8, possibly_encoded_pathname, possibly_encoded_pathname_[0..possibly_encoded_pathname.len]); const clone = possibly_encoded_pathname[0..possibly_encoded_pathname.len]; - var fbs = std.io.fixedBufferStream( - // This is safe because: - // - this comes from a non-const buffer - // - percent *decoding* will always be <= length of the original string (no buffer overflow) - @constCast(possibly_encoded_pathname), - ); + var fbs = std.io.fixedBufferStream(possibly_encoded_pathname); const writer = fbs.writer(); decoded_pathname = possibly_encoded_pathname[0..try PercentEncoding.decodeFaultTolerant(@TypeOf(writer), writer, clone, &needs_redirect, true)]; diff --git a/src/options.zig b/src/options.zig index dc3a9b802c..99d727c5fc 100644 --- a/src/options.zig +++ b/src/options.zig @@ -2589,6 +2589,7 @@ pub const PathTemplate = struct { pub fn format(self: PathTemplate, comptime _: []const u8, _: std.fmt.FormatOptions, writer: anytype) !void { var remain = self.data; + bun.path.posixToPlatformInPlace(u8, @constCast(remain)); while (strings.indexOfChar(remain, '[')) |j| { try writer.writeAll(remain[0..j]); remain = remain[j + 1 ..]; diff --git a/src/resolver/resolve_path.zig b/src/resolver/resolve_path.zig index d644b3ad9e..625cf0700f 100644 --- a/src/resolver/resolve_path.zig +++ b/src/resolver/resolve_path.zig @@ -1948,3 +1948,19 @@ export fn ResolvePath__joinAbsStringBufCurrentPlatformBunString( return bun.String.createUTF8(out_slice); } + +pub fn platformToPosixInPlace(comptime T: type, path_buffer: []T) void { + if (std.fs.path.sep == '/') return; + var idx: usize = 0; + while (std.mem.indexOfScalarPos(T, path_buffer, idx, std.fs.path.sep)) |index| : (idx = index) { + path_buffer[index] = '/'; + } +} + +pub fn posixToPlatformInPlace(comptime T: type, path_buffer: []T) void { + if (std.fs.path.sep == '/') return; + var idx: usize = 0; + while (std.mem.indexOfScalarPos(T, path_buffer, idx, '/')) |index| : (idx = index) { + path_buffer[index] = std.fs.path.sep; + } +} diff --git a/src/string_immutable.zig b/src/string_immutable.zig index c73bcc83b8..abf716e0f7 100644 --- a/src/string_immutable.zig +++ b/src/string_immutable.zig @@ -1726,7 +1726,7 @@ pub const toNTDir = toNTPath; pub fn toExtendedPathNormalized(wbuf: []u16, utf8: []const u8) [:0]const u16 { std.debug.assert(wbuf.len > 4); - wbuf[0..4].* = [_]u16{ '\\', '\\', '?', '\\' }; + wbuf[0..4].* = bun.windows.nt_maxpath_prefix; return wbuf[0 .. toWPathNormalized(wbuf[4..], utf8).len + 4 :0]; } diff --git a/src/windows.zig b/src/windows.zig index ab4da2601b..be35bc0eb6 100644 --- a/src/windows.zig +++ b/src/windows.zig @@ -64,6 +64,7 @@ pub const advapi32 = windows.advapi32; pub const INVALID_FILE_ATTRIBUTES: u32 = std.math.maxInt(u32); pub const nt_object_prefix = [4]u16{ '\\', '?', '?', '\\' }; +pub const nt_maxpath_prefix = [4]u16{ '\\', '\\', '?', '\\' }; const std = @import("std"); pub const HANDLE = win32.HANDLE; diff --git a/test/bundler/bundler_naming.test.ts b/test/bundler/bundler_naming.test.ts index 5bdc1fbce4..2004056d87 100644 --- a/test/bundler/bundler_naming.test.ts +++ b/test/bundler/bundler_naming.test.ts @@ -1,4 +1,3 @@ -// @known-failing-on-windows: panic "TODO on Windows" import assert from "assert"; import dedent from "dedent"; import { ESBUILD, itBundled, testForFile } from "./expectBundled"; @@ -270,4 +269,37 @@ describe("bundler", () => { file: "/out/_.._/hello/file.js", }, }); + itBundled("naming/WithPathTraversal", { + files: { + "/a/hello/entry.js": /* js */ ` + import data from '../dependency' + console.log(data); + `, + "/a/dependency.js": /* js */ ` + export default 1; + `, + "/a/hello/world/entry.js": /* js */ ` + console.log(2); + `, + "/a/hello/world/a/a/a/a/a/a/a/entry.js": /* js */ ` + console.log(3); + `, + }, + entryNaming: "foo/../bar/[dir]/file.[ext]", + entryPointsRaw: ["./a/hello/entry.js", "./a/hello/world/entry.js", "./a/hello/world/a/a/a/a/a/a/a/entry.js"], + run: [ + { + file: "/out/bar/file.js", + stdout: "1", + }, + { + file: "/out/bar/world/file.js", + stdout: "2", + }, + { + file: "/out/bar/world/a/a/a/a/a/a/a/file.js", + stdout: "3", + }, + ], + }); }); diff --git a/test/bundler/esbuild/splitting.test.ts b/test/bundler/esbuild/splitting.test.ts index 0a7907ae5d..cb74494cbc 100644 --- a/test/bundler/esbuild/splitting.test.ts +++ b/test/bundler/esbuild/splitting.test.ts @@ -1,8 +1,10 @@ -// @known-failing-on-windows: panic "TODO on Windows" import assert from "assert"; import { readdirSync } from "fs"; import { itBundled, testForFile } from "../expectBundled"; var { describe, test, expect } = testForFile(import.meta.path); +import process from "node:process"; + +const isWindows = process.platform === "win32"; // Tests ported from: // https://github.com/evanw/esbuild/blob/main/internal/bundler_tests/bundler_splitting_test.go @@ -276,7 +278,9 @@ describe("bundler", () => { { file: "/out/b.js", stdout: "[null]" }, ], bundleWarnings: { - "/common.js": [`Import "missing" will always be undefined because there is no matching export in "empty.js"`], + [isWindows ? "\\common.js" : "/common.js"]: [ + `Import "missing" will always be undefined because there is no matching export in "empty.js"`, + ], }, }); itBundled("splitting/ReExportESBuildIssue273", { diff --git a/test/bundler/expectBundled.ts b/test/bundler/expectBundled.ts index 32b60bedb9..83bcc0e2a9 100644 --- a/test/bundler/expectBundled.ts +++ b/test/bundler/expectBundled.ts @@ -445,7 +445,7 @@ function expectBundled( backend = plugins !== undefined ? "api" : "cli"; } - const root = path.join(outBase, id.replaceAll("/", path.sep)); + const root = path.join(outBase, id); if (DEBUG) console.log("root:", root); const entryPaths = entryPoints.map(file => path.join(root, file)); @@ -1176,7 +1176,7 @@ for (const [key, blob] of build.outputs) { // check reference if (matchesReference) { const { ref } = matchesReference; - const theirRoot = path.join(outBase, ref.id.replaceAll("/", path.sep)); + const theirRoot = path.join(outBase, ref.id); if (!existsSync(theirRoot)) { expectBundled(ref.id, ref.options, false, true); if (!existsSync(theirRoot)) { @@ -1287,7 +1287,7 @@ for (const [key, blob] of build.outputs) { if (typeof run.stdout === "string") { const expected = dedent(run.stdout).trim(); if (expected !== result) { - console.log(`runtime failed file=${file}`); + console.log(`runtime failed file: ${file}`); console.log(`reference stdout:`); console.log(result); console.log(`---`); @@ -1295,7 +1295,7 @@ for (const [key, blob] of build.outputs) { expect(result).toBe(expected); } else { if (!run.stdout.test(result)) { - console.log(`runtime failed file=${file}`); + console.log(`runtime failed file: ${file}`); console.log(`reference stdout:`); console.log(result); console.log(`---`); @@ -1330,7 +1330,7 @@ export function itBundled( ): BundlerTestRef { if (typeof opts === "function") { const fn = opts; - opts = opts({ root: path.join(outBase, id.replaceAll("/", path.sep)), getConfigRef }); + opts = opts({ root: path.join(outBase, id), getConfigRef }); // @ts-expect-error opts._referenceFn = fn; } From 91cfd614cae695c399dc59b5aa138180aa38677d Mon Sep 17 00:00:00 2001 From: dave caruso Date: Wed, 31 Jan 2024 22:37:03 -0800 Subject: [PATCH 32/45] fix(window): some more bin things (#8612) * allow linking bins that do not exist. * fix some things --- src/cli.zig | 128 +++++++++++---------- src/cli/run_command.zig | 9 +- src/install/windows-shim/bun_shim_impl.exe | Bin 10752 -> 10752 bytes src/install/windows-shim/bun_shim_impl.zig | 8 +- src/string_immutable.zig | 7 ++ 5 files changed, 88 insertions(+), 64 deletions(-) diff --git a/src/cli.zig b/src/cli.zig index 0bb6dbf966..a6e1791b93 100644 --- a/src/cli.zig +++ b/src/cli.zig @@ -1168,15 +1168,12 @@ pub const Command = struct { } }; - const exe_suffix = if (Environment.isWindows) ".exe" else ""; - pub fn isBunX(argv0: []const u8) bool { - return strings.endsWithComptime(argv0, "bunx" ++ exe_suffix) or - (Environment.isDebug and strings.endsWithComptime(argv0, "bunx-debug" ++ exe_suffix)); + return strings.endsWithComptime(argv0, "bunx") or (Environment.isDebug and strings.endsWithComptime(argv0, "bunx-debug")); } pub fn isNode(argv0: []const u8) bool { - return strings.endsWithComptime(argv0, "node" ++ exe_suffix); + return strings.endsWithComptime(argv0, "node"); } pub fn which() Tag { @@ -1184,10 +1181,15 @@ pub const Command = struct { const argv0 = args_iter.next() orelse return .HelpCommand; - // symlink is argv[0] - if (isBunX(argv0)) return .BunxCommand; + const without_exe = if (Environment.isWindows) + strings.withoutSuffixComptime(argv0, ".exe") + else + argv0; - if (isNode(argv0)) { + // symlink is argv[0] + if (isBunX(without_exe)) return .BunxCommand; + + if (isNode(without_exe)) { @import("./deps/zig-clap/clap/streaming.zig").warn_on_unrecognized_flag = false; pretend_to_be_node = true; return .RunAsNodeCommand; @@ -1626,7 +1628,7 @@ pub const Command = struct { const ctx = try Command.Context.create(allocator, log, .RunCommand); if (ctx.positionals.len > 0) { - if (try RunCommand.exec(ctx, false, true)) { + if (try RunCommand.exec(ctx, false, true, false)) { return; } @@ -1743,7 +1745,7 @@ pub const Command = struct { } if (ctx.positionals.len > 0 and extension.len == 0) { - if (try RunCommand.exec(ctx, true, false)) { + if (try RunCommand.exec(ctx, true, false, true)) { return; } @@ -1789,61 +1791,71 @@ pub const Command = struct { var absolute_script_path: ?string = null; + // TODO: optimize this pass for Windows. we can make better use of system apis available var file_path = script_name_to_search; - const file_: anyerror!std.fs.File = brk: { - if (std.fs.path.isAbsoluteWindows(script_name_to_search)) { - var win_resolver = resolve_path.PosixToWinNormalizer{}; - var resolved = win_resolver.resolveCWD(script_name_to_search) catch @panic("Could not resolve path"); - if (comptime Environment.isWindows) { - resolved = resolve_path.normalizeString(resolved, true, .windows); - } - absolute_script_path = resolved; - break :brk bun.openFile( - resolved, - .{ .mode = .read_only }, - ); - } else if (!strings.hasPrefix(script_name_to_search, "..") and script_name_to_search[0] != '~') { - const file_pathZ = brk2: { - @memcpy(script_name_buf[0..file_path.len], file_path); + { + const file = bun.toLibUVOwnedFD(((brk: { + if (std.fs.path.isAbsolute(script_name_to_search)) { + var win_resolver = resolve_path.PosixToWinNormalizer{}; + var resolved = win_resolver.resolveCWD(script_name_to_search) catch @panic("Could not resolve path"); + if (comptime Environment.isWindows) { + resolved = resolve_path.normalizeString(resolved, true, .windows); + } + absolute_script_path = resolved; + break :brk bun.openFile( + resolved, + .{ .mode = .read_only }, + ); + } else if (!strings.hasPrefix(script_name_to_search, "..") and script_name_to_search[0] != '~') { + const file_pathZ = brk2: { + @memcpy(script_name_buf[0..file_path.len], file_path); + script_name_buf[file_path.len] = 0; + break :brk2 script_name_buf[0..file_path.len :0]; + }; + + break :brk bun.openFileZ(file_pathZ, .{ .mode = .read_only }); + } else { + var path_buf: [bun.MAX_PATH_BYTES]u8 = undefined; + const cwd = bun.getcwd(&path_buf) catch return false; + path_buf[cwd.len] = std.fs.path.sep; + var parts = [_]string{script_name_to_search}; + file_path = resolve_path.joinAbsStringBuf( + path_buf[0 .. cwd.len + 1], + &script_name_buf, + &parts, + .auto, + ); + if (file_path.len == 0) return false; script_name_buf[file_path.len] = 0; - break :brk2 script_name_buf[0..file_path.len :0]; - }; + const file_pathZ = script_name_buf[0..file_path.len :0]; + break :brk bun.openFileZ(file_pathZ, .{ .mode = .read_only }); + } + }) catch return false).handle); + defer _ = bun.sys.close(file); - break :brk bun.openFileZ(file_pathZ, .{ .mode = .read_only }); - } else { - var path_buf: [bun.MAX_PATH_BYTES]u8 = undefined; - const cwd = bun.getcwd(&path_buf) catch return false; - path_buf[cwd.len] = std.fs.path.sep; - var parts = [_]string{script_name_to_search}; - file_path = resolve_path.joinAbsStringBuf( - path_buf[0 .. cwd.len + 1], - &script_name_buf, - &parts, - .auto, - ); - if (file_path.len == 0) return false; - script_name_buf[file_path.len] = 0; - const file_pathZ = script_name_buf[0..file_path.len :0]; - break :brk bun.openFileZ(file_pathZ, .{ .mode = .read_only }); + switch (bun.sys.fstat(file)) { + .result => |stat| { + // directories cannot be run. if only there was a faster way to check this + if (bun.S.ISDIR(@intCast(stat.mode))) return false; + }, + .err => return false, } - }; - const file = file_ catch return false; + Global.configureAllocator(.{ .long_running = true }); - Global.configureAllocator(.{ .long_running = true }); + // the case where this doesn't work is if the script name on disk doesn't end with a known JS-like file extension + absolute_script_path = absolute_script_path orelse brk: { + if (comptime !Environment.isWindows) break :brk bun.getFdPath(file, &script_name_buf) catch return false; - // the case where this doesn't work is if the script name on disk doesn't end with a known JS-like file extension - absolute_script_path = absolute_script_path orelse brk: { - if (comptime !Environment.isWindows) break :brk bun.getFdPath(file.handle, &script_name_buf) catch return false; - - var fd_path_buf: bun.PathBuffer = undefined; - const path = bun.getFdPath(file.handle, &fd_path_buf) catch return false; - break :brk resolve_path.normalizeString( - resolve_path.PosixToWinNormalizer.resolveCWDWithExternalBufZ(&script_name_buf, path) catch @panic("Could not resolve path"), - true, - .windows, - ); - }; + var fd_path_buf: bun.PathBuffer = undefined; + const path = bun.getFdPath(file, &fd_path_buf) catch return false; + break :brk resolve_path.normalizeString( + resolve_path.PosixToWinNormalizer.resolveCWDWithExternalBufZ(&script_name_buf, path) catch @panic("Could not resolve path"), + true, + .windows, + ); + }; + } if (!ctx.debug.loaded_bunfig) { bun.CLI.Arguments.loadConfigPath(ctx.allocator, true, "bunfig.toml", ctx, .RunCommand) catch {}; diff --git a/src/cli/run_command.zig b/src/cli/run_command.zig index 3466b2e10f..963500fcb1 100644 --- a/src/cli/run_command.zig +++ b/src/cli/run_command.zig @@ -1098,7 +1098,12 @@ pub const RunCommand = struct { } } - pub fn exec(ctx_: Command.Context, comptime bin_dirs_only: bool, comptime log_errors: bool) !bool { + pub fn exec( + ctx_: Command.Context, + comptime bin_dirs_only: bool, + comptime log_errors: bool, + comptime did_try_open_with_bun_js: bool, + ) !bool { var ctx = ctx_; // Step 1. Figure out what we're trying to run var positionals = ctx.positionals; @@ -1138,7 +1143,7 @@ pub const RunCommand = struct { return true; } - if (log_errors or force_using_bun) { + if (!did_try_open_with_bun_js and (log_errors or force_using_bun)) { if (script_name_to_search.len > 0) { possibly_open_with_bun_js: { const ext = std.fs.path.extension(script_name_to_search); diff --git a/src/install/windows-shim/bun_shim_impl.exe b/src/install/windows-shim/bun_shim_impl.exe index e2e560b5d03ff04d1bc2dd73e69e0bdff2fe582c..5a81cbcafbc4b88ed611e48686e5e89f1ead917d 100755 GIT binary patch delta 562 zcmZn&X$YCn!2DQk_r_*cMn<*Gos2V?7_BBNvxGAWO|E9y#TY)>jP*NX{$xEi1xCHe z{%qb%pI9f)VcTX}#m2zUdVs$*myvK+b2vwx^)#u(g4VO zu^PnMHo2X>h&vd>i~|vwlUX?Yc#As^c=fs{@OfC?C{3Rn&moy4!U8nT>3EBZMd32YkBzSgy zcyS4+#FO#+%f~>e7h4$^7(BXrR2qQvuKz&r!g=z24oRDzKx<&)FKVEi4xpkJ`9My$ zk4nsoWk81Gw__}&Tra9Y64O9LB9Pc@&zZ`|n6Y^&mnCDp9tY5%WR^w`#uNV^c=WOc zYBDe!2GUs|dKZvj0CMECKpbfZXA)RO4#HUoRUQMSFM}l|Ad*2~ZK@Cs3z#DX;mCqH zN(>CFAmeK`C-E3CN_>)JU^oK@|5aZ>`IEbNEhcZ`HJJQ~S8ua7-&V%S0esTBx4-}Y nFZAR8{~th_6-Yk@(n>(u6NsgNI1GsOf%r90+-9?`z-37Q+GEih delta 593 zcmZn&X$YCnz`W`3u8qyCjErWRI~iv(F?vl_W(j9hnq1AYi!pt&8S8h(`pJ513XFD> z{n@;knAj%IVcTZf#m2zUdVs&Rh>?Ne#abXIt@#HNf6FEymw)>K+b2vwx^+EB(gDbP zu^YrXHo2X>h&vg?%mWdXlUX?Ycq=*&c=fs{@OfC?D9xE1&mmc_!U8nT>3EBZMWSD$9A9{5D^6mWb;y*J3gD>Orh8mR!piRI2{{QdO`O~BGlSg-n zN`hzShZpxi3VLnIU)}@Cy*R+Yz~Ir{qtXDRcl`%~7k;b^498tmV*Vq^c=U#-#CW`5 z0ZPFXzi0=#2E^$As(VodbR1eTG5 za3(^P`#|Z#U`Yvxq!U=1DunX|EF%Ts@PcKO7#LVV#^-Df;xS;HyolFe@*`e_$)9-b z#Qv);l4M{w0|)c0N||G(0Y|Nl9FSQLmq1NlZkIueMrfH)0^?Sc3= MP~2y;uE1qU0JX~4z5oCK diff --git a/src/install/windows-shim/bun_shim_impl.zig b/src/install/windows-shim/bun_shim_impl.zig index ef918cfc72..278beeef27 100644 --- a/src/install/windows-shim/bun_shim_impl.zig +++ b/src/install/windows-shim/bun_shim_impl.zig @@ -609,9 +609,9 @@ fn launcher(bun_ctx: anytype) noreturn { // Copy the filename in. There is no leading " but there is a trailing " // BUF1: '\??\C:\Users\dave\project\node_modules\my-cli\src\app.js"#node #####!!!!!!!!!!' - // ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^ ^ read_ptr + // ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^ ^ read_ptr // BUF2: 'node "C:\Users\dave\project\node_modules\my-cli\src\app.js"!!!!!!!!!!!!!!!!!!!!' - const length_of_filename_u8 = (@intFromPtr(read_ptr) - (2 * "\x00".len)) - @intFromPtr(buf1_u8); + const length_of_filename_u8 = @intFromPtr(read_ptr) - @intFromPtr(buf1_u8) - nt_object_prefix.len - 6; @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], @@ -623,10 +623,10 @@ fn launcher(bun_ctx: anytype) noreturn { // | |filename_len where the user args go // | the quote // shebang_arg_len - read_ptr = @ptrFromInt(@intFromPtr(buf2_u8) + shebang_arg_len_u8 + length_of_filename_u8 + 2 * "\"".len); + read_ptr = @ptrFromInt(@intFromPtr(buf2_u8) + length_of_filename_u8 + 2 * "\"\"".len + 2 * nt_object_prefix.len); if (user_arguments_u8.len > 0) { @memcpy(@as([*]u8, @ptrCast(read_ptr)), user_arguments_u8); - read_ptr += user_arguments_u8.len; + read_ptr = @ptrFromInt(@intFromPtr(read_ptr) + user_arguments_u8.len); } // BUF2: 'node "C:\Users\dave\project\node_modules\my-cli\src\app.js" --flags#!!!!!!!!!!' diff --git a/src/string_immutable.zig b/src/string_immutable.zig index abf716e0f7..8ec698fc1e 100644 --- a/src/string_immutable.zig +++ b/src/string_immutable.zig @@ -5948,3 +5948,10 @@ pub inline fn indexOfScalar(input: anytype, scalar: std.meta.Child(@TypeOf(input pub fn containsScalar(input: anytype, item: std.meta.Child(@TypeOf(input))) bool { return indexOfScalar(input, item) != null; } + +pub fn withoutSuffixComptime(input: []const u8, comptime suffix: []const u8) []const u8 { + if (hasSuffixComptime(input, suffix)) { + return input[0 .. input.len - suffix.len]; + } + return input; +} From 4f98336f86477d5541f77c0375a931bec2abff45 Mon Sep 17 00:00:00 2001 From: Dylan Conway <35280289+dylan-conway@users.noreply.github.com> Date: Wed, 31 Jan 2024 22:44:23 -0800 Subject: [PATCH 33/45] fix(windows): fix installing non-ascii paths and make `normalizeBuf` generic (#8608) * comptime type normalizeStringBuf * delete * revert * revert revert * revert * remove unused * remove unnecessary assert * add comment * remove normalize, need ../ * use Output.err * update error message * generic T suffix * fix windows build * more fix build * more fix build * mkdiratZ * update test * [autofix.ci] apply automated fixes * update snapshot again * fix merge --------- Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> --- src/install/migration.zig | 13 +- src/install/resolution.zig | 1 + src/libarchive/libarchive-bindings.zig | 2 +- src/libarchive/libarchive.zig | 77 +++-- src/resolver/resolve_path.zig | 301 ++++++++++++++---- src/string_immutable.zig | 82 +++-- src/sys.zig | 2 +- test/bundler/expectBundled.ts | 2 +- .../__snapshots__/inspect-error.test.js.snap | 14 +- test/js/bun/util/inspect-error.test.js | 18 +- 10 files changed, 372 insertions(+), 140 deletions(-) diff --git a/src/install/migration.zig b/src/install/migration.zig index 5a058963f3..8c404bce59 100644 --- a/src/install/migration.zig +++ b/src/install/migration.zig @@ -387,9 +387,20 @@ pub fn migrateNPMLockfile(this: *Lockfile, allocator: Allocator, log: *logger.Lo try this.workspace_paths.ensureTotalCapacity(allocator, wksp.map.unmanaged.entries.len); try this.workspace_versions.ensureTotalCapacity(allocator, wksp.map.unmanaged.entries.len); + var path_buf: if (Environment.isWindows) bun.PathBuffer else void = undefined; for (wksp.map.keys(), wksp.map.values()) |k, v| { const name_hash = stringHash(v.name); - this.workspace_paths.putAssumeCapacity(name_hash, builder.append(String, k)); + + this.workspace_paths.putAssumeCapacity( + name_hash, + builder.append( + String, + if (comptime Environment.isWindows) + bun.path.normalizeBuf(k, &path_buf, .windows) + else + k, + ), + ); if (v.version) |version_string| { const sliced_version = Semver.SlicedString.init(version_string, version_string); diff --git a/src/install/resolution.zig b/src/install/resolution.zig index 040d2c0fda..bc8f14eed4 100644 --- a/src/install/resolution.zig +++ b/src/install/resolution.zig @@ -9,6 +9,7 @@ const ExtractTarball = @import("./extract_tarball.zig"); const strings = @import("../string_immutable.zig"); const VersionedURL = @import("./versioned_url.zig").VersionedURL; const bun = @import("root").bun; +const Path = bun.path; pub const Resolution = extern struct { tag: Tag = .uninitialized, diff --git a/src/libarchive/libarchive-bindings.zig b/src/libarchive/libarchive-bindings.zig index 73a6cc8cb7..33b0766b29 100644 --- a/src/libarchive/libarchive-bindings.zig +++ b/src/libarchive/libarchive-bindings.zig @@ -1,5 +1,5 @@ const bun = @import("root").bun; -pub const wchar_t = c_int; +pub const wchar_t = u16; pub const la_int64_t = i64; pub const la_ssize_t = isize; pub const struct_archive = opaque {}; diff --git a/src/libarchive/libarchive.zig b/src/libarchive/libarchive.zig index 7f3bfeccee..d5d00754e0 100644 --- a/src/libarchive/libarchive.zig +++ b/src/libarchive/libarchive.zig @@ -489,7 +489,7 @@ pub const Archive = struct { var count: u32 = 0; const dir_fd = dir.fd; - var w_path: if (Environment.isWindows) bun.WPathBuffer else void = undefined; + var w_path_buf: if (Environment.isWindows) bun.WPathBuffer else void = undefined; loop: while (true) { const r = @as(Status, @enumFromInt(lib.archive_read_next_header(archive, &entry))); @@ -499,19 +499,40 @@ pub const Archive = struct { Status.retry => continue :loop, Status.failed, Status.fatal => return error.Fail, else => { - var pathname: [:0]const u8 = bun.sliceTo(lib.archive_entry_pathname(entry).?, 0); + // TODO: + // Due to path separator replacement and other copies that happen internally, libarchive changes the + // storage type of paths on windows to wide character strings. Using `archive_entry_pathname` or `archive_entry_pathname_utf8` + // on an wide character string will return null if there are non-ascii characters. + // (this can be seen by installing @fastify/send, which has a path "@fastify\send\test\fixtures\snow ☃") + // + // Ideally, we find a way to tell libarchive to not convert the strings to wide characters and also to not + // replace path separators. We can do both of these with our own normalization and utf8/utf16 string conversion code. + var pathname: bun.OSPathSliceZ = if (comptime Environment.isWindows) brk: { + const normalized = bun.path.normalizeBufT( + u16, + std.mem.span(lib.archive_entry_pathname_w(entry)), + &w_path_buf, + .windows, + ); + w_path_buf[normalized.len] = 0; + break :brk w_path_buf[0..normalized.len :0]; + } else std.mem.sliceTo(lib.archive_entry_pathname(entry), 0); if (comptime ContextType != void and @hasDecl(std.meta.Child(ContextType), "onFirstDirectoryName")) { if (appender.needs_first_dirname) { - appender.onFirstDirectoryName(strings.withoutTrailingSlash(bun.asByteSlice(pathname))); + if (comptime Environment.isWindows) { + const list = std.ArrayList(u8).init(default_allocator); + var result = try strings.toUTF8ListWithType(list, []const u16, pathname[0..pathname.len]); + // onFirstDirectoryName copies the contents of pathname to another buffer, safe to free + defer result.deinit(); + appender.onFirstDirectoryName(strings.withoutTrailingSlash(result.items)); + } else { + appender.onFirstDirectoryName(strings.withoutTrailingSlash(bun.asByteSlice(pathname))); + } } } - var tokenizer = if (comptime Environment.isWindows) - // TODO(dylan-conway): I think this should only be '/' - std.mem.tokenizeAny(u8, bun.asByteSlice(pathname), "/\\") - else - std.mem.tokenizeScalar(u8, bun.asByteSlice(pathname), '/'); + var tokenizer = std.mem.tokenizeScalar(bun.OSPathChar, pathname, std.fs.path.sep); comptime var depth_i: usize = 0; inline while (depth_i < depth_to_skip) : (depth_i += 1) { @@ -519,15 +540,15 @@ pub const Archive = struct { } const pathname_ = tokenizer.rest(); - pathname = @as([*]const u8, @ptrFromInt(@intFromPtr(pathname_.ptr)))[0..pathname_.len :0]; + pathname = @as([*]const bun.OSPathChar, @ptrFromInt(@intFromPtr(pathname_.ptr)))[0..pathname_.len :0]; if (pathname.len == 0) continue; const kind = C.kindFromMode(lib.archive_entry_filetype(entry)); - const slice = bun.asByteSlice(pathname); + const path_slice: bun.OSPathSlice = pathname.ptr[0..pathname.len]; if (comptime log) { - Output.prettyln(" {s}", .{pathname}); + Output.prettyln(" {}", .{bun.fmt.fmtOSPath(path_slice)}); } count += 1; @@ -546,15 +567,15 @@ pub const Archive = struct { mode |= 0o1; if (comptime Environment.isWindows) { - std.os.mkdirat(dir_fd, pathname, @as(u32, @intCast(mode))) catch |err| { + std.os.mkdiratW(dir_fd, pathname, @as(u32, @intCast(mode))) catch |err| { if (err == error.PathAlreadyExists or err == error.NotDir) break; - try bun.makePath(dir, std.fs.path.dirname(slice) orelse return err); - try std.os.mkdirat(dir_fd, pathname, 0o777); + try bun.MakePath.makePath(u16, dir, bun.Dirname.dirname(u16, path_slice) orelse return err); + try std.os.mkdiratW(dir_fd, pathname, 0o777); }; } else { std.os.mkdiratZ(dir_fd, pathname, @as(u32, @intCast(mode))) catch |err| { if (err == error.PathAlreadyExists or err == error.NotDir) break; - try bun.makePath(dir, std.fs.path.dirname(slice) orelse return err); + try bun.makePath(dir, std.fs.path.dirname(path_slice) orelse return err); try std.os.mkdiratZ(dir_fd, pathname, 0o777); }; } @@ -567,7 +588,7 @@ pub const Archive = struct { std.os.symlinkatZ(link_target, dir_fd, pathname) catch |err| brk: { switch (err) { error.AccessDenied, error.FileNotFound => { - dir.makePath(std.fs.path.dirname(slice) orelse return err) catch {}; + dir.makePath(std.fs.path.dirname(path_slice) orelse return err) catch {}; break :brk try std.os.symlinkatZ(link_target, dir_fd, pathname); }, else => { @@ -582,13 +603,12 @@ pub const Archive = struct { const file_handle_native = brk: { if (Environment.isWindows) { const flags = std.os.O.WRONLY | std.os.O.CREAT | std.os.O.TRUNC; - const os_path = bun.strings.toWPathNormalized(&w_path, slice); - switch (bun.sys.openatWindows(bun.toFD(dir_fd), os_path, flags)) { + switch (bun.sys.openatWindows(bun.toFD(dir_fd), pathname, flags)) { .result => |fd| break :brk fd, .err => |e| switch (e.errno) { @intFromEnum(bun.C.E.PERM), @intFromEnum(bun.C.E.NOENT) => { - dir.makePath(std.fs.path.dirname(slice) orelse return bun.errnoToZigErr(e.errno)) catch {}; - break :brk try bun.sys.openatWindows(bun.toFD(dir_fd), os_path, flags).unwrap(); + bun.MakePath.makePath(u16, dir, bun.Dirname.dirname(u16, path_slice) orelse return bun.errnoToZigErr(e.errno)) catch {}; + break :brk try bun.sys.openatWindows(bun.toFD(dir_fd), pathname, flags).unwrap(); }, else => { return bun.errnoToZigErr(e.errno); @@ -599,7 +619,7 @@ pub const Archive = struct { break :brk (dir.createFileZ(pathname, .{ .truncate = true, .mode = mode }) catch |err| { switch (err) { error.AccessDenied, error.FileNotFound => { - dir.makePath(std.fs.path.dirname(slice) orelse return err) catch {}; + dir.makePath(std.fs.path.dirname(path_slice) orelse return err) catch {}; break :brk (try dir.createFileZ(pathname, .{ .truncate = true, .mode = mode, @@ -636,14 +656,14 @@ pub const Archive = struct { if (size > 0) { if (ctx) |ctx_| { const hash: u64 = if (ctx_.pluckers.len > 0) - bun.hash(slice) + bun.hash(std.mem.sliceAsBytes(path_slice)) else @as(u64, 0); if (comptime ContextType != void and @hasDecl(std.meta.Child(ContextType), "appendMutable")) { const result = ctx.?.all_files.getOrPutAdapted(hash, Context.U64Context{}) catch unreachable; if (!result.found_existing) { - result.value_ptr.* = (try appender.appendMutable(@TypeOf(slice), slice)).ptr; + result.value_ptr.* = (try appender.appendMutable(@TypeOf(path_slice), path_slice)).ptr; } } @@ -678,13 +698,20 @@ pub const Archive = struct { lib.ARCHIVE_OK => break :possibly_retry, lib.ARCHIVE_RETRY => { if (comptime log) { - Output.prettyErrorln("[libarchive] Error extracting {s}, retry {d} / {d}", .{ pathname_, retries_remaining, 5 }); + Output.err("libarchive error", "extracting {}, retry {d} / {d}", .{ + bun.fmt.fmtOSPath(path_slice), + retries_remaining, + 5, + }); } }, else => { if (comptime log) { const archive_error = std.mem.span(lib.archive_error_string(archive)); - Output.prettyErrorln("[libarchive] Error extracting {s}: {s}", .{ pathname_, archive_error }); + Output.err("libarchive error", "extracting {}: {s}", .{ + bun.fmt.fmtOSPath(path_slice), + archive_error, + }); } return error.Fail; }, diff --git a/src/resolver/resolve_path.zig b/src/resolver/resolve_path.zig index 625cf0700f..663acd1b2d 100644 --- a/src/resolver/resolve_path.zig +++ b/src/resolver/resolve_path.zig @@ -34,12 +34,19 @@ inline fn nqlAtIndexCaseInsensitive(comptime string_count: comptime_int, index: } const IsSeparatorFunc = fn (char: u8) bool; +const IsSeparatorFuncT = fn (comptime T: type, char: anytype) bool; const LastSeparatorFunction = fn (slice: []const u8) ?usize; +const LastSeparatorFunctionT = fn (comptime T: type, slice: anytype) ?usize; inline fn @"is .."(slice: []const u8) bool { return slice.len >= 2 and @as(u16, @bitCast(slice[0..2].*)) == comptime std.mem.readInt(u16, "..", .little); } +inline fn @"is .. with type"(comptime T: type, slice: []const T) bool { + if (comptime T == u8) return @"is .."(slice); + return slice.len >= 2 and slice[0] == '.' and slice[1] == '.'; +} + inline fn isDotSlash(slice: []const u8) bool { return @as(u16, @bitCast(slice[0..2].*)) == comptime std.mem.readInt(u16, "./", .little); } @@ -517,6 +524,9 @@ pub fn relativeAlloc(allocator: std.mem.Allocator, from: []const u8, to: []const // https://cs.opensource.google/go/go/+/refs/tags/go1.17.6:src/path/filepath/path_windows.go;l=57 // volumeNameLen returns length of the leading volume name on Windows. fn windowsVolumeNameLen(path: []const u8) struct { usize, usize } { + return windowsVolumeNameLenT(u8, path); +} +fn windowsVolumeNameLenT(comptime T: type, path: []const T) struct { usize, usize } { if (path.len < 2) return .{ 0, 0 }; // with drive letter const c = path[0]; @@ -527,18 +537,32 @@ fn windowsVolumeNameLen(path: []const u8) struct { usize, usize } { } // UNC if (path.len >= 5 and - Platform.windows.isSeparator(path[0]) and - Platform.windows.isSeparator(path[1]) and - !Platform.windows.isSeparator(path[2]) and + Platform.windows.isSeparatorT(T, path[0]) and + Platform.windows.isSeparatorT(T, path[1]) and + !Platform.windows.isSeparatorT(T, path[2]) and path[2] != '.') { - if (strings.indexOfAny(path[3..], "/\\")) |idx| { - // TODO: handle input "//abc//def" should be picked up as a unc path - if (path.len > idx + 4 and !Platform.windows.isSeparator(path[idx + 4])) { - if (strings.indexOfAny(path[idx + 4 ..], "/\\")) |idx2| { - return .{ idx + idx2 + 4, idx + 3 }; - } else { - return .{ path.len, idx + 3 }; + if (T == u8) { + if (strings.indexOfAny(path[3..], "/\\")) |idx| { + // TODO: handle input "//abc//def" should be picked up as a unc path + if (path.len > idx + 4 and !Platform.windows.isSeparatorT(T, path[idx + 4])) { + if (strings.indexOfAny(path[idx + 4 ..], "/\\")) |idx2| { + return .{ idx + idx2 + 4, idx + 3 }; + } else { + return .{ path.len, idx + 3 }; + } + } + } + } else { + // TODO(dylan-conway): use strings.indexOfAny instead of std + if (std.mem.indexOfAny(T, path[3..], comptime strings.literal(T, "/\\"))) |idx| { + // TODO: handle input "//abc//def" should be picked up as a unc path + if (path.len > idx + 4 and !Platform.windows.isSeparatorT(T, path[idx + 4])) { + if (std.mem.indexOfAny(T, path[idx + 4 ..], comptime strings.literal(T, "/\\"))) |idx2| { + return .{ idx + idx2 + 4, idx + 3 }; + } else { + return .{ path.len, idx + 3 }; + } } } } @@ -550,30 +574,40 @@ pub fn windowsVolumeName(path: []const u8) []const u8 { return path[0..@call(.always_inline, windowsVolumeNameLen, .{path})[0]]; } -// path.relative lets you do relative across different share drives pub fn windowsFilesystemRoot(path: []const u8) []const u8 { + return windowsFilesystemRootT(u8, path); +} + +// path.relative lets you do relative across different share drives +pub fn windowsFilesystemRootT(comptime T: type, path: []const T) []const T { if (path.len < 3) - return if (isSepAny(path[0])) path[0..1] else path[0..0]; + return if (isSepAnyT(T, path[0])) path[0..1] else path[0..0]; // with drive letter const c = path[0]; - if (path[1] == ':' and isSepAny(path[2])) { + if (path[1] == ':' and isSepAnyT(T, path[2])) { if ('a' <= c and c <= 'z' or 'A' <= c and c <= 'Z') { return path[0..3]; } } // UNC if (path.len >= 5 and - Platform.windows.isSeparator(path[0]) and - Platform.windows.isSeparator(path[1]) and - !Platform.windows.isSeparator(path[2]) and + Platform.windows.isSeparatorT(T, path[0]) and + Platform.windows.isSeparatorT(T, path[1]) and + !Platform.windows.isSeparatorT(T, path[2]) and path[2] != '.') { - if (strings.indexOfAny(path[3..], "/\\")) |idx| { - // TODO: handle input "//abc//def" should be picked up as a unc path - return path[0 .. idx + 4]; + if (comptime T == u8) { + if (strings.indexOfAny(path[3..], "/\\")) |idx| { + // TODO: handle input "//abc//def" should be picked up as a unc path + return path[0 .. idx + 4]; + } + } else { + if (std.mem.indexOfAny(T, path[3..], "/\\")) |idx| { + return path[0 .. idx + 4]; + } } } - if (isSepAny(path[0])) return path[0..1]; + if (isSepAnyT(T, path[0])) return path[0..1]; return path[0..0]; } @@ -585,24 +619,34 @@ pub fn normalizeStringGeneric( comptime allow_above_root: bool, comptime separator: u8, comptime isSeparator: anytype, - _: anytype, comptime preserve_trailing_slash: bool, ) []u8 { - const isWindows = comptime separator == std.fs.path.sep_windows; + return normalizeStringGenericT(u8, path_, buf, allow_above_root, separator, isSeparator, preserve_trailing_slash); +} +pub fn normalizeStringGenericT( + comptime T: type, + path_: []const T, + buf: []T, + comptime allow_above_root: bool, + comptime separator: T, + comptime isSeparatorT: anytype, + comptime preserve_trailing_slash: bool, +) []T { + const isWindows, const sep_str = comptime .{ separator == std.fs.path.sep_windows, &[_]u8{separator} }; if (isWindows and bun.Environment.isDebug) { // this is here to catch a potential mistake by the caller // // since it is theoretically possible to get here in release // we will not do this check in release. - std.debug.assert(!strings.startsWith(path_, ":\\")); + std.debug.assert(!strings.hasPrefixComptimeType(T, path_, comptime strings.literal(T, ":\\"))); } var buf_i: usize = 0; var dotdot: usize = 0; const volLen, const indexOfThirdUNCSlash = if (isWindows and !allow_above_root) - windowsVolumeNameLen(path_) + windowsVolumeNameLenT(T, path_) else .{ 0, 0 }; @@ -610,7 +654,7 @@ pub fn normalizeStringGeneric( if (volLen > 0) { if (path_[1] != ':') { // UNC paths - buf[0..2].* = [_]u8{ separator, separator }; + buf[0..2].* = comptime strings.literalBuf(T, sep_str ++ sep_str); @memcpy(buf[2 .. indexOfThirdUNCSlash + 1], path_[2 .. indexOfThirdUNCSlash + 1]); buf[indexOfThirdUNCSlash] = separator; @memcpy( @@ -630,7 +674,7 @@ pub fn normalizeStringGeneric( buf_i = 2; dotdot = buf_i; } - } else if (path_.len > 0 and isSeparator(path_[0])) { + } else if (path_.len > 0 and isSeparatorT(T, path_[0])) { buf[buf_i] = separator; buf_i += 1; dotdot = 1; @@ -655,7 +699,7 @@ pub fn normalizeStringGeneric( if (isWindows and (allow_above_root or volLen > 0)) { // consume leading slashes on windows - if (r < n and isSeparator(path[r])) { + if (r < n and isSeparatorT(T, path[r])) { r += 1; buf[buf_i] = separator; buf_i += 1; @@ -666,31 +710,31 @@ pub fn normalizeStringGeneric( // empty path element // or // . element - if (isSeparator(path[r])) { + if (isSeparatorT(T, path[r])) { r += 1; continue; } - if (path[r] == '.' and (r + 1 == n or isSeparator(path[r + 1]))) { + if (path[r] == '.' and (r + 1 == n or isSeparatorT(T, path[r + 1]))) { // skipping two is a windows-specific bugfix r += 1; continue; } - if (@"is .."(path[r..]) and (r + 2 == n or isSeparator(path[r + 2]))) { + if (@"is .. with type"(T, path[r..]) and (r + 2 == n or isSeparatorT(T, path[r + 2]))) { r += 2; // .. element: remove to last separator if (buf_i > dotdot) { buf_i -= 1; - while (buf_i > dotdot and !isSeparator(buf[buf_i])) { + while (buf_i > dotdot and !isSeparatorT(T, buf[buf_i])) { buf_i -= 1; } } else if (allow_above_root) { if (buf_i > buf_start) { - buf[buf_i..][0..3].* = [_]u8{ separator, '.', '.' }; + buf[buf_i..][0..3].* = comptime strings.literalBuf(T, sep_str ++ ".."); buf_i += 3; } else { - buf[buf_i..][0..2].* = [_]u8{ '.', '.' }; + buf[buf_i..][0..2].* = comptime strings.literalBuf(T, ".."); buf_i += 2; } dotdot = buf_i; @@ -701,13 +745,13 @@ pub fn normalizeStringGeneric( // real path element. // add slash if needed - if (buf_i != buf_start and !isSeparator(buf[buf_i - 1])) { + if (buf_i != buf_start and !isSeparatorT(T, buf[buf_i - 1])) { buf[buf_i] = separator; buf_i += 1; } const from = r; - while (r < n and !isSeparator(path[r])) : (r += 1) {} + while (r < n and !isSeparatorT(T, path[r])) : (r += 1) {} const count = r - from; @memcpy(buf[buf_i..][0..count], path[from..][0..count]); buf_i += count; @@ -731,7 +775,7 @@ pub fn normalizeStringGeneric( const result = buf[0..buf_i]; if (bun.Environment.allow_assert and isWindows) { - std.debug.assert(!strings.startsWith(result, "\\:\\")); + std.debug.assert(!strings.hasPrefixComptimeType(T, result, comptime strings.literal(T, "\\:\\"))); } return result; @@ -744,12 +788,20 @@ pub const Platform = enum { posix, pub fn isAbsolute(comptime platform: Platform, path: []const u8) bool { + return isAbsoluteT(platform, u8, path); + } + + pub fn isAbsoluteT(comptime platform: Platform, comptime T: type, path: []const T) bool { + if (comptime T != u8 and T != u16) @compileError("Unsupported type given to isAbsoluteT"); return switch (comptime platform) { - .auto => (comptime platform.resolve()).isAbsolute(path), + .auto => (comptime platform.resolve()).isAbsoluteT(T, path), .posix => path.len > 0 and path[0] == '/', .windows, .loose, - => std.fs.path.isAbsoluteWindows(path), + => if (T == u8) + std.fs.path.isAbsoluteWindows(path) + else + std.fs.path.isAbsoluteWindowsWTF16(path), }; } @@ -789,6 +841,21 @@ pub const Platform = enum { } } + pub fn getSeparatorFuncT(comptime _platform: Platform) IsSeparatorFuncT { + switch (comptime _platform.resolve()) { + .auto => comptime unreachable, + .loose => { + return isSepAnyT; + }, + .windows => { + return isSepAnyT; + }, + .posix => { + return isSepPosixT; + }, + } + } + pub fn getLastSeparatorFunc(comptime _platform: Platform) LastSeparatorFunction { switch (comptime _platform.resolve()) { .auto => comptime unreachable, @@ -804,17 +871,36 @@ pub const Platform = enum { } } - pub inline fn isSeparator(comptime _platform: Platform, char: u8) bool { + pub fn getLastSeparatorFuncT(comptime _platform: Platform) LastSeparatorFunctionT { switch (comptime _platform.resolve()) { .auto => comptime unreachable, .loose => { - return isSepAny(char); + return lastIndexOfSeparatorLooseT; }, .windows => { - return isSepAny(char); + return lastIndexOfSeparatorWindowsT; }, .posix => { - return isSepPosix(char); + return lastIndexOfSeparatorPosixT; + }, + } + } + + pub inline fn isSeparator(comptime _platform: Platform, char: u8) bool { + return isSeparatorT(_platform, u8, char); + } + + pub inline fn isSeparatorT(comptime _platform: Platform, comptime T: type, char: T) bool { + switch (comptime _platform.resolve()) { + .auto => comptime unreachable, + .loose => { + return isSepAnyT(T, char); + }, + .windows => { + return isSepAnyT(T, char); + }, + .posix => { + return isSepPosixT(T, char); }, } } @@ -888,35 +974,57 @@ pub fn normalizeString(str: []const u8, comptime allow_above_root: bool, comptim } pub fn normalizeBuf(str: []const u8, buf: []u8, comptime _platform: Platform) []u8 { + return normalizeBufT(u8, str, buf, _platform); +} + +pub fn normalizeBufT(comptime T: type, str: []const T, buf: []T, comptime _platform: Platform) []T { if (str.len == 0) { buf[0] = '.'; return buf[0..1]; } - const is_absolute = _platform.isAbsolute(str); + const is_absolute = _platform.isAbsoluteT(T, str); - const trailing_separator = _platform.getLastSeparatorFunc()(str) == str.len - 1; + const trailing_separator = _platform.getLastSeparatorFuncT()(T, str) == str.len - 1; if (is_absolute and trailing_separator) - return normalizeStringBuf(str, buf, true, _platform, true); + return normalizeStringBufT(T, str, buf, true, _platform, true); if (is_absolute and !trailing_separator) - return normalizeStringBuf(str, buf, true, _platform, false); + return normalizeStringBufT(T, str, buf, true, _platform, false); if (!is_absolute and !trailing_separator) - return normalizeStringBuf(str, buf, false, _platform, false); + return normalizeStringBufT(T, str, buf, false, _platform, false); - return normalizeStringBuf(str, buf, false, _platform, true); + return normalizeStringBufT(T, str, buf, false, _platform, true); } -pub fn normalizeStringBuf(str: []const u8, buf: []u8, comptime allow_above_root: bool, comptime _platform: Platform, comptime preserve_trailing_slash: anytype) []u8 { +pub fn normalizeStringBuf( + str: []const u8, + buf: []u8, + comptime allow_above_root: bool, + comptime _platform: Platform, + comptime preserve_trailing_slash: anytype, +) []u8 { + return normalizeStringBufT(u8, str, buf, allow_above_root, _platform, preserve_trailing_slash); +} + +pub fn normalizeStringBufT( + comptime T: type, + str: []const T, + buf: []T, + comptime allow_above_root: bool, + comptime _platform: Platform, + comptime preserve_trailing_slash: anytype, +) []T { const platform = comptime _platform.resolve(); switch (comptime platform) { .auto => @compileError("unreachable"), .windows => { - return normalizeStringWindows( + return normalizeStringWindowsT( + T, str, buf, allow_above_root, @@ -924,7 +1032,8 @@ pub fn normalizeStringBuf(str: []const u8, buf: []u8, comptime allow_above_root: ); }, .posix => { - return normalizeStringLooseBuf( + return normalizeStringLooseBufT( + T, str, buf, allow_above_root, @@ -933,7 +1042,8 @@ pub fn normalizeStringBuf(str: []const u8, buf: []u8, comptime allow_above_root: }, .loose => { - return normalizeStringLooseBuf( + return normalizeStringLooseBufT( + T, str, buf, allow_above_root, @@ -1248,7 +1358,7 @@ fn _joinAbsStringBufWindows( // skip over volume name const volume = part[0..windowsVolumeNameLen(part)[0]]; - if (volume.len > 0 and !strings.eql(volume, root)) + if (volume.len > 0 and !strings.eqlLong(volume, root, true)) continue; const part_without_vol = part[volume.len..]; @@ -1278,23 +1388,48 @@ fn _joinAbsStringBufWindows( } pub fn isSepPosix(char: u8) bool { + return isSepPosixT(u8, char); +} + +pub fn isSepPosixT(comptime T: type, char: anytype) bool { + if (comptime @TypeOf(char) != T) @compileError("Incorrect type passed to isSepPosixT"); return char == std.fs.path.sep_posix; } pub fn isSepWin32(char: u8) bool { + return isSepWin32T(u8, char); +} + +pub fn isSepWin32T(comptime T: type, char: anytype) bool { + if (comptime @TypeOf(char) != T) @compileError("Incorrect type passed to isSepWin32T"); return char == std.fs.path.sep_windows; } pub fn isSepAny(char: u8) bool { - return @call(.always_inline, isSepPosix, .{char}) or @call(.always_inline, isSepWin32, .{char}); + return isSepAnyT(u8, char); +} + +pub fn isSepAnyT(comptime T: type, char: anytype) bool { + if (comptime @TypeOf(char) != T) @compileError("Incorrect type passed to isSepAnyT"); + return @call(.always_inline, isSepPosixT, .{ T, char }) or @call(.always_inline, isSepWin32T, .{ T, char }); } pub fn lastIndexOfSeparatorWindows(slice: []const u8) ?usize { - return std.mem.lastIndexOfAny(u8, slice, "\\/"); + return lastIndexOfSeparatorWindowsT(u8, slice); +} + +pub fn lastIndexOfSeparatorWindowsT(comptime T: type, slice: anytype) ?usize { + if (comptime std.meta.Child(@TypeOf(slice)) != T) @compileError("Invalid type passed to lastIndexOfSeparatorWindowsT"); + return std.mem.lastIndexOfAny(T, slice, comptime strings.literal(T, "\\/")); } pub fn lastIndexOfSeparatorPosix(slice: []const u8) ?usize { - return std.mem.lastIndexOfScalar(u8, slice, std.fs.path.sep_posix); + return lastIndexOfSeparatorPosixT(u8, slice); +} + +pub fn lastIndexOfSeparatorPosixT(comptime T: type, slice: anytype) ?usize { + if (comptime std.meta.Child(@TypeOf(slice)) != T) @compileError("Invalid type passed to lastIndexOfSeparatorPosixT"); + return std.mem.lastIndexOfScalar(T, slice, std.fs.path.sep_posix); } pub fn lastIndexOfNonSeparatorPosix(slice: []const u8) ?u32 { @@ -1309,7 +1444,12 @@ pub fn lastIndexOfNonSeparatorPosix(slice: []const u8) ?u32 { } pub fn lastIndexOfSeparatorLoose(slice: []const u8) ?usize { - return lastIndexOfSep(slice); + return lastIndexOfSeparatorLooseT(u8, slice); +} + +pub fn lastIndexOfSeparatorLooseT(comptime T: type, slice: anytype) ?usize { + if (comptime std.meta.Child(@TypeOf(slice)) != T) @compileError("Invalid type passed to lastIndexOfSeparatorLooseT"); + return lastIndexOfSepT(T, slice); } pub fn normalizeStringLooseBuf( @@ -1318,13 +1458,23 @@ pub fn normalizeStringLooseBuf( comptime allow_above_root: bool, comptime preserve_trailing_slash: bool, ) []u8 { - return normalizeStringGeneric( + return normalizeStringLooseBufT(u8, str, buf, allow_above_root, preserve_trailing_slash); +} + +pub fn normalizeStringLooseBufT( + comptime T: type, + str: []const T, + buf: []T, + comptime allow_above_root: bool, + comptime preserve_trailing_slash: bool, +) []T { + return normalizeStringGenericT( + T, str, buf, allow_above_root, std.fs.path.sep_posix, - isSepAny, - lastIndexOfSeparatorLoose, + isSepAnyT, preserve_trailing_slash, ); } @@ -1335,13 +1485,23 @@ pub fn normalizeStringWindows( comptime allow_above_root: bool, comptime preserve_trailing_slash: bool, ) []u8 { - return normalizeStringGeneric( + return normalizeStringWindowsT(u8, str, buf, allow_above_root, preserve_trailing_slash); +} + +pub fn normalizeStringWindowsT( + comptime T: type, + str: []const T, + buf: []T, + comptime allow_above_root: bool, + comptime preserve_trailing_slash: bool, +) []T { + return normalizeStringGenericT( + T, str, buf, allow_above_root, std.fs.path.sep_windows, - isSepAny, - lastIndexOfSeparatorWindows, + isSepAnyT, preserve_trailing_slash, ); } @@ -1368,16 +1528,14 @@ pub fn normalizeStringNode( buf_, true, comptime platform.resolve().separator(), - comptime platform.getSeparatorFunc(), - comptime platform.getLastSeparatorFunc(), + comptime platform.getSeparatorFuncT(), false, ) else normalizeStringGeneric( str, buf_, false, comptime platform.resolve().separator(), - comptime platform.getSeparatorFunc(), - comptime platform.getLastSeparatorFunc(), + comptime platform.getSeparatorFuncT(), false, ); @@ -1765,12 +1923,17 @@ pub fn basename(path: []const u8) []const u8 { return path[start_index + 1 .. end_index]; } + pub fn lastIndexOfSep(path: []const u8) ?usize { + return lastIndexOfSepT(u8, path); +} + +pub fn lastIndexOfSepT(comptime T: type, path: []const T) ?usize { if (comptime !bun.Environment.isWindows) { - return strings.lastIndexOfChar(path, '/'); + return strings.lastIndexOfCharT(T, path, '/'); } - return std.mem.lastIndexOfAny(u8, path, "/\\"); + return std.mem.lastIndexOfAny(T, path, "/\\"); } pub fn nextDirname(path_: []const u8) ?[]const u8 { diff --git a/src/string_immutable.zig b/src/string_immutable.zig index 8ec698fc1e..47ad783768 100644 --- a/src/string_immutable.zig +++ b/src/string_immutable.zig @@ -40,34 +40,51 @@ pub inline fn w(comptime str: []const u8) [:0]const u16 { } pub fn toUTF16Literal(comptime str: []const u8) []const u16 { - return comptime brk: { - comptime var output: [str.len]u16 = undefined; - - for (str, 0..) |c, i| { - output[i] = c; - } - - const Static = struct { - pub const literal: []const u16 = output[0..]; - }; - break :brk Static.literal; - }; + return comptime literal(u16, str); } -pub fn toUTF16LiteralZ(comptime str: []const u8) [:0]const u16 { - return comptime brk: { - comptime var output: [str.len + 1]u16 = undefined; +pub inline fn literal(comptime T: type, comptime str: string) []const T { + if (!@inComptime()) @compileError("strings.literal() should be called in a comptime context"); + comptime var output: [str.len]T = undefined; - for (str, 0..) |c, i| { - output[i] = c; - } - output[str.len] = 0; + for (str, 0..) |c, i| { + // TODO(dylan-conway): should we check for non-ascii characters like JSC does with operator""_s + output[i] = c; + } - const Static = struct { - pub const literal: [:0]const u16 = output[0..str.len :0]; - }; - break :brk Static.literal; + const Static = struct { + pub const literal: []const T = output[0..]; }; + return Static.literal; +} + +pub inline fn literalBuf(comptime T: type, comptime str: string) [str.len]T { + if (!@inComptime()) @compileError("strings.literalBuf() should be called in a comptime context"); + comptime var output: [str.len]T = undefined; + + for (str, 0..) |c, i| { + // TODO(dylan-conway): should we check for non-ascii characters like JSC does with operator""_s + output[i] = c; + } + + const Static = struct { + pub const literal: [str.len]T = output; + }; + return Static.literal; +} + +pub inline fn toUTF16LiteralZ(comptime str: []const u8) [:0]const u16 { + comptime var output: [str.len + 1]u16 = undefined; + + for (str, 0..) |c, i| { + output[i] = c; + } + output[str.len] = 0; + + const Static = struct { + pub const literal: [:0]const u16 = output[0..str.len :0]; + }; + return Static.literal; } pub const OptionalUsize = std.meta.Int(.unsigned, @bitSizeOf(usize) - 1); @@ -227,8 +244,12 @@ pub fn indexOfSigned(self: string, str: string) i32 { return @as(i32, @intCast(i)); } -pub inline fn lastIndexOfChar(self: string, char: u8) ?usize { - return std.mem.lastIndexOfScalar(u8, self, char); +pub inline fn lastIndexOfChar(self: []const u8, char: u8) ?usize { + return lastIndexOfCharT(u8, self, char); +} + +pub inline fn lastIndexOfCharT(comptime T: type, self: []const T, char: T) ?usize { + return std.mem.lastIndexOfScalar(T, self, char); } pub inline fn lastIndexOf(self: string, str: string) ?usize { @@ -860,13 +881,10 @@ pub fn hasPrefixComptimeType(comptime T: type, self: []const T, comptime alt: an const rhs = comptime switch (T) { u8 => alt, u16 => switch (std.meta.Child(@TypeOf(alt))) { - u8 => w(alt), - else => |t| switch (std.meta.Child(t)) { - u8 => w(alt), - else => alt, - }, + u16 => alt, + else => w(alt), }, - else => unreachable, + else => @compileError("Unsupported type given to hasPrefixComptimeType"), }; return self.len >= alt.len and eqlComptimeCheckLenWithType(T, self[0..rhs.len], rhs, false); } @@ -1713,7 +1731,7 @@ pub fn addNTPathPrefix(wbuf: []u16, utf16: []const u16) [:0]const u16 { } pub fn addNTPathPrefixIfNeeded(wbuf: []u16, utf16: []const u16) [:0]const u16 { - if (hasPrefixComptimeType(u16, utf16, &bun.windows.nt_object_prefix)) { + if (hasPrefixComptimeType(u16, utf16, bun.windows.nt_object_prefix)) { @memcpy(wbuf[0..utf16.len], utf16); wbuf[utf16.len] = 0; return wbuf[0..utf16.len :0]; diff --git a/src/sys.zig b/src/sys.zig index 537c126980..5a436707ea 100644 --- a/src/sys.zig +++ b/src/sys.zig @@ -598,7 +598,7 @@ pub fn ntCreateFile( // file specification, provided that the floppy driver and overlying file system are already // loaded. For more information, see File Names, Paths, and Namespaces. .ObjectName = &nt_name, - .RootDirectory = if (bun.strings.hasPrefixComptimeType(u16, path, &windows.nt_object_prefix)) + .RootDirectory = if (bun.strings.hasPrefixComptimeType(u16, path, windows.nt_object_prefix)) null else if (dir == bun.invalid_fd) std.fs.cwd().fd diff --git a/test/bundler/expectBundled.ts b/test/bundler/expectBundled.ts index 83bcc0e2a9..2cf4dff3ce 100644 --- a/test/bundler/expectBundled.ts +++ b/test/bundler/expectBundled.ts @@ -789,7 +789,7 @@ function expectBundled( const warningText = stderr!.toUnixString(); const allWarnings = warnParser(warningText).map(([error, source]) => { const [_str2, fullFilename, line, col] = source.match(/bun-build-tests[\/\\](.*):(\d+):(\d+)/)!; - const file = fullFilename.slice(id.length + path.basename(outBase).length + 1); + const file = fullFilename.slice(id.length + path.basename(outBase).length + 1).replaceAll("\\", "/"); return { error, file, line, col }; }); const expectedWarnings = bundleWarnings diff --git a/test/js/bun/util/__snapshots__/inspect-error.test.js.snap b/test/js/bun/util/__snapshots__/inspect-error.test.js.snap index 82555bf5c5..5bf59a64d0 100644 --- a/test/js/bun/util/__snapshots__/inspect-error.test.js.snap +++ b/test/js/bun/util/__snapshots__/inspect-error.test.js.snap @@ -23,15 +23,15 @@ error: error 1 `; exports[`Error 1`] = ` -" 6 | const err2 = new Error("error 2", { cause: err }); - 7 | expect(Bun.inspect(err2).replaceAll(import.meta.dir, "[dir]")).toMatchSnapshot(); - 8 | }); - 9 | -10 | test("Error", () => { -11 | const err = new Error("my message"); +"10 | .replaceAll("//", "/"), +11 | ).toMatchSnapshot(); +12 | }); +13 | +14 | test("Error", () => { +15 | const err = new Error("my message"); ^ error: my message - at [dir]/inspect-error.test.js:11:15 + at [dir]/inspect-error.test.js:15:15 " `; diff --git a/test/js/bun/util/inspect-error.test.js b/test/js/bun/util/inspect-error.test.js index f9f8feb701..56f72e4aea 100644 --- a/test/js/bun/util/inspect-error.test.js +++ b/test/js/bun/util/inspect-error.test.js @@ -4,12 +4,20 @@ import { test, expect } from "bun:test"; test("error.cause", () => { const err = new Error("error 1"); const err2 = new Error("error 2", { cause: err }); - expect(Bun.inspect(err2).replaceAll(import.meta.dir, "[dir]")).toMatchSnapshot(); + expect( + Bun.inspect(err2) + .replaceAll(import.meta.dir, "[dir]") + .replaceAll("\\", "/"), + ).toMatchSnapshot(); }); test("Error", () => { const err = new Error("my message"); - expect(Bun.inspect(err).replaceAll(import.meta.dir, "[dir]")).toMatchSnapshot(); + expect( + Bun.inspect(err) + .replaceAll(import.meta.dir, "[dir]") + .replaceAll("\\", "/"), + ).toMatchSnapshot(); }); test("BuildMessage", async () => { @@ -17,6 +25,10 @@ test("BuildMessage", async () => { await import("./inspect-error-fixture-bad.js"); expect.unreachable(); } catch (e) { - expect(Bun.inspect(e).replaceAll(import.meta.dir, "[dir]")).toMatchSnapshot(); + expect( + Bun.inspect(e) + .replaceAll(import.meta.dir, "[dir]") + .replaceAll("\\", "/"), + ).toMatchSnapshot(); } }); From 2af7cdedc24d3c29baba7e4d8c07e18648a85c95 Mon Sep 17 00:00:00 2001 From: Dylan Conway Date: Thu, 1 Feb 2024 00:34:34 -0800 Subject: [PATCH 34/45] path should be dirname --- src/bun.js/bindings/CommonJSModuleRecord.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/bun.js/bindings/CommonJSModuleRecord.cpp b/src/bun.js/bindings/CommonJSModuleRecord.cpp index c6f95d628b..48c03939b6 100644 --- a/src/bun.js/bindings/CommonJSModuleRecord.cpp +++ b/src/bun.js/bindings/CommonJSModuleRecord.cpp @@ -306,7 +306,7 @@ JSC_DEFINE_CUSTOM_GETTER(getterPath, (JSC::JSGlobalObject * globalObject, JSC::E if (UNLIKELY(!thisObject)) { return JSValue::encode(jsUndefined()); } - return JSValue::encode(thisObject->m_id.get()); + return JSValue::encode(thisObject->m_dirname.get()); } JSC_DEFINE_CUSTOM_GETTER(getterParent, (JSC::JSGlobalObject * globalObject, JSC::EncodedJSValue thisValue, JSC::PropertyName)) @@ -343,7 +343,7 @@ JSC_DEFINE_CUSTOM_SETTER(setterPath, if (!thisObject) return false; - thisObject->m_id.set(globalObject->vm(), thisObject, JSValue::decode(value).toString(globalObject)); + thisObject->m_dirname.set(globalObject->vm(), thisObject, JSValue::decode(value).toString(globalObject)); return true; } From 8191fbd970b77ddfcb11a3f2748a97ac5471156b Mon Sep 17 00:00:00 2001 From: Dylan Conway <35280289+dylan-conway@users.noreply.github.com> Date: Thu, 1 Feb 2024 02:19:18 -0800 Subject: [PATCH 35/45] fix: id instead of path (#8617) * use id * another path --- src/js/builtins/Module.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/js/builtins/Module.ts b/src/js/builtins/Module.ts index 01ca8db21c..ad098e93b6 100644 --- a/src/js/builtins/Module.ts +++ b/src/js/builtins/Module.ts @@ -12,7 +12,7 @@ export function require(this: CommonJSModuleRecord, id: string) { $overriddenName = "require"; $visibility = "Private"; export function overridableRequire(this: CommonJSModuleRecord, id: string) { - const existing = $requireMap.$get(id) || $requireMap.$get((id = $resolveSync(id, this.path, false))); + const existing = $requireMap.$get(id) || $requireMap.$get((id = $resolveSync(id, this.id, false))); if (existing) { // Scenario where this is necessary: // @@ -86,8 +86,8 @@ export function overridableRequire(this: CommonJSModuleRecord, id: string) { } $visibility = "Private"; -export function requireResolve(this: string | { path: string }, id: string) { - return $resolveSync(id, typeof this === "string" ? this : this?.path, false); +export function requireResolve(this: string | { id: string }, id: string) { + return $resolveSync(id, typeof this === "string" ? this : this?.id, false); } $visibility = "Private"; From ce1eba1954305763f308a7775cea783e946303c7 Mon Sep 17 00:00:00 2001 From: Andres Guarneros <34948183+guarner8@users.noreply.github.com> Date: Thu, 1 Feb 2024 15:38:21 -0500 Subject: [PATCH 36/45] Update quickstart.md (#8622) Update index.ts code snippet for consistency in quickstart.md. --- docs/quickstart.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/quickstart.md b/docs/quickstart.md index 73d487698e..9865c22f2b 100644 --- a/docs/quickstart.md +++ b/docs/quickstart.md @@ -124,12 +124,12 @@ Update `index.ts` to use `figlet` in the `fetch` handler. + import figlet from "figlet"; const server = Bun.serve({ - fetch() { + port: 3000, + fetch(req) { + const body = figlet.textSync("Bun!"); + return new Response(body); - return new Response("Bun!"); }, - port: 3000, }); ``` From c0fe042102f7ab93fcf02f2766152bff86d6ca19 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E3=81=B5=E3=81=8B=E3=81=AD=E3=81=93?= <67921654+fneco@users.noreply.github.com> Date: Fri, 2 Feb 2024 05:38:54 +0900 Subject: [PATCH 37/45] Update binary-data.md (#8619) Binary notation ("0b") is appropriate instead of hexadecimal notation ("0x"). --- docs/api/binary-data.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/api/binary-data.md b/docs/api/binary-data.md index c6f9df35bb..98c9a21119 100644 --- a/docs/api/binary-data.md +++ b/docs/api/binary-data.md @@ -70,14 +70,14 @@ const buf = new ArrayBuffer(4); const dv = new DataView(buf); dv.setUint8(0, 3); // write value 3 at byte offset 0 dv.getUint8(0); // => 3 -// [0x11, 0x0, 0x0, 0x0] +// [0b00000011, 0b00000000, 0b00000000, 0b00000000] ``` Now let's write a `Uint16` at byte offset `1`. This requires two bytes. We're using the value `513`, which is `2 * 256 + 1`; in bytes, that's `00000010 00000001`. ```ts dv.setUint16(1, 513); -// [0x11, 0x10, 0x1, 0x0] +// [0b00000011, 0b00000010, 0b00000001, 0b00000000] console.log(dv.getUint16(1)); // => 513 ``` From 0037dc2537297fbe3298a6a7e23809c0b470400b Mon Sep 17 00:00:00 2001 From: dave caruso Date: Thu, 1 Feb 2024 13:38:09 -0800 Subject: [PATCH 38/45] chore: update webkit --- CMakeLists.txt | 2 +- src/bun.js/WebKit | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 7b3fb3d6dc..c23c4b2001 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -3,7 +3,7 @@ cmake_policy(SET CMP0091 NEW) cmake_policy(SET CMP0067 NEW) set(Bun_VERSION "1.1.0") -set(WEBKIT_TAG 9c501b9aa712b7959f80dc99491e8758c151c20e) +set(WEBKIT_TAG c3712c13dcdc091cfe4c7cb8f2c1fd16472e6f92) set(BUN_WORKDIR "${CMAKE_CURRENT_BINARY_DIR}") message(STATUS "Configuring Bun ${Bun_VERSION} in ${BUN_WORKDIR}") diff --git a/src/bun.js/WebKit b/src/bun.js/WebKit index 54eae570cd..c3712c13dc 160000 --- a/src/bun.js/WebKit +++ b/src/bun.js/WebKit @@ -1 +1 @@ -Subproject commit 54eae570cdca140cff95a1dda35ee2eb7a3523ab +Subproject commit c3712c13dcdc091cfe4c7cb8f2c1fd16472e6f92 From 3a7df3f6c73436451efda305d69ce2e02f35384b Mon Sep 17 00:00:00 2001 From: Meghan Denny Date: Thu, 1 Feb 2024 13:45:47 -0800 Subject: [PATCH 39/45] windows: add more passing test (#8615) --- test/bundler/bundler_cjs2esm.test.ts | 1 - test/bundler/esbuild/importstar.test.ts | 1 - test/bundler/esbuild/loader.test.ts | 1 - test/bundler/esbuild/packagejson.test.ts | 1 - test/bundler/esbuild/splitting.test.ts | 7 +------ test/cli/hot/hot.test.ts | 1 - test/cli/install/bun-create.test.ts | 1 - test/cli/install/bun-remove.test.ts | 1 - test/cli/install/bun-update.test.ts | 1 - test/cli/install/migration/migrate.test.ts | 1 - test/cli/run/env.test.ts | 1 - test/cli/run/run-extensionless.test.ts | 1 - test/cli/test/bun-test.test.ts | 1 - test/js/bun/console/console-iterator.test.ts | 1 - test/js/bun/util/bun-isMainThread.test.js | 1 - test/js/bun/util/inspect-error.test.js | 1 - test/js/bun/util/text-loader.test.ts | 1 - test/js/node/buffer.test.js | 1 - test/js/node/events/event-emitter.test.ts | 1 - test/js/node/http/node-http.test.ts | 1 - test/js/node/worker_threads/worker_threads.test.ts | 1 - test/js/third_party/comlink/comlink.test.ts | 1 - test/js/third_party/resvg/bbox.test.js | 1 - test/js/third_party/rollup-v4/rollup-v4.test.ts | 1 - test/js/third_party/socket.io/socket.io-close.test.ts | 1 - .../socket.io/socket.io-connection-state-recovery.test.ts | 1 - test/js/web/workers/message-channel.test.ts | 1 - 27 files changed, 1 insertion(+), 32 deletions(-) diff --git a/test/bundler/bundler_cjs2esm.test.ts b/test/bundler/bundler_cjs2esm.test.ts index 5b34a27f8f..01a17356b6 100644 --- a/test/bundler/bundler_cjs2esm.test.ts +++ b/test/bundler/bundler_cjs2esm.test.ts @@ -1,4 +1,3 @@ -// @known-failing-on-windows: panic "TODO on Windows" import assert from "assert"; import dedent from "dedent"; import { itBundled, testForFile } from "./expectBundled"; diff --git a/test/bundler/esbuild/importstar.test.ts b/test/bundler/esbuild/importstar.test.ts index 4b49a68da0..cabe81a4b1 100644 --- a/test/bundler/esbuild/importstar.test.ts +++ b/test/bundler/esbuild/importstar.test.ts @@ -1,4 +1,3 @@ -// @known-failing-on-windows: panic "TODO on Windows" import assert from "assert"; import { itBundled, testForFile } from "../expectBundled"; var { describe, test, expect } = testForFile(import.meta.path); diff --git a/test/bundler/esbuild/loader.test.ts b/test/bundler/esbuild/loader.test.ts index 380f74eda3..c76fdd18ac 100644 --- a/test/bundler/esbuild/loader.test.ts +++ b/test/bundler/esbuild/loader.test.ts @@ -1,4 +1,3 @@ -// @known-failing-on-windows: panic "TODO on Windows" import fs from "fs"; import { itBundled, testForFile } from "../expectBundled"; var { describe, test, expect } = testForFile(import.meta.path); diff --git a/test/bundler/esbuild/packagejson.test.ts b/test/bundler/esbuild/packagejson.test.ts index 8fed430d38..5d459b968a 100644 --- a/test/bundler/esbuild/packagejson.test.ts +++ b/test/bundler/esbuild/packagejson.test.ts @@ -1,4 +1,3 @@ -// @known-failing-on-windows: panic "TODO on Windows" import { itBundled, testForFile } from "../expectBundled"; var { describe, test, expect } = testForFile(import.meta.path); diff --git a/test/bundler/esbuild/splitting.test.ts b/test/bundler/esbuild/splitting.test.ts index cb74494cbc..fbf5634fe7 100644 --- a/test/bundler/esbuild/splitting.test.ts +++ b/test/bundler/esbuild/splitting.test.ts @@ -2,9 +2,6 @@ import assert from "assert"; import { readdirSync } from "fs"; import { itBundled, testForFile } from "../expectBundled"; var { describe, test, expect } = testForFile(import.meta.path); -import process from "node:process"; - -const isWindows = process.platform === "win32"; // Tests ported from: // https://github.com/evanw/esbuild/blob/main/internal/bundler_tests/bundler_splitting_test.go @@ -278,9 +275,7 @@ describe("bundler", () => { { file: "/out/b.js", stdout: "[null]" }, ], bundleWarnings: { - [isWindows ? "\\common.js" : "/common.js"]: [ - `Import "missing" will always be undefined because there is no matching export in "empty.js"`, - ], + "/common.js": [`Import "missing" will always be undefined because there is no matching export in "empty.js"`], }, }); itBundled("splitting/ReExportESBuildIssue273", { diff --git a/test/cli/hot/hot.test.ts b/test/cli/hot/hot.test.ts index c3c97fc782..422f87eb67 100644 --- a/test/cli/hot/hot.test.ts +++ b/test/cli/hot/hot.test.ts @@ -1,4 +1,3 @@ -// @known-failing-on-windows: 1 failing import { spawn } from "bun"; import { expect, it } from "bun:test"; import { bunExe, bunEnv, tempDirWithFiles, bunRun, bunRunAsScript } from "harness"; diff --git a/test/cli/install/bun-create.test.ts b/test/cli/install/bun-create.test.ts index 50783d8c18..97ecb89714 100644 --- a/test/cli/install/bun-create.test.ts +++ b/test/cli/install/bun-create.test.ts @@ -1,4 +1,3 @@ -// @known-failing-on-windows: 1 failing import { spawn, spawnSync } from "bun"; import { afterEach, beforeEach, expect, it, describe } from "bun:test"; import { bunExe, bunEnv as env } from "harness"; diff --git a/test/cli/install/bun-remove.test.ts b/test/cli/install/bun-remove.test.ts index 7659215af7..2a302821af 100644 --- a/test/cli/install/bun-remove.test.ts +++ b/test/cli/install/bun-remove.test.ts @@ -1,4 +1,3 @@ -// @known-failing-on-windows: 1 failing import { bunExe, bunEnv as env } from "harness"; import { mkdir, mkdtemp, realpath, rm, writeFile } from "fs/promises"; import { join, relative } from "path"; diff --git a/test/cli/install/bun-update.test.ts b/test/cli/install/bun-update.test.ts index 4a5f53b61c..29dcec610d 100644 --- a/test/cli/install/bun-update.test.ts +++ b/test/cli/install/bun-update.test.ts @@ -1,4 +1,3 @@ -// @known-failing-on-windows: 1 failing import { file, listen, Socket, spawn } from "bun"; import { afterAll, afterEach, beforeAll, beforeEach, expect, it } from "bun:test"; import { bunExe, bunEnv as env, toBeValidBin, toHaveBins } from "harness"; diff --git a/test/cli/install/migration/migrate.test.ts b/test/cli/install/migration/migrate.test.ts index bd5c9f2795..5af930d5dc 100644 --- a/test/cli/install/migration/migrate.test.ts +++ b/test/cli/install/migration/migrate.test.ts @@ -1,4 +1,3 @@ -// @known-failing-on-windows: 1 failing import fs from "fs"; import { test, expect, beforeAll, afterAll } from "bun:test"; import { bunEnv, bunExe } from "harness"; diff --git a/test/cli/run/env.test.ts b/test/cli/run/env.test.ts index 6fcacd6ab9..a381338535 100644 --- a/test/cli/run/env.test.ts +++ b/test/cli/run/env.test.ts @@ -1,4 +1,3 @@ -// @known-failing-on-windows: 1 failing import { describe, expect, test, beforeAll, afterAll } from "bun:test"; import { bunRun, bunRunAsScript, bunTest, tempDirWithFiles, bunExe, bunEnv } from "harness"; import path from "path"; diff --git a/test/cli/run/run-extensionless.test.ts b/test/cli/run/run-extensionless.test.ts index 077b2b5f56..f2c168847c 100644 --- a/test/cli/run/run-extensionless.test.ts +++ b/test/cli/run/run-extensionless.test.ts @@ -1,4 +1,3 @@ -// @known-failing-on-windows: 1 failing import { expect, test } from "bun:test"; import { mkdirSync, realpathSync } from "fs"; import { bunEnv, bunExe } from "harness"; diff --git a/test/cli/test/bun-test.test.ts b/test/cli/test/bun-test.test.ts index 0c28800280..1ae8d3557c 100644 --- a/test/cli/test/bun-test.test.ts +++ b/test/cli/test/bun-test.test.ts @@ -1,4 +1,3 @@ -// @known-failing-on-windows: 1 failing import { join, resolve, dirname } from "node:path"; import { tmpdir } from "node:os"; import { mkdtempSync, writeFileSync, rmSync, mkdirSync } from "node:fs"; diff --git a/test/js/bun/console/console-iterator.test.ts b/test/js/bun/console/console-iterator.test.ts index ba20d74bda..6c10625440 100644 --- a/test/js/bun/console/console-iterator.test.ts +++ b/test/js/bun/console/console-iterator.test.ts @@ -1,4 +1,3 @@ -// @known-failing-on-windows: 1 failing import { spawnSync, spawn } from "bun"; import { describe, expect, it } from "bun:test"; import { bunExe } from "harness"; diff --git a/test/js/bun/util/bun-isMainThread.test.js b/test/js/bun/util/bun-isMainThread.test.js index 46fecbc743..87f74d136c 100644 --- a/test/js/bun/util/bun-isMainThread.test.js +++ b/test/js/bun/util/bun-isMainThread.test.js @@ -1,4 +1,3 @@ -// @known-failing-on-windows: 1 failing import { test, expect } from "bun:test"; import { bunEnv, bunExe } from "harness"; diff --git a/test/js/bun/util/inspect-error.test.js b/test/js/bun/util/inspect-error.test.js index 56f72e4aea..31f8d79f74 100644 --- a/test/js/bun/util/inspect-error.test.js +++ b/test/js/bun/util/inspect-error.test.js @@ -1,4 +1,3 @@ -// @known-failing-on-windows: 1 failing import { test, expect } from "bun:test"; test("error.cause", () => { diff --git a/test/js/bun/util/text-loader.test.ts b/test/js/bun/util/text-loader.test.ts index a081b6daa9..f7d3b2a284 100644 --- a/test/js/bun/util/text-loader.test.ts +++ b/test/js/bun/util/text-loader.test.ts @@ -1,4 +1,3 @@ -// @known-failing-on-windows: 1 failing import { spawnSync } from "bun"; import { describe, expect, it } from "bun:test"; import { bunEnv, bunExe } from "harness"; diff --git a/test/js/node/buffer.test.js b/test/js/node/buffer.test.js index a6d067acb7..ee32d73218 100644 --- a/test/js/node/buffer.test.js +++ b/test/js/node/buffer.test.js @@ -1,4 +1,3 @@ -// @known-failing-on-windows: 1 failing import { Buffer, SlowBuffer, isAscii, isUtf8 } from "buffer"; import { describe, it, expect, beforeEach, afterEach } from "bun:test"; import { gc } from "harness"; diff --git a/test/js/node/events/event-emitter.test.ts b/test/js/node/events/event-emitter.test.ts index c18fea561b..687e90910e 100644 --- a/test/js/node/events/event-emitter.test.ts +++ b/test/js/node/events/event-emitter.test.ts @@ -1,4 +1,3 @@ -// @known-failing-on-windows: 1 failing import { test, describe, expect } from "bun:test"; import { sleep } from "bun"; import { createRequire } from "module"; diff --git a/test/js/node/http/node-http.test.ts b/test/js/node/http/node-http.test.ts index 4e84b1f6de..a8e2372211 100644 --- a/test/js/node/http/node-http.test.ts +++ b/test/js/node/http/node-http.test.ts @@ -1,4 +1,3 @@ -// @known-failing-on-windows: 1 failing // @ts-nocheck import { createServer, diff --git a/test/js/node/worker_threads/worker_threads.test.ts b/test/js/node/worker_threads/worker_threads.test.ts index f00b401c5b..3fd164a889 100644 --- a/test/js/node/worker_threads/worker_threads.test.ts +++ b/test/js/node/worker_threads/worker_threads.test.ts @@ -1,4 +1,3 @@ -// @known-failing-on-windows: 1 failing import wt, { getEnvironmentData, isMainThread, diff --git a/test/js/third_party/comlink/comlink.test.ts b/test/js/third_party/comlink/comlink.test.ts index f3b87ea01f..77d8109504 100644 --- a/test/js/third_party/comlink/comlink.test.ts +++ b/test/js/third_party/comlink/comlink.test.ts @@ -1,4 +1,3 @@ -// @known-failing-on-windows: 1 failing import { test, expect, describe } from "bun:test"; import { join } from "path"; import * as Comlink from "comlink"; diff --git a/test/js/third_party/resvg/bbox.test.js b/test/js/third_party/resvg/bbox.test.js index 2932edcc7e..0c8a195781 100644 --- a/test/js/third_party/resvg/bbox.test.js +++ b/test/js/third_party/resvg/bbox.test.js @@ -1,4 +1,3 @@ -// @known-failing-on-windows: code 3765269347 import { test, expect } from "bun:test"; import { Resvg } from "@resvg/resvg-js"; diff --git a/test/js/third_party/rollup-v4/rollup-v4.test.ts b/test/js/third_party/rollup-v4/rollup-v4.test.ts index 3c8ff062f8..9aef2c0275 100644 --- a/test/js/third_party/rollup-v4/rollup-v4.test.ts +++ b/test/js/third_party/rollup-v4/rollup-v4.test.ts @@ -1,4 +1,3 @@ -// @known-failing-on-windows: 1 failing import { parseAst } from "rollup/parseAst"; test("it works", () => { diff --git a/test/js/third_party/socket.io/socket.io-close.test.ts b/test/js/third_party/socket.io/socket.io-close.test.ts index bef50f0243..feac0fbad1 100644 --- a/test/js/third_party/socket.io/socket.io-close.test.ts +++ b/test/js/third_party/socket.io/socket.io-close.test.ts @@ -1,4 +1,3 @@ -// @known-failing-on-windows: 1 failing import { describe, it, expect } from "bun:test"; import { io as ioc } from "socket.io-client"; import { join } from "path"; diff --git a/test/js/third_party/socket.io/socket.io-connection-state-recovery.test.ts b/test/js/third_party/socket.io/socket.io-connection-state-recovery.test.ts index 6677d29633..02a5271962 100644 --- a/test/js/third_party/socket.io/socket.io-connection-state-recovery.test.ts +++ b/test/js/third_party/socket.io/socket.io-connection-state-recovery.test.ts @@ -1,4 +1,3 @@ -// @known-failing-on-windows: 1 failing import { describe, it, expect } from "bun:test"; import { Server, Socket } from "socket.io"; diff --git a/test/js/web/workers/message-channel.test.ts b/test/js/web/workers/message-channel.test.ts index 092d8f31b0..033343b34f 100644 --- a/test/js/web/workers/message-channel.test.ts +++ b/test/js/web/workers/message-channel.test.ts @@ -1,4 +1,3 @@ -// @known-failing-on-windows: 1 failing test("simple usage", done => { const channel = new MessageChannel(); const port1 = channel.port1; From f73bde79c7f3ba7d1a5706214f770d1b3b08d025 Mon Sep 17 00:00:00 2001 From: dave caruso Date: Thu, 1 Feb 2024 14:26:20 -0800 Subject: [PATCH 40/45] fix(debug): get the result for fd debug message before we close it (#8626) --- src/fd.zig | 25 ++++++++++++++++++------- 1 file changed, 18 insertions(+), 7 deletions(-) diff --git a/src/fd.zig b/src/fd.zig index 162a08328b..7bfe837cf0 100644 --- a/src/fd.zig +++ b/src/fd.zig @@ -185,9 +185,9 @@ pub const FDImpl = packed struct { if (env.os != .windows or this.kind == .uv) { // This branch executes always on linux (uv() is no-op), // or on Windows when given a UV file descriptor. - const fd = bun.toFD(this.uv()); - if (fd == bun.STDOUT_FD or fd == bun.STDERR_FD) { - log("close({}) SKIPPED", .{this}); + const fd = this.uv(); + if (fd == 1 or fd == 2) { + log("close({}) SKIPPED", .{fd}); return null; } } @@ -211,6 +211,11 @@ pub const FDImpl = packed struct { std.debug.assert(this.value.as_system != invalid_value); // probably a UAF } + // Format the file descriptor for logging BEFORE closing it. + // Otherwise the file descriptor is always invalid after closing it. + var buf: [1050]u8 = undefined; + const this_fmt = if (env.isDebug) std.fmt.bufPrint(&buf, "{}", .{this}) catch unreachable; + const result: ?bun.sys.Error = switch (env.os) { .linux => result: { const fd = this.encode(); @@ -262,12 +267,12 @@ pub const FDImpl = packed struct { if (result) |err| { if (err.errno == @intFromEnum(os.E.BADF)) { // TODO(@paperdave): Zig Compiler Bug, if you remove `this` from the log. An error is correctly printed, but with the wrong reference trace - bun.Output.debugWarn("close({}) = EBADF. This is an indication of a file descriptor UAF", .{this}); + bun.Output.debugWarn("close({s}) = EBADF. This is an indication of a file descriptor UAF", .{this_fmt}); } else { - log("close({}) = {}", .{ this, err }); + log("close({s}) = {}", .{ this_fmt, err }); } } else { - log("close({})", .{this}); + log("close({s})", .{this_fmt}); } } @@ -304,7 +309,13 @@ pub const FDImpl = packed struct { } switch (env.os) { else => { - try writer.print("{d}", .{this.system()}); + const fd = this.system(); + try writer.print("{d}", .{fd}); + if (env.isDebug and fd >= 3) print_with_path: { + var path_buf: [1024]u8 = undefined; + const path = std.os.getFdPath(fd, &path_buf) catch break :print_with_path; + try writer.print("[{s}]", .{path}); + } }, .windows => { switch (this.kind) { From 63a9a8b01599b738978b04489cbd4fb7b64293aa Mon Sep 17 00:00:00 2001 From: Ashcon Partovi Date: Thu, 1 Feb 2024 14:55:35 -0800 Subject: [PATCH 41/45] Improve `.vscode` extensions, tasks, and configuration (#8511) --- .prettierignore | 14 - .prettierrc.cjs | 15 - .vscode/extensions.json | 27 +- .vscode/launch.json | 1129 ++++++++++++++++++++++++++++----------- .vscode/settings.json | 144 +++-- .vscode/tasks.json | 49 +- bun.lockb | Bin 75654 -> 75654 bytes package.json | 20 +- 8 files changed, 993 insertions(+), 405 deletions(-) delete mode 100644 .prettierignore delete mode 100644 .prettierrc.cjs diff --git a/.prettierignore b/.prettierignore deleted file mode 100644 index 03366aa692..0000000000 --- a/.prettierignore +++ /dev/null @@ -1,14 +0,0 @@ -src/fallback.html -src/bun.js/WebKit -src/js/out -src/*.out.js -src/*out.*.js -src/deps -src/test/fixtures -src/react-refresh.js -test/snapshots -test/snapshots-no-hmr -test/js/deno/*.test.ts -test/js/deno/**/*.test.ts -bench/react-hello-world/react-hello-world.node.js -test/cli/run/encoding-utf16-le-bom.ts diff --git a/.prettierrc.cjs b/.prettierrc.cjs deleted file mode 100644 index 44f2bd9331..0000000000 --- a/.prettierrc.cjs +++ /dev/null @@ -1,15 +0,0 @@ -module.exports = { - arrowParens: "avoid", - printWidth: 120, - trailingComma: "all", - useTabs: false, - quoteProps: "preserve", - overrides: [ - { - files: ["*.md"], - options: { - printWidth: 80, - }, - }, - ], -}; diff --git a/.vscode/extensions.json b/.vscode/extensions.json index be3d5ef659..4b5985593d 100644 --- a/.vscode/extensions.json +++ b/.vscode/extensions.json @@ -1,10 +1,33 @@ { "recommendations": [ + // Zig "ziglang.vscode-zig", - "biomejs.biome", + + // C/C++ + "clang.clangd", + "ms-vscode.cmake-tools", "xaver.clang-format", "vadimcn.vscode-lldb", + + // JavaScript + "oven.bun-vscode", + "biomejs.biome", + + // TypeScript + "better-ts-errors.better-ts-errors", + "MylesMurphy.prettify-ts", + + // Markdown + "bierner.markdown-preview-github-styles", + "bierner.markdown-emoji", + "bierner.emojisense", + "bierner.markdown-checkbox", + "bierner.jsdoc-markdown-highlighting", + + // TOML + "tamasfe.even-better-toml", + + // Other "bierner.comment-tagged-templates", - "clangd.clangd" ] } diff --git a/.vscode/launch.json b/.vscode/launch.json index 9100299202..24279f7d2d 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -1,36 +1,24 @@ { - // The usage of BUN_GARBAGE_COLLECTOR_LEVEL=2 is important for debugging - // It will force the garbage collector to run after every test and every call to expect() - // it makes our tests very slow - // But it helps catch memory bugs + // Notes: + // - BUN_GARBAGE_COLLECTOR_LEVEL=2 forces GC to run after every `expect()`, but is slower + // - BUN_DEBUG_QUIET_LOGS=1 disables the debug logs + // - FORCE_COLOR=1 forces colors in the terminal + // - "${workspaceFolder}/test" is the cwd for `bun test` so it matches CI, we should fix this later + // - "cppvsdbg" is used instead of "lldb" on Windows, because "lldb" is too slow "version": "0.2.0", "configurations": [ - { - "type": "lldb", - "request": "launch", - "name": "sharp", - "program": "bun-debug", - "args": ["install", "sharp"], - // The cwd here must be the same as in CI. Or you will cause test failures that only happen in CI. - "cwd": "/tmp/scratchpad_20230911T213851", - "env": { - "FORCE_COLOR": "1", - "BUN_GARBAGE_COLLECTOR_LEVEL": "2" - }, - "console": "internalConsole" - }, + // bun test [file] { "type": "lldb", "request": "launch", "name": "bun test [file]", - "program": "bun-debug", + "program": "${workspaceFolder}/build/bun-debug", "args": ["test", "${file}"], - // The cwd here must be the same as in CI. Or you will cause test failures that only happen in CI. "cwd": "${workspaceFolder}/test", "env": { - "BUN_GARBAGE_COLLECTOR_LEVEL": "2", "FORCE_COLOR": "1", - "BUN_DEBUG_QUIET_LOGS": "1" + "BUN_DEBUG_QUIET_LOGS": "1", + "BUN_GARBAGE_COLLECTOR_LEVEL": "2", }, "console": "internalConsole" }, @@ -38,13 +26,13 @@ "type": "lldb", "request": "launch", "name": "bun test [file] (fast)", - "program": "bun-debug", + "program": "${workspaceFolder}/build/bun-debug", "args": ["test", "${file}"], - // The cwd here must be the same as in CI. Or you will cause test failures that only happen in CI. "cwd": "${workspaceFolder}/test", "env": { "FORCE_COLOR": "1", - "BUN_DEBUG_QUIET_LOGS": "1" + "BUN_DEBUG_QUIET_LOGS": "1", + "BUN_GARBAGE_COLLECTOR_LEVEL": "0", }, "console": "internalConsole" }, @@ -52,12 +40,13 @@ "type": "lldb", "request": "launch", "name": "bun test [file] (verbose)", - "program": "bun-debug", + "program": "${workspaceFolder}/build/bun-debug", "args": ["test", "${file}"], - // The cwd here must be the same as in CI. Or you will cause test failures that only happen in CI. "cwd": "${workspaceFolder}/test", "env": { - "FORCE_COLOR": "1" + "FORCE_COLOR": "1", + "BUN_DEBUG_QUIET_LOGS": "0", + "BUN_GARBAGE_COLLECTOR_LEVEL": "2", }, "console": "internalConsole" }, @@ -65,36 +54,299 @@ "type": "lldb", "request": "launch", "name": "bun test [file] --watch", - "program": "bun-debug", + "program": "${workspaceFolder}/build/bun-debug", "args": ["test", "--watch", "${file}"], - // The cwd here must be the same as in CI. Or you will cause test failures that only happen in CI. "cwd": "${workspaceFolder}/test", "env": { "FORCE_COLOR": "1", - "BUN_DEBUG_QUIET_LOGS": "1" + "BUN_DEBUG_QUIET_LOGS": "1", + "BUN_GARBAGE_COLLECTOR_LEVEL": "2", }, "console": "internalConsole" }, { "type": "lldb", "request": "launch", - "name": "bun test [file] --only", - "program": "bun-debug", - "args": ["test", "--only", "${file}"], - // The cwd here must be the same as in CI. Or you will cause test failures that only happen in CI. + "name": "bun test [file] --hot", + "program": "${workspaceFolder}/build/bun-debug", + "args": ["test", "--hot", "${file}"], "cwd": "${workspaceFolder}/test", "env": { "FORCE_COLOR": "1", - "BUN_DEBUG_QUIET_LOGS": "1" + "BUN_DEBUG_QUIET_LOGS": "1", + "BUN_GARBAGE_COLLECTOR_LEVEL": "2", }, "console": "internalConsole" }, + { + "type": "lldb", + "request": "launch", + "name": "bun test [file] --inspect", + "program": "${workspaceFolder}/build/bun-debug", + "args": ["test", "${file}"], + "cwd": "${workspaceFolder}/test", + "env": { + "FORCE_COLOR": "1", + "BUN_DEBUG_QUIET_LOGS": "1", + "BUN_GARBAGE_COLLECTOR_LEVEL": "2", + "BUN_INSPECT": "ws://localhost:0/?wait=1" + }, + "console": "internalConsole", + "serverReadyAction": { + "pattern": "https:\/\/debug.bun.sh\/#localhost:([0-9]+)/", + "uriFormat": "https://debug.bun.sh/#ws://localhost:%s/", + "action": "openExternally" + } + }, + { + "type": "lldb", + "request": "launch", + "name": "bun test [file] --inspect-brk", + "program": "${workspaceFolder}/build/bun-debug", + "args": ["test", "${file}"], + "cwd": "${workspaceFolder}/test", + "env": { + "FORCE_COLOR": "1", + "BUN_DEBUG_QUIET_LOGS": "1", + "BUN_GARBAGE_COLLECTOR_LEVEL": "2", + "BUN_INSPECT": "ws://localhost:0/?break=1" + }, + "console": "internalConsole", + "serverReadyAction": { + "pattern": "https:\/\/debug.bun.sh\/#localhost:([0-9]+)/", + "uriFormat": "https://debug.bun.sh/#ws://localhost:%s/", + "action": "openExternally" + } + }, + // bun run [file] + { + "type": "lldb", + "request": "launch", + "name": "bun run [file]", + "program": "${workspaceFolder}/build/bun-debug", + "args": ["run", "${fileBasename}"], + "cwd": "${fileDirname}", + "env": { + "FORCE_COLOR": "0", + "BUN_DEBUG_QUIET_LOGS": "1", + "BUN_GARBAGE_COLLECTOR_LEVEL": "2", + }, + "console": "internalConsole", + }, + { + "type": "lldb", + "request": "launch", + "name": "bun run [file] (fast)", + "program": "${workspaceFolder}/build/bun-debug", + "args": ["run", "${fileBasename}"], + "cwd": "${fileDirname}", + "env": { + "FORCE_COLOR": "1", + "BUN_DEBUG_QUIET_LOGS": "1", + "BUN_GARBAGE_COLLECTOR_LEVEL": "0", + }, + "console": "internalConsole" + }, + { + "type": "lldb", + "request": "launch", + "name": "bun run [file] (verbose)", + "program": "${workspaceFolder}/build/bun-debug", + "args": ["run", "${fileBasename}"], + "cwd": "${fileDirname}", + "env": { + "FORCE_COLOR": "1", + "BUN_DEBUG_QUIET_LOGS": "0", + "BUN_GARBAGE_COLLECTOR_LEVEL": "2", + }, + "console": "internalConsole" + }, + { + "type": "lldb", + "request": "launch", + "name": "bun run [file] --watch", + "program": "${workspaceFolder}/build/bun-debug", + "args": ["run", "--watch", "${fileBasename}"], + "cwd": "${fileDirname}", + "env": { + "FORCE_COLOR": "1", + "BUN_DEBUG_QUIET_LOGS": "1", + "BUN_GARBAGE_COLLECTOR_LEVEL": "2", + }, + "console": "internalConsole" + }, + { + "type": "lldb", + "request": "launch", + "name": "bun run [file] --hot", + "program": "${workspaceFolder}/build/bun-debug", + "args": ["run", "--hot", "${fileBasename}"], + "cwd": "${fileDirname}", + "env": { + "FORCE_COLOR": "1", + "BUN_DEBUG_QUIET_LOGS": "1", + "BUN_GARBAGE_COLLECTOR_LEVEL": "2", + }, + "console": "internalConsole" + }, + { + "type": "lldb", + "request": "launch", + "name": "bun run [file] --inspect", + "program": "${workspaceFolder}/build/bun-debug", + "args": ["run", "${fileBasename}"], + "cwd": "${fileDirname}", + "env": { + "FORCE_COLOR": "0", + "BUN_DEBUG_QUIET_LOGS": "1", + "BUN_GARBAGE_COLLECTOR_LEVEL": "2", + "BUN_INSPECT": "ws://localhost:0/?wait=1" + }, + "console": "internalConsole", + "serverReadyAction": { + "pattern": "https:\/\/debug.bun.sh\/#localhost:([0-9]+)/", + "uriFormat": "https://debug.bun.sh/#ws://localhost:%s/", + "action": "openExternally" + }, + }, + { + "type": "lldb", + "request": "launch", + "name": "bun run [file] --inspect-brk", + "program": "${workspaceFolder}/build/bun-debug", + "args": ["run", "${fileBasename}"], + "cwd": "${fileDirname}", + "env": { + "FORCE_COLOR": "0", + "BUN_DEBUG_QUIET_LOGS": "1", + "BUN_GARBAGE_COLLECTOR_LEVEL": "2", + "BUN_INSPECT": "ws://localhost:0/?break=1" + }, + "console": "internalConsole", + "serverReadyAction": { + "pattern": "https:\/\/debug.bun.sh\/#localhost:([0-9]+)/", + "uriFormat": "https://debug.bun.sh/#ws://localhost:%s/", + "action": "openExternally" + }, + }, + // bun test [...] + { + "type": "lldb", + "request": "launch", + "name": "bun test [...]", + "program": "${workspaceFolder}/build/bun-debug", + "args": ["test", "${input:testName}"], + "cwd": "${workspaceFolder}/test", + "env": { + "FORCE_COLOR": "1", + "BUN_DEBUG_QUIET_LOGS": "1", + "BUN_GARBAGE_COLLECTOR_LEVEL": "2", + }, + "console": "internalConsole" + }, + { + "type": "lldb", + "request": "launch", + "name": "bun test [...] (fast)", + "program": "${workspaceFolder}/build/bun-debug", + "args": ["test", "${input:testName}"], + "cwd": "${workspaceFolder}/test", + "env": { + "FORCE_COLOR": "1", + "BUN_DEBUG_QUIET_LOGS": "1", + "BUN_GARBAGE_COLLECTOR_LEVEL": "0", + }, + "console": "internalConsole" + }, + { + "type": "lldb", + "request": "launch", + "name": "bun test [...] (verbose)", + "program": "${workspaceFolder}/build/bun-debug", + "args": ["test", "${input:testName}"], + "cwd": "${workspaceFolder}/test", + "env": { + "FORCE_COLOR": "1", + "BUN_DEBUG_QUIET_LOGS": "0", + "BUN_GARBAGE_COLLECTOR_LEVEL": "2", + }, + "console": "internalConsole" + }, + { + "type": "lldb", + "request": "launch", + "name": "bun test [...] --watch", + "program": "${workspaceFolder}/build/bun-debug", + "args": ["test", "--watch", "${input:testName}"], + "cwd": "${workspaceFolder}/test", + "env": { + "FORCE_COLOR": "1", + "BUN_DEBUG_QUIET_LOGS": "1", + "BUN_GARBAGE_COLLECTOR_LEVEL": "2", + }, + "console": "internalConsole" + }, + { + "type": "lldb", + "request": "launch", + "name": "bun test [...] --hot", + "program": "${workspaceFolder}/build/bun-debug", + "args": ["test", "--hot", "${input:testName}"], + "cwd": "${workspaceFolder}/test", + "env": { + "FORCE_COLOR": "1", + "BUN_DEBUG_QUIET_LOGS": "1", + "BUN_GARBAGE_COLLECTOR_LEVEL": "2", + }, + "console": "internalConsole" + }, + { + "type": "lldb", + "request": "launch", + "name": "bun test [...] --inspect", + "program": "${workspaceFolder}/build/bun-debug", + "args": ["test", "${input:testName}"], + "cwd": "${workspaceFolder}/test", + "env": { + "FORCE_COLOR": "1", + "BUN_DEBUG_QUIET_LOGS": "1", + "BUN_GARBAGE_COLLECTOR_LEVEL": "2", + "BUN_INSPECT": "ws://localhost:0/?wait=1" + }, + "console": "internalConsole", + "serverReadyAction": { + "pattern": "https:\/\/debug.bun.sh\/#localhost:([0-9]+)/", + "uriFormat": "https://debug.bun.sh/#ws://localhost:%s/", + "action": "openExternally" + } + }, + { + "type": "lldb", + "request": "launch", + "name": "bun test [...] --inspect-brk", + "program": "${workspaceFolder}/build/bun-debug", + "args": ["test", "${input:testName}"], + "cwd": "${workspaceFolder}/test", + "env": { + "FORCE_COLOR": "1", + "BUN_DEBUG_QUIET_LOGS": "1", + "BUN_GARBAGE_COLLECTOR_LEVEL": "2", + "BUN_INSPECT": "ws://localhost:0/?break=1" + }, + "console": "internalConsole", + "serverReadyAction": { + "pattern": "https:\/\/debug.bun.sh\/#localhost:([0-9]+)/", + "uriFormat": "https://debug.bun.sh/#ws://localhost:%s/", + "action": "openExternally" + } + }, + // bun test [*] { "type": "lldb", "request": "launch", "name": "bun test [*]", - "program": "bun-debug", - "args": ["test", "js/node"], + "program": "${workspaceFolder}/build/bun-debug", + "args": ["test"], "cwd": "${workspaceFolder}/test", "env": { "FORCE_COLOR": "1", @@ -107,297 +359,572 @@ "type": "lldb", "request": "launch", "name": "bun test [*] (fast)", - "program": "bun-debug", - "args": ["test", "js"], - // The cwd here must be the same as in CI. Or you will cause test failures that only happen in CI. + "program": "${workspaceFolder}/build/bun-debug", + "args": ["test"], "cwd": "${workspaceFolder}/test", "env": { "FORCE_COLOR": "1", - "BUN_DEBUG_QUIET_LOGS": "1" + "BUN_DEBUG_QUIET_LOGS": "1", + "BUN_GARBAGE_COLLECTOR_LEVEL": "0", }, "console": "internalConsole" }, { "type": "lldb", "request": "launch", - "name": "bun test [*] --only", - "program": "bun-debug", - "args": ["test", "--only"], - // The cwd here must be the same as in CI. Or you will cause test failures that only happen in CI. + "name": "bun test [*] --inspect", + "program": "${workspaceFolder}/build/bun-debug", + "args": ["test"], "cwd": "${workspaceFolder}/test", - "env": { - "FORCE_COLOR": "1", - "BUN_DEBUG_QUIET_LOGS": "1" - }, - "console": "internalConsole" - }, - { - "type": "lldb", - "request": "launch", - "name": "ZACK", - "program": "bun-debug", - "args": ["run", "./wtf"], - // "args": ["test", "bunshell.test.ts", "-t", "cd"], - // "args": ["test", "lex.test.ts"], - // "args": ["test", "bunshell.test.ts", "-t", "invalid surrogates"], - // "args": ["test", "bunshell.test.ts"], - // "args": ["test", "bunshell.test.ts", "-t", "recursive"], - // The cwd here must be the same as in CI. Or you will cause test failures that only happen in CI. - "cwd": "${workspaceFolder}", "env": { "FORCE_COLOR": "1", "BUN_DEBUG_QUIET_LOGS": "1", - "BUN_GARBAGE_COLLECTOR_LEVEL": "2" + "BUN_GARBAGE_COLLECTOR_LEVEL": "2", + "BUN_INSPECT": "ws://localhost:0/" }, - "console": "internalConsole" - }, - { - "type": "lldb", - "request": "launch", - "name": "bun run [file]", - "program": "${workspaceFolder}/build/bun-debug", - "args": ["run", "${file}"], - "cwd": "${fileDirname}", - "env": { - "FORCE_COLOR": "1", - "NODE_ENV": "development" - }, - "console": "internalConsole" - }, - { - "type": "lldb", - "request": "launch", - "name": "bun run [file] (gc)", - "program": "bun-debug", - "args": ["run", "${file}"], - "cwd": "${fileDirname}", - "env": { - "FORCE_COLOR": "1", - "BUN_DEBUG_QUIET_LOGS": "1", - "BUN_GARBAGE_COLLECTOR_LEVEL": "2" - }, - "console": "internalConsole" - }, - { - "type": "lldb", - "request": "launch", - "name": "bun run [file] (verbose)", - "program": "bun-debug", - "args": ["run", "${file}"], - "cwd": "${fileDirname}", - "env": { - "FORCE_COLOR": "1" - }, - "console": "internalConsole" - }, - { - "type": "lldb", - "request": "launch", - "name": "bun run [file] --watch", - "program": "bun-debug", - "args": ["run", "--watch", "${file}"], - "cwd": "${fileDirname}", - "env": { - "FORCE_COLOR": "1" - }, - "console": "internalConsole" - }, - { - "type": "lldb", - "request": "launch", - "name": "bun run [file] --hot", - "program": "bun-debug", - "args": ["run", "--hot", "${file}"], - "cwd": "${fileDirname}", - "env": { - "FORCE_COLOR": "1" - }, - "console": "internalConsole" - }, - { - "type": "lldb", - "request": "launch", - "name": "HTTP bench", - "program": "${workspaceFolder}/misctools/http_bench", - "args": ["https://twitter.com", "--count=100"], - "cwd": "${workspaceFolder}", - "console": "internalConsole" - }, - { - "type": "lldb", - "request": "launch", - "name": "bun build debug", - "program": "bun-debug", - "args": ["bun", "${file}"], - "cwd": "${workspaceFolder}", "console": "internalConsole", - "env": { - "BUN_CONFIG_MINIFY_WHITESPACE": "1" + "serverReadyAction": { + "pattern": "https:\/\/debug.bun.sh\/#localhost:([0-9]+)/", + "uriFormat": "https://debug.bun.sh/#ws://localhost:%s/", + "action": "openExternally" } }, { "type": "lldb", "request": "launch", - "name": "bun build debug out.js", - "program": "bun-debug", - "args": ["--outfile=out.js", "bun", "${file}"], - "cwd": "${file}/../", - "console": "internalConsole", - "env": { - "BUN_CONFIG_MINIFY_WHITESPACE": "1" - } - }, - { - "type": "lldb", - "request": "launch", - "name": "bun build debug STDOUT", - "program": "bun-debug", - "args": ["bun", "${file}"], - "cwd": "${file}/../", - "console": "internalConsole", - "env": {} - }, - { - "type": "lldb", - "request": "launch", - "name": "bun build debug (no splitting, browser entry)", - "program": "bun-debug", - "args": [ - "--entry-names=./[name].[ext]", - "--outdir=/Users/jarred/Code/bun-rsc/.rsc-no-split", - "--platform=browser", - "bun", - "./quick.tsx" - ], - "cwd": "/Users/jarred/Code/bun-rsc", - "console": "internalConsole", - "env": { - "NODE_ENV": "production" - // "BUN_DEBUG_QUIET_LOGS": "1" - // "BUN_DUMP_SYMBOLS": "1" - } - }, - { - "type": "lldb", - "request": "launch", - "name": "bun build debug (splitting, rsc)", - "program": "bun-debug", - "args": [ - "--entry-names=./[name].[ext]", - "--outdir=/Users/jarred/Code/bun-rsc/.rsc-split", - "--server-components", - "--platform=bun", - "--splitting", - "bun", - "/Users/jarred/Code/bun-rsc/components/Message.tsx", - "/Users/jarred/Code/bun-rsc/components/Button.tsx" - ], - "cwd": "/Users/jarred/Code/bun-rsc", - "console": "internalConsole", - "env": { - "NODE_ENV": "production" - // "BUN_DEBUG_QUIET_LOGS": "1" - // "BUN_DUMP_SYMBOLS": "1" - } - }, - { - "type": "lldb", - "request": "launch", - "name": "bun build debug (NO splitting, rsc)", - "program": "bun-debug", - "args": [ - "--entry-names=./[name].[ext]", - "--outdir=/Users/jarred/Code/bun-rsc/.rsccheck", - "--server-components", - "--platform=bun", - "bun", - "/Users/jarred/Code/bun-rsc/pages/index.js" - ], - "cwd": "/Users/jarred/Code/bun-rsc", - "console": "internalConsole", - "env": { - "NODE_ENV": "production" - // "BUN_DEBUG_QUIET_LOGS": "1" - // "BUN_DUMP_SYMBOLS": "1" - } - }, - { - "type": "lldb", - "request": "launch", - "name": "bunx debug", - "program": "bun-debug", - "args": ["--bun", "x", "tsc", "--help"], - "cwd": "${workspaceFolder}", - "console": "internalConsole", - "env": { - "BUN_DEBUG_QUIET_LOGS": "1" - } - }, - { - "type": "lldb", - "request": "launch", - "name": "bun install", - "program": "bun-debug", - "args": ["install"], - "cwd": "${fileDirname}", - "console": "internalConsole", - "env": {} - }, - { - "type": "lldb", - "request": "launch", - "name": "fetch debug", - "program": "${workspaceFolder}/misctools/fetch", - "args": ["https://example.com", "--verbose"], - "cwd": "${workspaceFolder}", + "name": "bun test [*] (ci)", + "program": "node", + "args": ["src/runner.node.mjs"], + "cwd": "${workspaceFolder}/packages/bun-internal-test", "console": "internalConsole" }, - { - "type": "lldb", - "request": "launch", - "name": "Build zig unit test", - "program": "make", - "args": ["build-unit", "${file}"], - "cwd": "${workspaceFolder}", - "console": "internalConsole" - }, - { - "type": "lldb", - "request": "launch", - "name": "Run zig unit test", - "program": "${workspaceFolder}/zig-out/bin/test", - "args": ["abc"], - "cwd": "${workspaceFolder}", - "console": "internalConsole" - }, - { - "type": "lldb", - "request": "launch", - "name": "Debug REPL", - "program": "${workspaceFolder}/build/bun-debug", - "args": ["/Users/dave/.bun/bin/bun-repl"], - "cwd": "${workspaceFolder}", - "console": "internalConsole", - "env": { - "BUN_DEBUG_QUIET_LOGS": "1" - }, - "terminal": "integrated" - }, - { - "type": "cppvsdbg", - "request": "launch", - "name": "Windows: bun run [file]", - "program": "${workspaceFolder}/build/bun-debug.exe", - "args": ["run", "${file}"], - "cwd": "${fileDirname}" - }, + // Windows: bun test [file] { "type": "cppvsdbg", "request": "launch", "name": "Windows: bun test [file]", "program": "${workspaceFolder}/build/bun-debug.exe", "args": ["test", "${file}"], - "cwd": "${fileDirname}" + "cwd": "${workspaceFolder}/test", + "environment": [ + { + "name": "FORCE_COLOR", + "value": "1" + }, + { + "name": "BUN_DEBUG_QUIET_LOGS", + "value": "1" + }, + { + "name": "BUN_GARBAGE_COLLECTOR_LEVEL", + "value": "2" + } + ], + }, + { + "type": "cppvsdbg", + "request": "launch", + "name": "Windows: bun test [file] (fast)", + "program": "${workspaceFolder}/build/bun-debug.exe", + "args": ["test", "${file}"], + "cwd": "${workspaceFolder}/test", + "environment": [ + { + "name": "FORCE_COLOR", + "value": "1" + }, + { + "name": "BUN_DEBUG_QUIET_LOGS", + "value": "1" + }, + { + "name": "BUN_GARBAGE_COLLECTOR_LEVEL", + "value": "0" + } + ], + }, + { + "type": "cppvsdbg", + "request": "launch", + "name": "Windows: bun test [file] (verbose)", + "program": "${workspaceFolder}/build/bun-debug.exe", + "args": ["test", "${file}"], + "cwd": "${workspaceFolder}/test", + "environment": [ + { + "name": "FORCE_COLOR", + "value": "1" + }, + { + "name": "BUN_DEBUG_QUIET_LOGS", + "value": "0" + }, + { + "name": "BUN_GARBAGE_COLLECTOR_LEVEL", + "value": "2" + } + ], + }, + { + "type": "cppvsdbg", + "request": "launch", + "name": "Windows: bun test [file] --inspect", + "program": "${workspaceFolder}/build/bun-debug.exe", + "args": ["test", "${file}"], + "cwd": "${workspaceFolder}/test", + "environment": [ + { + "name": "FORCE_COLOR", + "value": "1" + }, + { + "name": "BUN_DEBUG_QUIET_LOGS", + "value": "1" + }, + { + "name": "BUN_GARBAGE_COLLECTOR_LEVEL", + "value": "2" + }, + { + "name": "BUN_INSPECT", + "value": "ws://localhost:0/?wait=1" + } + ], + "serverReadyAction": { + "pattern": "https:\/\/debug.bun.sh\/#localhost:([0-9]+)/", + "uriFormat": "https://debug.bun.sh/#ws://localhost:%s/", + "action": "openExternally" + }, + }, + { + "type": "cppvsdbg", + "request": "launch", + "name": "Windows: bun test [file] --inspect-brk", + "program": "${workspaceFolder}/build/bun-debug.exe", + "args": ["test", "${file}"], + "cwd": "${workspaceFolder}/test", + "environment": [ + { + "name": "FORCE_COLOR", + "value": "1" + }, + { + "name": "BUN_DEBUG_QUIET_LOGS", + "value": "1" + }, + { + "name": "BUN_GARBAGE_COLLECTOR_LEVEL", + "value": "2" + }, + { + "name": "BUN_INSPECT", + "value": "ws://localhost:0/?break=1" + } + ], + "serverReadyAction": { + "pattern": "https:\/\/debug.bun.sh\/#localhost:([0-9]+)/", + "uriFormat": "https://debug.bun.sh/#ws://localhost:%s/", + "action": "openExternally" + }, + }, + // Windows: bun run [file] + { + "type": "cppvsdbg", + "request": "launch", + "name": "Windows: bun run [file]", + "program": "${workspaceFolder}/build/bun-debug.exe", + "args": ["run", "${fileBasename}"], + "cwd": "${fileDirname}", + "environment": [ + { + "name": "FORCE_COLOR", + "value": "1" + }, + { + "name": "BUN_DEBUG_QUIET_LOGS", + "value": "1" + }, + { + "name": "BUN_GARBAGE_COLLECTOR_LEVEL", + "value": "2" + } + ], + }, + { + "type": "cppvsdbg", + "request": "launch", + "name": "Windows: bun run [file] (fast)", + "program": "${workspaceFolder}/build/bun-debug.exe", + "args": ["run", "${fileBasename}"], + "cwd": "${fileDirname}", + "environment": [ + { + "name": "FORCE_COLOR", + "value": "1" + }, + { + "name": "BUN_DEBUG_QUIET_LOGS", + "value": "1" + }, + { + "name": "BUN_GARBAGE_COLLECTOR_LEVEL", + "value": "0" + } + ], + }, + { + "type": "cppvsdbg", + "request": "launch", + "name": "Windows: bun run [file] (verbose)", + "program": "${workspaceFolder}/build/bun-debug.exe", + "args": ["run", "${fileBasename}"], + "cwd": "${fileDirname}", + "environment": [ + { + "name": "FORCE_COLOR", + "value": "1" + }, + { + "name": "BUN_DEBUG_QUIET_LOGS", + "value": "0" + }, + { + "name": "BUN_GARBAGE_COLLECTOR_LEVEL", + "value": "2" + } + ], + }, + { + "type": "cppvsdbg", + "request": "launch", + "name": "Windows: bun run [file] --inspect", + "program": "${workspaceFolder}/build/bun-debug.exe", + "args": ["run", "${fileBasename}"], + "cwd": "${fileDirname}", + "environment": [ + { + "name": "FORCE_COLOR", + "value": "1" + }, + { + "name": "BUN_DEBUG_QUIET_LOGS", + "value": "1" + }, + { + "name": "BUN_GARBAGE_COLLECTOR_LEVEL", + "value": "2" + }, + { + "name": "BUN_INSPECT", + "value": "ws://localhost:0/?wait=1" + } + ], + "serverReadyAction": { + "pattern": "https:\/\/debug.bun.sh\/#localhost:([0-9]+)/", + "uriFormat": "https://debug.bun.sh/#ws://localhost:%s/", + "action": "openExternally" + }, + }, + { + "type": "cppvsdbg", + "request": "launch", + "name": "Windows: bun run [file] --inspect-brk", + "program": "${workspaceFolder}/build/bun-debug.exe", + "args": ["run", "${fileBasename}"], + "cwd": "${fileDirname}", + "environment": [ + { + "name": "FORCE_COLOR", + "value": "1" + }, + { + "name": "BUN_DEBUG_QUIET_LOGS", + "value": "1" + }, + { + "name": "BUN_GARBAGE_COLLECTOR_LEVEL", + "value": "2" + }, + { + "name": "BUN_INSPECT", + "value": "ws://localhost:0/?break=1" + } + ], + "serverReadyAction": { + "pattern": "https:\/\/debug.bun.sh\/#localhost:([0-9]+)/", + "uriFormat": "https://debug.bun.sh/#ws://localhost:%s/", + "action": "openExternally" + }, + }, + // Windows: bun test [...] + { + "type": "cppvsdbg", + "request": "launch", + "name": "Windows: bun test [...]", + "program": "${workspaceFolder}/build/bun-debug.exe", + "args": ["test", "${input:testName}"], + "cwd": "${workspaceFolder}/test", + "environment": [ + { + "name": "FORCE_COLOR", + "value": "1" + }, + { + "name": "BUN_DEBUG_QUIET_LOGS", + "value": "1" + }, + { + "name": "BUN_GARBAGE_COLLECTOR_LEVEL", + "value": "2" + } + ], + }, + { + "type": "cppvsdbg", + "request": "launch", + "name": "Windows: bun test [...] (fast)", + "program": "${workspaceFolder}/build/bun-debug.exe", + "args": ["test", "${input:testName}"], + "cwd": "${workspaceFolder}/test", + "environment": [ + { + "name": "FORCE_COLOR", + "value": "1" + }, + { + "name": "BUN_DEBUG_QUIET_LOGS", + "value": "1" + }, + { + "name": "BUN_GARBAGE_COLLECTOR_LEVEL", + "value": "0" + } + ], + }, + { + "type": "cppvsdbg", + "request": "launch", + "name": "Windows: bun test [...] (verbose)", + "program": "${workspaceFolder}/build/bun-debug.exe", + "args": ["test", "${input:testName}"], + "cwd": "${workspaceFolder}/test", + "environment": [ + { + "name": "FORCE_COLOR", + "value": "1" + }, + { + "name": "BUN_DEBUG_QUIET_LOGS", + "value": "0" + }, + { + "name": "BUN_GARBAGE_COLLECTOR_LEVEL", + "value": "2" + } + ], + }, + { + "type": "cppvsdbg", + "request": "launch", + "name": "Windows: bun test [...] --watch", + "program": "${workspaceFolder}/build/bun-debug.exe", + "args": ["test", "--watch", "${input:testName}"], + "cwd": "${workspaceFolder}/test", + "environment": [ + { + "name": "FORCE_COLOR", + "value": "1" + }, + { + "name": "BUN_DEBUG_QUIET_LOGS", + "value": "1" + }, + { + "name": "BUN_GARBAGE_COLLECTOR_LEVEL", + "value": "2" + } + ], + }, + { + "type": "cppvsdbg", + "request": "launch", + "name": "Windows: bun test [...] --hot", + "program": "${workspaceFolder}/build/bun-debug.exe", + "args": ["test", "--hot", "${input:testName}"], + "cwd": "${workspaceFolder}/test", + "environment": [ + { + "name": "FORCE_COLOR", + "value": "1" + }, + { + "name": "BUN_DEBUG_QUIET_LOGS", + "value": "1" + }, + { + "name": "BUN_GARBAGE_COLLECTOR_LEVEL", + "value": "2" + } + ], + }, + { + "type": "cppvsdbg", + "request": "launch", + "name": "Windows: bun test [...] --inspect", + "program": "${workspaceFolder}/build/bun-debug.exe", + "args": ["test", "${input:testName}"], + "cwd": "${workspaceFolder}/test", + "environment": [ + { + "name": "FORCE_COLOR", + "value": "1" + }, + { + "name": "BUN_DEBUG_QUIET_LOGS", + "value": "1" + }, + { + "name": "BUN_GARBAGE_COLLECTOR_LEVEL", + "value": "2" + }, + { + "name": "BUN_INSPECT", + "value": "ws://localhost:0/?wait=1" + } + ], + "serverReadyAction": { + "pattern": "https:\/\/debug.bun.sh\/#localhost:([0-9]+)/", + "uriFormat": "https://debug.bun.sh/#ws://localhost:%s/", + "action": "openExternally" + }, + }, + { + "type": "cppvsdbg", + "request": "launch", + "name": "Windows: bun test [...] --inspect-brk", + "program": "${workspaceFolder}/build/bun-debug.exe", + "args": ["test", "${input:testName}"], + "cwd": "${workspaceFolder}/test", + "environment": [ + { + "name": "FORCE_COLOR", + "value": "1" + }, + { + "name": "BUN_DEBUG_QUIET_LOGS", + "value": "1" + }, + { + "name": "BUN_GARBAGE_COLLECTOR_LEVEL", + "value": "2" + }, + { + "name": "BUN_INSPECT", + "value": "ws://localhost:0/?break=1" + } + ], + "serverReadyAction": { + "pattern": "https:\/\/debug.bun.sh\/#localhost:([0-9]+)/", + "uriFormat": "https://debug.bun.sh/#ws://localhost:%s/", + "action": "openExternally" + }, + }, + // Windows: bun test [*] + { + "type": "cppvsdbg", + "request": "launch", + "name": "Windows: bun test [*]", + "program": "${workspaceFolder}/build/bun-debug.exe", + "args": ["test"], + "cwd": "${workspaceFolder}/test", + "environment": [ + { + "name": "FORCE_COLOR", + "value": "1" + }, + { + "name": "BUN_DEBUG_QUIET_LOGS", + "value": "1" + }, + { + "name": "BUN_GARBAGE_COLLECTOR_LEVEL", + "value": "2" + } + ], + }, + { + "type": "cppvsdbg", + "request": "launch", + "name": "Windows: bun test [*] (fast)", + "program": "${workspaceFolder}/build/bun-debug.exe", + "args": ["test"], + "cwd": "${workspaceFolder}/test", + "environment": [ + { + "name": "FORCE_COLOR", + "value": "1" + }, + { + "name": "BUN_DEBUG_QUIET_LOGS", + "value": "1" + }, + { + "name": "BUN_GARBAGE_COLLECTOR_LEVEL", + "value": "0" + } + ], + }, + { + "type": "cppvsdbg", + "request": "launch", + "name": "Windows: bun test [*] --inspect", + "program": "${workspaceFolder}/build/bun-debug.exe", + "args": ["test"], + "cwd": "${workspaceFolder}/test", + "environment": [ + { + "name": "FORCE_COLOR", + "value": "1" + }, + { + "name": "BUN_DEBUG_QUIET_LOGS", + "value": "0" + }, + { + "name": "BUN_GARBAGE_COLLECTOR_LEVEL", + "value": "2" + }, + { + "name": "BUN_INSPECT", + "value": "ws://localhost:0/" + } + ], + "serverReadyAction": { + "pattern": "https:\/\/debug.bun.sh\/#localhost:([0-9]+)/", + "uriFormat": "https://debug.bun.sh/#ws://localhost:%s/", + "action": "openExternally" + }, + }, + { + "type": "cppvsdbg", + "request": "launch", + "name": "Windows: bun test [*] (ci)", + "program": "node", + "args": ["src/runner.node.mjs"], + "cwd": "${workspaceFolder}/packages/bun-internal-test", + "console": "internalConsole" + }, + ], + "inputs": [ + { + "id": "commandLine", + "type": "promptString", + "description": "Usage: bun [...]" + }, + { + "id": "testName", + "type": "promptString", + "description": "Usage: bun test [...]" }, ] } diff --git a/.vscode/settings.json b/.vscode/settings.json index ff8461dcf0..c0001c02d6 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,58 +1,89 @@ { - "git.autoRepositoryDetection": "openEditors", + // Editor + "editor.tabSize": 2, + "editor.insertSpaces": true, + "editor.formatOnSave": true, + "editor.formatOnSaveMode": "modificationsIfAvailable", + + // Search "search.quickOpen.includeSymbols": false, "search.seedWithNearestWord": true, "search.smartCase": true, "search.exclude": { "node_modules": true, - "src/bun.js/WebKit": true, ".git": true, + "src/bun.js/WebKit": true, "src/deps/*/**": true }, "search.followSymlinks": false, "search.useIgnoreFiles": true, + + // Git + "git.autoRepositoryDetection": "openEditors", + "git.ignoreSubmodules": true, + "git.ignoreLimitWarning": true, + + // Zig + "zig.initialSetupDone": true, "zig.buildOnSave": false, - "zig.formattingProvider": "zls", "zig.buildOption": "build", "zig.buildFilePath": "${workspaceFolder}/build.zig", - "zig.initialSetupDone": true, - "editor.formatOnSave": true, - - // We are using biome instead of prettier. - "prettier.enable": false, - - "[json]": { - "editor.defaultFormatter": "biomejs.biome" - }, - "[jsonc]": { - "editor.defaultFormatter": "biomejs.biome" - }, + "zig.path": "${workspaceFolder}/.cache/zig/zig.exe", + "zig.formattingProvider": "zls", + "zig.zls.enableInlayHints": false, "[zig]": { "editor.tabSize": 4, "editor.useTabStops": false, "editor.defaultFormatter": "ziglang.vscode-zig" }, - "[ts]": { + + // C++ + "lldb.verboseLogging": false, + "cmake.configureOnOpen": false, + "C_Cpp.errorSquiggles": "enabled", + "[cpp]": { + "editor.defaultFormatter": "xaver.clang-format" + }, + "[c]": { + "editor.defaultFormatter": "xaver.clang-format" + }, + "[h]": { + "editor.defaultFormatter": "xaver.clang-format" + }, + + // JavaScript + "prettier.enable": false, + "eslint.workingDirectories": ["${workspaceFolder}/packages/bun-types"], + "[javascript]": { + "editor.defaultFormatter": "biomejs.biome", + }, + "[javascriptreact]": { "editor.defaultFormatter": "biomejs.biome" }, - "[js]": { + + // TypeScript + "typescript.tsdk": "${workspaceFolder}/node_modules/typescript/lib", + "[typescript]": { "editor.defaultFormatter": "biomejs.biome" }, - "zig.zls.enableInlayHints": false, - "zig.path": "${workspaceFolder}/.cache/zig/zig.exe", - "git.ignoreSubmodules": true, - "[jsx]": { + "[typescriptreact]": { "editor.defaultFormatter": "biomejs.biome" }, - "[tsx]": { - "editor.defaultFormatter": "biomejs.biome" + + // JSON + "[json]": { + "editor.defaultFormatter": "biomejs.biome", }, - "[yaml]": {}, + "[jsonc]": { + "editor.defaultFormatter": "biomejs.biome", + }, + + // Markdown "[markdown]": { + "editor.defaultFormatter": "biomejs.biome", "editor.unicodeHighlight.ambiguousCharacters": false, "editor.unicodeHighlight.invisibleCharacters": false, "diffEditor.ignoreTrimWhitespace": false, - "editor.defaultFormatter": "biomejs.biome", "editor.wordWrap": "on", "editor.quickSuggestions": { "comments": "off", @@ -60,7 +91,18 @@ "other": "off" } }, - "lldb.verboseLogging": false, + + // TOML + "[toml]": { + "editor.defaultFormatter": "biomejs.biome", + }, + + // YAML + "[yaml]": { + "editor.defaultFormatter": "biomejs.biome", + }, + + // Files "files.exclude": { "**/.git": true, "**/.svn": true, @@ -87,33 +129,6 @@ "**/*.i": true, "packages/bun-uws/fuzzing/seed-corpus/**/*": true }, - "C_Cpp.files.exclude": { - "**/.vscode": true, - "WebKit/JSTests": true, - "WebKit/Tools": true, - "WebKit/WebDriverTests": true, - "WebKit/WebKit.xcworkspace": true, - "WebKit/WebKitLibraries": true, - "WebKit/Websites": true, - "WebKit/resources": true, - "WebKit/LayoutTests": true, - "WebKit/ManualTests": true, - "WebKit/PerformanceTests": true, - "WebKit/WebKitLegacy": true, - "WebKit/WebCore": true, - "WebKit/WebDriver": true, - "WebKit/WebKitBuild": true, - "WebKit/WebInspectorUI": true - }, - "[cpp]": { - "editor.defaultFormatter": "xaver.clang-format" - }, - "[h]": { - "editor.defaultFormatter": "xaver.clang-format" - }, - "[c]": { - "editor.defaultFormatter": "xaver.clang-format" - }, "files.associations": { "*.lock": "yarnlock", "*.idl": "cpp", @@ -233,9 +248,22 @@ "xtree": "cpp", "xutility": "cpp" }, - "C_Cpp.errorSquiggles": "enabled", - "eslint.workingDirectories": ["packages/bun-types"], - "typescript.tsdk": "node_modules/typescript/lib", - "cmake.configureOnOpen": false, - "git.ignoreLimitWarning": true + "C_Cpp.files.exclude": { + "**/.vscode": true, + "WebKit/JSTests": true, + "WebKit/Tools": true, + "WebKit/WebDriverTests": true, + "WebKit/WebKit.xcworkspace": true, + "WebKit/WebKitLibraries": true, + "WebKit/Websites": true, + "WebKit/resources": true, + "WebKit/LayoutTests": true, + "WebKit/ManualTests": true, + "WebKit/PerformanceTests": true, + "WebKit/WebKitLegacy": true, + "WebKit/WebCore": true, + "WebKit/WebDriver": true, + "WebKit/WebKitBuild": true, + "WebKit/WebInspectorUI": true + }, } diff --git a/.vscode/tasks.json b/.vscode/tasks.json index ae67585340..cec77199e5 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -2,10 +2,51 @@ "version": "2.0.0", "tasks": [ { - "label": "Rebuild Debug", - "command": "ninja", - "args": ["-Cbuild"], "type": "process", - } + "label": "Install Dependencies", + "command": "scripts/all-dependencies.sh", + "windows": { + "command": "scripts/all-dependencies.ps1" + }, + "icon": { + "id": "arrow-down" + }, + "options": { + "cwd": "${workspaceFolder}" + }, + }, + { + "type": "process", + "label": "Setup Environment", + "dependsOn": ["Install Dependencies"], + "command": "scripts/setup.sh", + "windows": { + "command": "scripts/setup.ps1" + }, + "icon": { + "id": "check" + }, + "options": { + "cwd": "${workspaceFolder}" + }, + }, + { + "type": "process", + "label": "Build Bun", + "dependsOn": ["Setup Environment"], + "command": "bun", + "args": ["run", "build"], + "icon": { + "id": "gear" + }, + "options": { + "cwd": "${workspaceFolder}" + }, + "isBuildCommand": true, + "runOptions": { + "instanceLimit": 1, + "reevaluateOnRerun": true, + }, + }, ] } diff --git a/bun.lockb b/bun.lockb index e7d7e711d7a5f30d2e2c03b19b14a23332508b68..2811f62f925ca723d56355b7767389bc2155b4ab 100755 GIT binary patch delta 21 dcmZoW&(e0DWy6lMY=(NKdd3W!_nkcv2>@kh2|@q> delta 21 dcmZoW&(e0DWy6lMY;lHqrh3Mk_nkcv2>@t(37`M~ diff --git a/package.json b/package.json index defee9719e..4dc8e1453e 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,8 @@ { + "private": true, "name": "bun", "dependencies": { - "@biomejs/biome": "^1.5.3", + "@biomejs/biome": "1.5.3", "@vscode/debugadapter": "^1.61.0", "esbuild": "^0.17.15", "eslint": "^8.20.0", @@ -14,7 +15,11 @@ "source-map-js": "^1.0.2", "typescript": "^5.0.2" }, - "private": true, + "devDependencies": { + "@types/react": "^18.0.25", + "@typescript-eslint/eslint-plugin": "^5.31.0", + "@typescript-eslint/parser": "^5.31.0" + }, "scripts": { "setup": "./scripts/setup.sh", "build": "if [ ! -e build ]; then bun setup; fi && ninja -C build", @@ -22,19 +27,12 @@ "build:release": "cmake . -DCMAKE_BUILD_TYPE=Release -GNinja -Bbuild-release && ninja -Cbuild-release", "build:safe": "cmake . -DZIG_OPTIMIZE=ReleaseSafe -DUSE_DEBUG_JSC=ON -DCMAKE_BUILD_TYPE=Release -GNinja -Bbuild-safe && ninja -Cbuild-safe", "typecheck": "tsc --noEmit && cd test && bun run typecheck", - "fmt": "biome format --write {src,test,bench,packages/{bun-types,bun-inspector-*,bun-vscode,bun-debug-adapter-protocol}}", + "fmt": "biome format --write {.vscode,src,test,bench,packages/{bun-types,bun-inspector-*,bun-vscode,bun-debug-adapter-protocol}}", "fmt:zig": "zig fmt src/*.zig src/**/*.zig", "lint": "eslint './**/*.d.ts' --cache", "lint:fix": "eslint './**/*.d.ts' --cache --fix", "test": "node packages/bun-internal-test/src/runner.node.mjs ./build/bun-debug", "test:release": "node packages/bun-internal-test/src/runner.node.mjs ./build-release/bun", "update-known-failures": "node packages/bun-internal-test/src/update-known-windows-failures.mjs" - }, - "devDependencies": { - "@types/react": "^18.0.25", - "@typescript-eslint/eslint-plugin": "^5.31.0", - "@typescript-eslint/parser": "^5.31.0" - }, - "version": "0.0.0", - "prettier": "./.prettierrc.cjs" + } } From 3a1229d03dcee9516e6c55638897c7629ed07d5d Mon Sep 17 00:00:00 2001 From: Ashcon Partovi Date: Thu, 1 Feb 2024 15:04:49 -0800 Subject: [PATCH 42/45] Add mysql2 integration test (#8075) --- test/bun.lockb | Bin 292445 -> 295407 bytes test/harness.ts | 108 ++++++++++++++++++++++++- test/integration/mysql2/mysql2.test.ts | 63 +++++++++++++++ test/package.json | 1 + 4 files changed, 171 insertions(+), 1 deletion(-) create mode 100644 test/integration/mysql2/mysql2.test.ts diff --git a/test/bun.lockb b/test/bun.lockb index 9484b06ecaf26f52cf5870f1ae79dac5469c5c98..858b706a69b9df21b9f9af3ebf86de02d2ca507d 100755 GIT binary patch delta 22908 zcmeHvcU)9Q|MlIwpsW!aAVmwGwSzTY$7nLBsx+>MD?Fj@KK~ogLd=RxL@K6ea!fMl)Xlmx8}G%Qj%)bVFQd?%?)U@O;KE-$0WxOq#lWKj_@mNSCr!5)a|A`$Fga0I~2tp zZci}xZxb3rKPfi4XNph1W!mcWD6QNHMR9{`C;9$yMJWyc1eo2H1!lJ_ zk~|6Q0zU=p4DJDT1V>8!b8MQ`yX2V3N3+T756VCn;~A$Cv~Fpp>d3+8_4aM9ckO(Yv7R{*mg9KbvUsp^i?S6 z1mOmldv*%Uz#U*#+!@S{ZKarxY%rMdRl(HlE}JWix@;=qQnpCw6|-F9%Gv;z^A3@< zx=-ElNE+d)vF)hbPQEwg1?WQ~ic4ik`D{ zhL{*!_0Y~1HM%Twcv+y@CBxrfj z2ZHS2q`VGXwnb6;!m~AHHZvMZ;nc&qnO?{g9cVP1fENQ#HAN>G4UsrqL*aeEJliqw zT9~2&Lkve?ad-4u868gMhVZbf4UL8M_b8dkw$Bx(;f+Fpd$$P{(Go*mBf z###mW;YPz4c;WDLrsx$x%<_d}qt*RDdT0+?=Z)(?152K5D!hiKdF?|CS+GK&duRvg zG4fGiqcD&qMHZVzzJm$_25pE+leqkah%8jT8;U2jD`q!jo=kFc{f6{;PGJV zP5EI)!$WxWv`8Fm+hANV>hcg8l3|$#2oKk4c#JA+%HJAf2Zz=bxiJH|3mN|FcYwZdwb6oX7GxcrYnuD7ku>?+I*H+_Giqg)U3#+Y(MPaniq##^lwM@fg zSn{~kHX4HR6eZexl;F+!llSaTUY9et7i*CmQgAxGE>`@kqV##~4TjhKwRhrAUafPA z67@Q7Iy`fg=pDl?c3MVx;fxjx7a4o4|2CNqqx!v#~`wJ)t#42c{jsU*UKiiTVaL)mlY*K z+bf=YE8y{}f$PLHqk8YMDGwnnub2l3uKuPV-4#>k+hK+s&>A7IxG5ijhO4H$+hJ;> zt0uQQb<_M6MIj0U7)M&NW<8V;KnI}LwDdn=md6ND$PED2gGOtNHl@MT8poTp(iU(5 z`U6a46^NsQWX*-(CjmCVV1RK$0H${_2wm2Oij{um+j2jJ5 z9|N!<6Qn;8%=CX{7Sfw;|L!Ty7S?&Cv94(>i1aRI<7a45W@b5)^sSk38B!;6bF%=p zeJ;Rc#cuyDXgfC#Y9UkhT~Dof-)S+5@;2t?z6V(SG8sp<1J(e{{}Es_HUPFjHo!!t zo}tuHSsyN- zTJ&*qwnAn+u9VvLaM@E7k!{d9dsD`BOiR{mfO3hdC@|$3*U_);MOl11M(?KG810@j5o_8RN>>|3#+Awy8i$BGMACehXM*3u$ z?)czIRSx_C*oUHIn!fm8Ie)2Jb7|;Jq;AFL)5uCd*joetmvng=zpDWpa?NGS$=t(M z(kIjWNcz^yav#h5b~2yLq25*cU2Pa3qKfLAP5U2~=!Yz&yUcqVGpmQpC$lX*!G*#7 zrEblXKE($)PUe$UQ|KR_!rflyqIE#L%p%iFkUqH*{Fz`jd%o1kJTn(b|G(fgZG+#U zBG&aCJ~+aDkQI~Jk1M7BHfBS9L_P=nI>{SkyfribXXwn@gbz<}o~}ARHRBC4@T{zW z%m!Qp+ncfz=^B3Ebf zc;c+xC#uL;G8<7t`eaA=;nF9waS>oj4W<5Ha5?R;G)6^i*GFJ(ptY=+tcrOhbk5pQ z{a9wVliVK6_I3jE=ynA&y^9&rO~#Q~E*i|u#z_4wTo>8z40xT-3S(u3y}`C(KuOvD zI4KVR<4=j_hh&%VseXbt)T6|nsyWCKRZ3duaX$4UQP z%#aB(j!bhBMcWb7;O9u+n%T~UQYUkN7D=DX zj#@5#GR>7L=mQbzfCI&5X36|_F+(=XI5OL_Rr+LZcZc-Jtaqp6T~fDZmfxrHd4bu` zI{dIKa0JXU$MC@&JWWTkD(oF)yU$43nt8jpB#Zq9W;?FSIBTYU13I&A$++8+?@&Z~ z8#C*!ifK4I_L(g39LybhB@2>i>ewriY1&BNnkf~=2ODH3^WVm-zbNuqZ!x`jfWt6% zkb^8pW`L9Q$sBm@U~a$z%!XH%`PR(*DpDu2Tum@_KdF;>Kx(JSj5;!d%s`{$AgNn3 z8&(%OvqEM5yO{Nb$v85na_yvVZ!yR^Q`E~oVs2@joz^W~WsEhmt|*3ryUBbq+tfpH zjLh#T^U3zePmuoK;06f)9~JmZylR^F$~l`tDX`#g zGT?^fTQbm^d9iyab!%q%KcsHW)E`ORnt7@h@RVjM_G z8p@NjLmKvoJJkgr?6R(44vpRv|6%3^lVrUXPO}*QBy&fSu_12jGkkEvqvVGFjnm$m z!9g-c7Wg~N_;HBmhQ~{u0H!`k)HgcHK~b%qOW#*31TLg(b7W=>T~Iz(i);Xn^_{ zzy_Eg{fW4w!2BDJRuS|5$trv5{gc%j>;@!j=HPn&WL4{yw>`SmGXC>}Q+XQy=K%9r z%O|Q#WS-UUpR8(=r2pcHs@9z-|NhA;dcyh%D{ngQpRB%rvZ_5$eg9;Y-^JebM3vVO z=JT|9|76vCcY6P1m7@Xa{gYMeC#vib`s|$dPgcdtbi}L=->iZ|F@1Lw{ zr^4TQu*t~gHu78L`zNdapFUZ|KlK0OlhyUP9=acM(~`gJsZZ*9A}yf8zixKE-FWe= zh$?Y4U#U*Njm~Y=dPdKU*PVNNR`mRN?zX3&U;4O#Q`rg+*90qNT>QqC7`#Q~9C(az zrc+)Pe~AYYL*;CTW6x$u&6q>cvBp)0McS&VhQ|+DPXDON)?J(Q*$-`^b|-wgaAt@{ zNI`7(v~lNp6zUZ^d`ZInJ$GF`{OUr|0tlL>^mBX{#X{P*-U$p7o-n2NP(UyQl#`8Ct^NC#6Xz7XJ)(sADnwin*PQUCnWfnZ} zckJ3OHn&E_o3TNjTXugt%CF}9k=w47S-!@;NN}a06KAa1F!@eQwS`-H#rUX;Qi~iN zVdpfxwN=Gmh=?+3hn8v9^`~A;b$wu;dh_awULAiJP-^U_ZmxUAKj>uFJtjM&3`pEbO^okvU5L&=&AdjE_CT;5>uY(T*Q>ey5_o3BKNVbgKo6& ze*zjKCXmL8e9|}({1h}^Oeakc*FmCVfzDpEe1@Wv#H?o!>|R1BpfE)=eGXv*h2_s7 zOchTl^nC>(>IH=9V(ANAhYT0hUf37FH;GOK@OQ({rk^1UFHvBK4#I$!5QNxBp^6H@ z;}wKiqTeeBrzsquFek%R?I6bMRr`$LYIEJZj6HCJY>@4vgTFwe>LC0^;T(lbQB{TT zwJn4xDuhKMmqKJA2%&li--!u&2u~>7rtrN8wt=vuFobzF5SEJT5Om8#ge~X?F^lvs zagVfIG%W;LAu>rT#Z%HM(WWqHwOC48BXoA4wW1U0N3ojplQ0-S>qInZz1T?FAW9Yi zZ4~`TKZ_jFCgJ)4C`%-gvc(?KX5n2F^ovL(Z4pOEIijjPXsZ}O+9q;I+l7BI&<-(y zv{U4hc8TEPpxt6RX^*&0+AAVTfcA-5r2XO^>40cj5_C{xk`9Tdq{E_3DbNwIlyp?+ z96-lJC(?1Tnsh=K96={VH0hMsNIES_I)QRUKT@8^A)OJfr9o#!BI%siLpm?Kok15w zD(Rv)Ldq9aT|mEz5u{5ZmvmY9V?^k#hzX>tBA;|k1iSGNdhigs;SgRI*D2svpZ4Or zG7xTxd$ve6^$2yajpZj|E?rQ4v^$AVX@wy^go|vyHUDcV>^p0ZfUA1;FSBmk0xaE3d?Q?Ml z)3-8)Ps>Ya3Ko3`?|Z64+G3f-M?3B^>wBqj5k*C*Es+}68&s0oQmJt*Lk+1dgT~Ep znTwCqxOk0CECTpSZG~A&(-xrkNfE2tv_Sr!wk7{ThqOv+T+Y)@YOAHje?n%<{{pOm zhClugR5`}2A^j-hxX)ag$l^aqtt9NvRe9@MC&f|_@3Hwv>!s!Zd$QCvNR3ZKYe;P) zG@4F;3k>!em*cSwTr1!zk`8A(5F`I`nN%jKQ?$PuBQlz#U8mmq#i;uxl#G*be&=cAasqL4V7qp>L zI{*!T+V2BCm)ap2R}Q+iG7-!*fs88;j6@vA$x#_s0rph>$AO891z8UV+jLpsaaovy zEkkN2rB)dlCtVz5TwuuTDu8Bya%Egq*jz5p#Fd6L`Hx;(sh#EVr_CkZg{62-269Bl z0!-(n#u3ebzcXEs8{vqq4e(^kms(BOmbETi(#Wbg$jit9d|7J#+<(ggrz=wA!pdFp zxL=i8AT-Oet!vQm#~<-2b7UOXM^a-$7E0|V1a7zvkSR6GdPyU+Y0$L(w=A9vf;dA4 zased^2LlzLaU^r#QR9PfduSZVSWu~HA+U=>`*cLYDvrOfs_AzW8Q=SDJ6KqU7fX9?tMX9xi z%|psm2^xFm6M*A}XG;|s*MZ~D8>a+Mm#R|i2%GEBn5s#Qs~54%PfOLowy-(_T$#t@ zC*!)nJ})(Ysda^R0XEN|0I5a6&e!75|AA8M263Gx;?$B_cW6IIt+v#9KwH6GN2(*W zXxO}@GI1R(i^KqXpoN3^gqKBn0HUZ#TNgTp7rMV1Dgm@D+k6{a`CBeQ9 zo5!%F)RJLeg3V*tN@^*vdCO(`NNR&%_l4FI+*)dbVRw{qZ8Qxve+H2wlY7}#267=% zIKb9_EVWeF>@8~A(q-^a;1s~rUdD~*`HXb2xaj{(tt7sBre?L_#h*_hrUBCdE)L^D zF|JNa0R{nFYS#zo3-kl}1D^tMzyM$%5Dz2(i6Zg2THT2cwORn2EVTkY0$PjA=c=>t zdZE?`4Tm2AGyoa`jerk<#y}Gw5@-rE1NZ}{EMPOR1;`Q6FI0z5cEj8Y>;nz}2Z2Mt z5#T7mxz=&u1aJ~K4deplQDp_7B2Wpa46H=Ya4BeM8?pC=TJA$W)8GS5u3LQxyaKqm z^)c`ScnUlNo&zrcE}*>)+yyvIyAM1Nt_5lt|A{a;OPd04*>wsq82Ahr0;B>%fg~US zNEERJYM>z=CeNk%BC`OK`;WzKR2JY1r8M9S@T$wJET?RC0B2-2fGto6;7#$CD(b#e z8?;Hsevbmi0Aqo1z<6K^@Fjo=k1`G59LEHF1#oWjJ4VM{;6CsGcnJIfJQ7Do2djJc)Td<)xD6DKCXQZFoxXD%M>@yi&{S`ia1BL@%09+lK4lF=Rz5#Bt(Lf&Hgbv^uiDy7f#P|VRfLjHq z%7wL@2)P2B{FDM50Inb|4A=o&=DZEq4(tGS0-XQM1~|W&3(Nx+01JUkU=i>wFbkLp zOao>BUjbYaKOFc1NaN2f(qW7Mh5_Ax?f}nVURZgX2mopVBfTh4PfYZ#g0B3@ns;uA)KLeNvdA+NA1TYHVg_yUOc+phVmrwf@<`Lj1upig~>;yQ&2nP6z z$$P+kAP~cei@Lesn~TJ~0Io4F3lsl_4eSB9YU&~I z2k;2s3i|4RCr}9uxr~na1;_?s0M341LFZJd0+`=Dc?<6i^anlx@a#mpT{q^}v`9Ej zf#yI9pe4`>;H{eX_2$^&>0wpc;3p7 z!9EV01Wp0lfq^`U^MKO;zg*@ba1ttS1MmR^A1(|+rJT0&Yy3!P6M@OVW5jX-QyaJg zzZ1$B5MLDd4gN52SMWoWn-2H^oRii9T0t)dPOE?qdnptL^Xt@O;0fRYlmRXP{3f&n z_yNcS<^W#6ks~Qietb5KnNh<^Gk}hL2w0t-#hp{1K%ssl#ytCJ%DrlAb>L_ zPTf6#JTz!GZ~?dotO0%mmI6zFuYnAJm0N?~nk@bv*n6#fi_Y0{SG71sJPL0Feq<#g zv5>w)T4R_gXa{2*07t;--|QL49|ueZI4Ne^Lx6A~LfZVf$p^pzw2|}N!@v>XC~ytn zw3O3OPCK^(tAGhSDfXjr2Y^EW8^QS|H%5CMupXEK|2M$0;ZrDV=|DE*82oT>1Yp@| z3vWSvaX=SFasAm92#v_0?s^0)!Q8;|r&VwStRqs%7 z4a}D)y%5|M5Wr_>E^k#lb+p^l`j&S3kBS>cK;s$ER%ANsoqYx&EFJCyU_3Am;N6RN zvC+UNUpjz^!ki57=Eose9b4c$gf~JHz#HRCpb|8mJ0k$vv!HSKzCxPM z7-*K^JQuzRK8N@`pcw20V9s+^gLw+@RKO|GSvwaN!TARG7WfWW3@inf0V{xi0m}gv zHeY+q*Wq38cLF;AUJbSZTLE4$TYx9iD{FV9mv9Pz7l5@rp4S+puCqjGH+{ABjokEORcFg<6)kU*Q+0v9 zMqj`6yWI61dInlvnrL}foT}><;Op;;FFKajD_Y(vr{x6t`YYl{s@^3Hb5P5R7%lIg zLqu&~l*Wt&cXb1Xm*ow0$O-cGW385#Ia=Ofha7)w54Bld_h@;`9db~@m)F~$&9zzH zgNK}edcJ|a7$R@kkkQw_7TREWE2QORc-n^iSY~jR@JiRa)RFIfOkcaH@5P@sWp1!R z$7lh{jx6m-NP4erW7e(A*jwaH&iyPgkYz3Jk{q*k$I_2R+zNdYVR_qR$F?a4b`C#$ z>`l&)Y>|bsYGjT$nXV70(=`XzU35}&gKcQBK9Qesw|wiclTwjW0y)Qzdv@-;)PSE` zYn_Cg2{|Hcgg(G=0U}%xG4oFEgzdlDjC0gw zOI#hT4^VIH5^iI_e!E4hG3ZRoiz`DPY+1dxMD4yPi2=%SWOb@@C4V!5Il{UGFcj(P4!82I9J%a$KxJL>R7I%qny}GKOjV{2K=L4o_nC@`AEf08N zyU(@(%35-~a>YTGwY)&HYRruOm2>os-$Ynmu~~CnOaGzsFZ;g98I~*DC!=NZt(@tf z^$NapYf=8?HxZk1MQcRVvAoB#uj8Jzj^`KWy@{~A>GQ-2y<^P9;pN}txa5iXlW~|k zUl2#h{V#|@Q*iEB-n{9u^5+sQ>-DWG4<^p;9Tzaw!CqFZ8u{yxfhTO=P7SWI4?g89zXW8+e6;uI9wE2D63YuC?uO||cuD)8TDn*mpm%e(yKHX5FBiN0 z+A=;o=FRyUby*afiW9EJ714Dn+7Nn044#TU`8U7F5`LRTS~45|UJS=QLWED#FHEz% z-S*kc$dHoT7jKK{H_*3#GH&9l^UhQ{rORlv1#?U6N`*&`PCe^fzB2&+2k@t#**M#M z?gYmn@GHXKty?+UW5zc#=fSs!f35A_UH#7(K0D1X`bZt*yM2q)8x$1gttzk_1&vf$*DB#}5zU)I%Z_Z!#;b-uQJ0(=9-h%w?q6Ds3jU(8HMrbh zC-M5-grxX{*rb$H;XhMf#??wdNh?pZ`9ojP$$X1Oj@dv(xIeEi+T3Px1reTLQ*{0D Jc$*1@{tsf?v55cx delta 21104 zcmeI4dz?+x|Nr+nXNEcBZd}I@99R2)!>-YIy{;?kOItsrlN6Y*zS|9-eGep9#}90ym1`ww{V zo&k4fI^MN`{iP~q=dE>|63EBbIZk;vbI^bX*@AQOUB@YfzkR*q6oV_h=QxGo`-Tis zwsYT*441d@Sp>c0h9LW4KmOaaMf$)#8NDB_+i%bpHoy1A;P!gooAJQ?juRMCxy?5D8GW)95g-G|L|+#J*ca8dLxVdeMlGw_~14IF0_@rCgF4XS(30|PRhom+zO zgVA+}`eqDzIE(fT>@#>!zrI7Au3H@^2E7c8*1lxI?3*vA&!#|hnX}njcRNlj`YV=a z!e#J>!SRZRb&T3tPJ+wf$HHad0ZAMIGK=t9;zb1#TBZ1XTqd{S6%6Dj zrTDwyDY8Id!H&ieIIRNd-!+ZGxXCTQ;LgSoTu*6%^xIPX_hTg@N4V9_vK~#LVO#K8 zgz_RQaSgQ$#CJ>eXJVzI#|IK_OYzT@e!)9|$N9)98CcLNC9*o#+YKt?eRyMF|K4W) zl?V;UjtFF=r1-zaYlT-(!T#D@IM?I(0vXLy{Il_zx{(PSheOH>q&H9Xm*+BU=tk*W zb;nawaL*UuslG^#Lu1e8!2aZB{+e8tiuVK(8m0LEiPy}H(DrBJ={(Sy8&mxI@l=;L zkdTxTS%%BCc_4mbs{dZBZEZI$ZJ!qU+u zin?;~s4F?udv;qOHm_M^-R(Ln@p-9{Sy(p(;tw_TY!AfdH;=fG(ei!zq#H6Gix7f$ zVFino{vec_j@7}odKUlTQ&_bf& zTX21f_sLy>*rUz7t9Au?9c>nQc(>#92&5lPjlBNjkTn(S7FRorb%$%Eed0LXTx*7F z67x^mQO^WoN!Rl~9zwrx|o7*-uawHa`33YA2qI)z<^%lwx>_5@W z-#OoL+EaupGC9TjOn#u($!6Z~^8>R_HuJYU>^L`)=XJ+|C-F3llnmrgPVsIz9O(6Z zGk?`1!A`@FkdorLo}?D~#-0Gxtw3Q!7X zA3kG@;{mBa>F==0b7&kfZFlQoh;+FnH&IM?H*`aEIs@)sPCuX|_JaH6kixkDein!T zgMs3P0HrIj;)Vk8FuQ$>U=pjP!>upYo~xPS9H5i~6gLt`e-fx6W2`?GR{Bpljv!Su z0jQ#hK*?p39_^_Vc;LsHfeAlGWvjtc+{_>=e5&=sS#i^>E>^o{0&QjvP`VPU-e-Ug z@N+;3_Bx(G-JdGCC+zv4>WHp;!O1(Dz#pd^;Q4k)byihs}Y zM$4OFCAg%!p=~&7Cxbte(!8I_n$v|m#mlP=jIwSDXOD?5>?xb1su?5Ql8lCK600T# zKzGv&aBdQN0{u^yttFQs%1vTFI2?2@VUJl>*i*?3KWekXS#A3oXk*_fKXBr7S&w7R z(XzH`*>@62V)-ZKki-d~7$sFnNz0{G6DgcMf%G#qvK19;bHy?%@KFFxfD6HOBwChP zmk*VzZ}o64gMO3M{{jcE>Aw(2wSTMPzZf6fzT0ATu6DNNV(no^>x*S}vVJ(L+^shM zHk&V2r@r0#VwwN6oNo1WQ=*tBrUW_89aeHVP~_)I8EQZ#s)pNsHaDCl-N%Rcew!~= zhv;wpE3qdq_vd=q)rp=?w3<8DmJ;jQd(Qg*4y%fVe2AatLmg|WoFHer7xgPvxE!k? zuamFcT4_1g#)q@=SEDQIEj}v2`)ph|EADfvf9|<0II0{%C_=_}HbX4)g!TUxn{&lI zH3A)ejdHK9A8i$4^_8>MzZ|P=zmPBf&Bpx|Ru}Q8(4`#g-QgLk(@S7sxG1a!m4v0o z*a|Mk$|`O1#oAz;<*HWyzd42|Qg{aAAtG*1Y{{`06>hc9t;oY{vKCmvC`)tKx?rEiHzmx~9ESC9@^~GvWmi7M%M-xBH z#{UHl_Qvo)m3^EKwLb?|14h^i#oCY2*1r-fWQ>gy%N)mt>KkwM@!kr-ZadWq;mipx z?Fr6Trjf0P88$4Ob>e4RJ)G6fc~%!|f1b0xSO@B5>x*T+!iR3It6{bCEw5yAqBJK8 z*Lhn(e_}~%tS(l2)>~h!?QXQbSoLnQyxHpEtn%BfF4lg0c`^H9xv-D%=%KFyEeP{WEMAGG0S>N-a!}_a@{|(me zocG#!I)+=Zdr-&_>-hNe@h6s4fDbjOpv}J=YXgPIr)sB&jT37}OIp9AkJG1sQdSUa zxU2wc197lgUft%0v+`?LU9576u=ILX7i)tJt$v-=#fodByN-+$n-R`xSQB(*HMRLy zV%2kljT38ncboOYS#e#^#gSbVFnx#T9;U2y%iDo7-Ifxo6?ac98zWYuAA@x+Mp*v|oBx;Cbo6@aC8;Y+ zw8fvc#l@N@O@r0688+W2-{i&8q@!L>xwaZVp0{}~D32sonMKwQXEk}Tjb8$*KQEJS z(ks|uXSppV)+FH_Wy5PMziacw($~V;+!eQi;dp~D}TH7Kk(V>7Q@~5 z-7tU79?PG}AzhBufc-XKtcpLkd;r#l^I`s+qkJg-D>;`i*R2YEZ3B*3{?-PDv#!e@ zt^WVRI_baL`p;W-+<6cgIxT952UbgcI!%9K(pC_wqOvxl9IQhaFWoGQw^ww6&8`9~g|q6bYV)tL`C?UD-Rd>0E|y-)>epI5 zoHg#GSse~eCBg)_Ji9GZ_(f0pf0Ws?2&fr7Cv;uI>YK3w@bVdhq}st8W>iQ zHh7Qq|6R_$QU%(Rd)bh-^#C8*@FRA^e}~m49<}BE#Mzh4P=UuNpawi{c{nUR$5td( z{s>qX$&*&U94r4Rn~$mg`?x5m{uln>QVI)ne&2cT?_r01S) z|CgaX{vUe)YPUB*Pxf>UuRZ`(&wdYxH(K5dE5TPEfL?t7>h^{I_XAdYCsTk%x~mUB z-Q#uj0qE5SpqD-n)d+j_0VoII>H|=Bgm52}YOWssJX1MW@PlJ*cz}A<)d!%#8Tr)* zpzfsh>I2Xk`cgp>Uwr`jU-2OH>H|=^(;~W)zBOKb02=(hc=ZA3)d!&V;a1w;X8X1A z?-$g-9(+=J^#Q1k#FZY1YIgXi2cIGTf8zn@`C{iae!405WM$91eddL8p7x$c&8c&q z?w(<$?Rn@ivq1E?`BgOBbp8X%HcKG$&F`Kf(~Ef9dqzy}>FsW2p2J;(JJR?)-tL|! zO@?TcSuGlEN_e5COh3^WvtBgTl=peNPw(L^VjlNVa=h7wZ=yU1bt4canxPR0t0d$} zm}F{2BHZgmm>7vL#q5(%&WCVA0fc}VTL59JgcA~`nvnGfScozL8L*5W)h}vk=0}D1bO!=Z5m8B+2^pe>odf6ltgO-`0 zqF2l=(W|CbacH?2AzESfiC!~FC7{>MSkW6MU$oLRDG9x4rigOQG0`e>V+^#~%o4q2 zPKn+&ZA(G#m<6IW=2y|XrgLd%tyv;kXFO$~^`@ujJ+nfz!T8HU8%>62lUXg=Y)X`a zwwQjRt!BMwn<-x&+HSH$@0)F+4@^QVw8IP)eQ0)xJ~Fi`Ks(I{(Jr%3wA&=bK_8p3 zqEAe|Xpd<0}`j&s*-J@FAo8w}uX>meZt1Ypb7NoyJ3$&9<(fHHP z!<^k7d!y83tvuRERYKBoC1uBg0ct7f z9W-U@v}IZCT?CogaeNH2;#wORk2ct9TDK&#A{b(|_pDY4ZK%~Y*s_(;9<^F%ZBqi; zaI0y36Ms$>Fv7$6SEtyDqWr4hX&boBYU)^%t)@j#{J9-#n$JgvbEGZ!iri@CG%QPz-k{`O}+PSp!A8=)Oq!BkH^j zfjVtyK~^Gm=zslMl%=Zcss9zXy>-78Rat0V#^+W`LfdT5`vI#pK$~T?JT(6FTPSBq8JTH7htcPgc7LZG{Suv_s0i8N6n-vo@{`+8nfMI{rUfu`RaN^sCqYf~G^+4qR&s z>rl#U58^P?lZ#oc19o{db>!k`{5c{d)&7^`){o&=Q2pt*}?|A8|CsCR3$qKtD3 zcmODAw34QOY44Lj(hoGG-3oM0brom;lg6TL{r1Y$Ld)v9;Omqs#Sy0?(^dUX9Y>c- zMO(NVb{Vb+T`rZZ)*V~xaFi-rtp~PNCn+Vsx-sSRf_aBu9}(cDgEwY$+)u@QB$WUKYT*6Mq8GOakIh|?F02Rem~Y}`HA zUjdy$EjUzL?*-XviOx-9EB3=qvzk^SD)2s_bzeH<*IVs=>>XB1g|%VzmyfJ=gN^Ht zw$p0Oto9(e6G|=)GEtSzaoqpgx zaJ!l1^VP1^7gGz7ZUwi2E}$#u2D*bDpeN{M%18Jr+;B5y8=xssJJ23zumfK3}~X^lOc`!5Xj@tOFasMz9HN23x>3upN8=c7P876aENa*=zS= zeg?`>Yk3e0Du6if3Wp>bE(8X8&3h5PDs{D1P7C*b0;j>Z;5%>voCM#4AHXRvl5dc{ z3Q6NJCxC%q5Eu;p1%`k}z)+9{27re^rWqXROZKaus$Zs=6_LL7o*L#Hl{F}hqTx{^ zpGG>3Y&vzCdTE7n1c(H>x#&hTz+>7K@UWmgGXOMq zIY~G89-IO{f}g-?a0dKr@(TExC+T9<_^R<#*C^wqqoGjOp)M0$7V7dEr8FwtWjYr0 zRrRzqSp|K`?$1G!P#b_`a2;p_wB%|cthHbL!GmA`cnD;HfnX4L80a+^&(hchK>u<+ z3G`!~QtY#S2=gn@y1{Bd>kG96u_9oelkF76(A=gVhyq$Dsa2KdfaW(Fz(%kMXl^qL zXznrxJOiEunzzgc&w+(tCU_c50#iT$OazaC$H8!r4RW;1dl*JKxC7h+? zcpGqU&vstMcmcc!Mt~M<_%2wB=ym^W40OMBfX4R!0KbDc5DzMW${+z$0ZVD@OWI&aKSyb{o(ZyokRTECi!~Zdk*?QSc==3_jM>Y&XUSUfGeE0@D+4{WuSP?TaA4jA?|=-TdBb`1KR{KWuX4IQ_W}JtSI`aUPTU&YtgmKm zFxr9kpabX#bQ>NF+Og4Z;7`CFupMa0^By*I6nqKRf^}dG*h$$nU>)cQh7qsZw7%|r z4D{9QQ?LQ_2m5pZ?**R;1m;lrO+XJ4^o-zPD%EURU(xv{>x>0wiPOZSAvl5GiTEgD z3xTikhr#->>14{(0}Vhj=!m9CY*kR$`q@P>^abQB_!*Q1u|VH|^zCLTm=9(H17?Am zU;>y7RA~mB1g-}&@b#s`-56XI=zD~|H|Tr92pXLVG<8qW`PU3cQ}L2uAC1`p4uV5q z1$Z4S28)0J(|{@u$2r>&X?!1r2IIk3fX2J)d^KB3v$ht?JKzJV)x7m1uoLV8Uja=sHLcV%ay@tzjG@T; z`i}Ymjr$O&5t=`0W3u&o*Ehig{I5Z1!=F-EwhrL+U^jlKF)A01wbXY%=miuTs$(zq z5AJl4Ru_&n$BX%@r|WB>x`Mug>JFv(#|dxiv?JO( z)cc>p)?hUrOyrrYFD6@kQg^~XNYi266XZ_;x>E-5r^32@7R824tNI41-cd%fEU0*@H|jiT`P;cjwzVnyROP>I4i(%putvy?lSN)cnK`^n%Hu_ z>U#Q};H%U~qsB)-XMYD!PoPVZQ40H%p?i-*yonUFg57?)y{}Zh7;S4x^sa?oP)3lXzc9O}g z;=49?OcmcaZ*HAxzAnAvLN6uzZAP2a5*uIK;PqUWSU*vnujyN6{#akRMxoc3-MwMd zo3Bpa9v$IHCPF79^rEwbq{(A9d%Fz3n6vRMb5dnPuSV-}>%%)XKfd$hixHuhrNHT&1<{^VpnEKB?5J*Ajn`Ql2}K5|a|^Gjgsq z&l6dyH4)_*m}i{q`_RV25l@x!q^>o`$N8?0yMvr~a!&Sq`pq}Kd1Fp-&mGB$NsSvP z*3VmO(#HE{dylU(2gb9N?dyYY3LCKU`A=T#aQKW@k@XXqFjsod6rVt(e}j2>g0Gyn zByY7jhNp|_%~ ztutqTYS(h@$Vp0UplyuWWI8-eU6VJN95^oY9=5J`efP(2lQYh`b%_Vw7AbE^Xw$* z3cZc(se-kh?YQu@MQ*#25*f8Jx8?4hpln>|9c^XK6kWVM z`V%^EgT#h94$h=cXH2K^&`aHxHTWg+{m2%xF9vL8W>ArHIpMjL*JrI56?-w~=%=QW z%7$JPSMSY^Nsm5zH1T3Y=v8vpe%G&5%*?2)i#e_LnrA2*7kc~LwQbv$kNN)P#TO%n z?=?FW5qe+Uo>zROGQW7d>cxn6_nL?q^cW}4G!_@nGY^Qn<(c_2XjkYxan)+IIsC|j zdm`;Q<+2UENv^`yal`8WR%MObFX|^UqD;;+Csj7|4!WbKn~WJfQ2ZJko)Ix}9sxZM+Z<{=`y=km-rL+=Z=v~(mV z!WK5sGktZUZq9dqU6o_jjrYaI9mFfa8Cd(p9fvy(NXztk^cYhq`LKC-CiQranBudj zH|B^rFq0$jCqFt(lPpr&6w`N>FU>5T<(uz~d&{(&?W@@C_!{?FY0fogOO@{3=;+Cq zcio5eIa5AgJv;81v85{FS7Y;AJ+ID=pZeU4miR^RzrJ<*mis>U4|!-xu<}iVb6=b7 f+q^n=cejW!*O;?iBkG%59*ZcP+jm&R*aH6tr-Bx9 diff --git a/test/harness.ts b/test/harness.ts index a0952b6f4d..416df595c7 100644 --- a/test/harness.ts +++ b/test/harness.ts @@ -1,5 +1,5 @@ import { gc as bunGC, unsafe, which } from "bun"; -import { expect } from "bun:test"; +import { describe, test, expect, afterAll, beforeAll } from "bun:test"; import { readlink, readFile } from "fs/promises"; import { isAbsolute } from "path"; import { openSync, closeSync } from "node:fs"; @@ -323,3 +323,109 @@ declare global { Buffer.prototype.toUnixString = function () { return this.toString("utf-8").replaceAll("\r\n", "\n"); }; + +export function dockerExe(): string | null { + return which("docker") || which("podman") || null; +} + +export async function waitForPort(port: number, timeout: number = 60_000): Promise { + let deadline = Date.now() + Math.max(1, timeout); + let error: unknown; + while (Date.now() < deadline) { + error = await new Promise(resolve => { + Bun.connect({ + hostname: "localhost", + port, + socket: { + data: socket => { + resolve(undefined); + socket.end(); + }, + end: () => resolve(new Error("Socket closed")), + error: (_, cause) => resolve(new Error("Socket error", { cause })), + connectError: (_, cause) => resolve(new Error("Socket connect error", { cause })), + }, + }); + }); + if (error) { + await Bun.sleep(1000); + } else { + return; + } + } + throw error; +} + +export async function describeWithContainer( + label: string, + { + image, + env = {}, + args = [], + archs, + }: { + image: string; + env?: Record; + args?: string[]; + archs?: NodeJS.Architecture[]; + }, + fn: (port: number) => void, +) { + describe(label, () => { + const docker = dockerExe(); + if (!docker) { + test.skip(`docker is not installed, skipped: ${image}`, () => {}); + return; + } + const { arch, platform } = process; + if ((archs && !archs?.includes(arch)) || platform === "win32") { + test.skip(`docker image is not supported on ${platform}/${arch}, skipped: ${image}`, () => {}); + return false; + } + let containerId: string; + { + const envs = Object.entries(env).map(([k, v]) => `-e${k}=${v}`); + const { exitCode, stdout, stderr } = Bun.spawnSync({ + cmd: [docker, "run", "--rm", "-dPit", ...envs, image, ...args], + stdout: "pipe", + stderr: "pipe", + }); + if (exitCode !== 0) { + process.stderr.write(stderr); + test.skip(`docker container for ${image} failed to start`, () => {}); + return false; + } + containerId = stdout.toString("utf-8").trim(); + } + let port: number; + { + const { exitCode, stdout, stderr } = Bun.spawnSync({ + cmd: [docker, "port", containerId], + stdout: "pipe", + stderr: "pipe", + }); + if (exitCode !== 0) { + process.stderr.write(stderr); + test.skip(`docker container for ${image} failed to find a port`, () => {}); + return false; + } + const [firstPort] = stdout + .toString("utf-8") + .trim() + .split("\n") + .map(line => parseInt(line.split(":").pop()!)); + port = firstPort; + } + beforeAll(async () => { + await waitForPort(port); + }); + afterAll(() => { + Bun.spawnSync({ + cmd: [docker, "rm", "-f", containerId], + stdout: "ignore", + stderr: "ignore", + }); + }); + fn(port); + }); +} diff --git a/test/integration/mysql2/mysql2.test.ts b/test/integration/mysql2/mysql2.test.ts new file mode 100644 index 0000000000..cb0ef919f6 --- /dev/null +++ b/test/integration/mysql2/mysql2.test.ts @@ -0,0 +1,63 @@ +import { test, expect } from "bun:test"; +import { describeWithContainer } from "harness"; +import type { Connection, ConnectionOptions } from "mysql2/promise"; +import { createConnection } from "mysql2/promise"; + +const tests: { + label: string; + database: { + image: string; + env?: Record; + }; + client: ConnectionOptions; +}[] = [ + { + label: "mysql:8 with root user and password", + database: { + image: "mysql:8", + env: { + MYSQL_ROOT_PASSWORD: "bun", + }, + }, + client: { + user: "root", + password: "bun", + }, + }, + { + label: "mysql:8 with root user and empty password", + database: { + image: "mysql:8", + env: { + MYSQL_ALLOW_EMPTY_PASSWORD: "yes", + }, + }, + client: { + user: "root", + password: "", + }, + }, +]; + +for (const { label, client, database } of tests) { + describeWithContainer(label, database, (port: number) => { + let sql: Connection; + test("can connect to database", async () => { + sql = await createConnection({ + ...client, + port, + }); + }); + test("can query database", async () => { + const result = await sql?.query("SELECT 1"); + expect(result).toBeArrayOfSize(2); + const [rows, fields] = result; + expect(rows).toBeArrayOfSize(1); + const [row] = rows as any[]; + expect(row).toMatchObject({ "1": 1 }); + }); + test("can close database", async () => { + await sql?.end(); + }); + }); +} diff --git a/test/package.json b/test/package.json index 7f2c75454a..dcd4d721c8 100644 --- a/test/package.json +++ b/test/package.json @@ -28,6 +28,7 @@ "lodash": "4.17.21", "mongodb": "6.0.0", "msgpackr-extract": "3.0.2", + "mysql2": "3.7.0", "node-gyp": "10.0.1", "nodemailer": "6.9.3", "pg": "8.11.1", From 330a4744de80c3229ae65787cd80c6cae8ea5ac2 Mon Sep 17 00:00:00 2001 From: "Tiramify (A.K. Daniel)" <94789999+TiranexDev@users.noreply.github.com> Date: Fri, 2 Feb 2024 01:31:54 +0000 Subject: [PATCH 43/45] chore: fix compiling fd.zig on linux (#8630) --- src/fd.zig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/fd.zig b/src/fd.zig index 7bfe837cf0..a1ade3c6ef 100644 --- a/src/fd.zig +++ b/src/fd.zig @@ -312,7 +312,7 @@ pub const FDImpl = packed struct { const fd = this.system(); try writer.print("{d}", .{fd}); if (env.isDebug and fd >= 3) print_with_path: { - var path_buf: [1024]u8 = undefined; + var path_buf: bun.PathBuffer = undefined; const path = std.os.getFdPath(fd, &path_buf) catch break :print_with_path; try writer.print("[{s}]", .{path}); } From 8808437a02418d8880d09b91978ea1b101b41817 Mon Sep 17 00:00:00 2001 From: Meghan Denny Date: Thu, 1 Feb 2024 19:24:17 -0800 Subject: [PATCH 44/45] meta: remove remaining references to prettierrc.cjs (#8634) --- Dockerfile | 3 +-- Makefile | 1 - bench/package.json | 3 +-- packages/bun-vscode/assets/package.json | 3 --- test/snippets/package.json | 3 +-- 5 files changed, 3 insertions(+), 10 deletions(-) diff --git a/Dockerfile b/Dockerfile index f0f9871de8..392db49f34 100644 --- a/Dockerfile +++ b/Dockerfile @@ -290,7 +290,6 @@ ENV CCACHE_DIR=/ccache COPY Makefile ${BUN_DIR}/Makefile COPY src/deps/zstd ${BUN_DIR}/src/deps/zstd -COPY .prettierrc.cjs ${BUN_DIR}/.prettierrc.cjs WORKDIR $BUN_DIR @@ -378,7 +377,7 @@ RUN --mount=type=cache,target=/ccache mkdir ${BUN_DIR}/build \ FROM bun-base-with-zig as bun-codegen-for-zig -COPY package.json bun.lockb Makefile .gitmodules .prettierrc.cjs ${BUN_DIR}/ +COPY package.json bun.lockb Makefile .gitmodules ${BUN_DIR}/ COPY src/runtime ${BUN_DIR}/src/runtime COPY src/runtime.js src/runtime.bun.js ${BUN_DIR}/src/ COPY packages/bun-error ${BUN_DIR}/packages/bun-error diff --git a/Makefile b/Makefile index 580f1c37ff..8a6142c4ff 100644 --- a/Makefile +++ b/Makefile @@ -823,7 +823,6 @@ fmt: fmt-cpp fmt-zig api: ./node_modules/.bin/peechy --schema src/api/schema.peechy --esm src/api/schema.js --ts src/api/schema.d.ts --zig src/api/schema.zig $(ZIG) fmt src/api/schema.zig - $(PRETTIER) --config=.prettierrc.cjs --write src/api/schema.js src/api/schema.d.ts .PHONY: node-fallbacks node-fallbacks: diff --git a/bench/package.json b/bench/package.json index 1e9d5bc9bb..b91097050b 100644 --- a/bench/package.json +++ b/bench/package.json @@ -25,6 +25,5 @@ }, "devDependencies": { "fast-deep-equal": "^3.1.3" - }, - "prettier": "../.prettierrc.cjs" + } } diff --git a/packages/bun-vscode/assets/package.json b/packages/bun-vscode/assets/package.json index 021c8125e7..b5faabfeb1 100644 --- a/packages/bun-vscode/assets/package.json +++ b/packages/bun-vscode/assets/package.json @@ -796,9 +796,6 @@ "eslintConfig": { "$ref": "https://json.schemastore.org/eslintrc.json" }, - "prettier": { - "$ref": "https://json.schemastore.org/prettierrc.json" - }, "stylelint": { "$ref": "https://json.schemastore.org/stylelintrc.json" }, diff --git a/test/snippets/package.json b/test/snippets/package.json index 478234d5c0..0c05b97bea 100644 --- a/test/snippets/package.json +++ b/test/snippets/package.json @@ -10,6 +10,5 @@ "react-dom": "^17.0.2", "redux": "^4.1.1", "styled-components": "^5.3.1" - }, - "prettier": "../../.prettierrc.cjs" + } } From 4959c7d36361fe57b638b05b636a096d189e5a19 Mon Sep 17 00:00:00 2001 From: Meghan Denny Date: Thu, 1 Feb 2024 20:35:47 -0800 Subject: [PATCH 45/45] remove ZigString.Slice.from .init is the exact same thing (#8632) Co-authored-by: Jarred Sumner --- src/bun.js/api/glob.zig | 6 +++--- src/bun.js/bindings/bindings.zig | 7 ------- src/bun.js/node/types.zig | 2 +- src/install/lockfile.zig | 2 +- 4 files changed, 5 insertions(+), 12 deletions(-) diff --git a/src/bun.js/api/glob.zig b/src/bun.js/api/glob.zig index d2be93a941..b57b61c48a 100644 --- a/src/bun.js/api/glob.zig +++ b/src/bun.js/api/glob.zig @@ -51,10 +51,10 @@ const ScanOpts = struct { return null; }; - break :cwd_str_raw ZigString.Slice.from(duped, allocator); + break :cwd_str_raw ZigString.Slice.init(allocator, duped); } - // Conver to utf-16 + // Convert to utf-16 const utf16 = bun.strings.toUTF16AllocForReal( allocator, cwd_zig_str.slice(), @@ -67,7 +67,7 @@ const ScanOpts = struct { }; const ptr: [*]u8 = @ptrCast(utf16.ptr); - break :cwd_str_raw ZigString.Slice.from(ptr[0 .. utf16.len * 2], allocator); + break :cwd_str_raw ZigString.Slice.init(allocator, ptr[0 .. utf16.len * 2]); } // `.toSlice()` internally converts to WTF-8 diff --git a/src/bun.js/bindings/bindings.zig b/src/bun.js/bindings/bindings.zig index 369ab361de..22ce921799 100644 --- a/src/bun.js/bindings/bindings.zig +++ b/src/bun.js/bindings/bindings.zig @@ -432,13 +432,6 @@ pub const ZigString = extern struct { pub const byteSlice = Slice.slice; - pub fn from(input: []u8, allocator: std.mem.Allocator) Slice { - return .{ - .ptr = input.ptr, - .len = @as(u32, @truncate(input.len)), - .allocator = NullableAllocator.init(allocator), - }; - } pub fn fromUTF8NeverFree(input: []const u8) Slice { return .{ diff --git a/src/bun.js/node/types.zig b/src/bun.js/node/types.zig index aa1a247b4e..7dc0946cc6 100644 --- a/src/bun.js/node/types.zig +++ b/src/bun.js/node/types.zig @@ -431,7 +431,7 @@ pub const StringOrBuffer = union(enum) { defer global.vm().reportExtraMemory(out.len); return .{ - .encoded_slice = JSC.ZigString.Slice.from(out, bun.default_allocator), + .encoded_slice = JSC.ZigString.Slice.init(bun.default_allocator, out), }; } diff --git a/src/install/lockfile.zig b/src/install/lockfile.zig index cbf5d5e006..602efc4aa0 100644 --- a/src/install/lockfile.zig +++ b/src/install/lockfile.zig @@ -1646,7 +1646,7 @@ pub fn saveToDisk(this: *Lockfile, filename: stringZ) void { .fd = bun.toFD(file.handle), }, .dirfd = bun.invalid_fd, - .data = .{ .string = .{ .utf8 = bun.JSC.ZigString.Slice.from(bytes.items, bun.default_allocator) } }, + .data = .{ .string = .{ .utf8 = bun.JSC.ZigString.Slice.init(bun.default_allocator, bytes.items) } }, }, .sync, )) {