diff --git a/build.zig b/build.zig index 024f692412..88f2e6f05d 100644 --- a/build.zig +++ b/build.zig @@ -591,6 +591,7 @@ pub fn build_(b: *Build) !void { \\For more info, see https://bun.sh/docs/project/contributing \\ }); + b.default_step.dependOn(&mistake_message.step); } diff --git a/src/standalone_bun.zig b/src/StandaloneModuleGraph.zig similarity index 71% rename from src/standalone_bun.zig rename to src/StandaloneModuleGraph.zig index c18772e0de..a2fffb6c33 100644 --- a/src/standalone_bun.zig +++ b/src/StandaloneModuleGraph.zig @@ -1,15 +1,17 @@ -// Originally, we tried using LIEF to inject the module graph into a MachO segment -// But this incurred a fixed 350ms overhead on every build, which is unacceptable -// so we give up on codesigning support on macOS for now until we can find a better solution +//! Originally, we tried using LIEF to inject the module graph into a MachO segment +//! But this incurred a fixed 350ms overhead on every build, which is unacceptable +//! so we give up on codesigning support on macOS for now until we can find a better solution const bun = @import("root").bun; const std = @import("std"); const Schema = bun.Schema.Api; const strings = bun.strings; - +const Output = bun.Output; +const Global = bun.Global; const Environment = bun.Environment; - const Syscall = bun.sys; +const w = std.os.windows; + pub const StandaloneModuleGraph = struct { bytes: []const u8 = "", files: bun.StringArrayHashMap(File), @@ -252,6 +254,40 @@ pub const StandaloneModuleGraph = struct { self_buf[self_exe.len] = 0; const self_exeZ = self_buf[0..self_exe.len :0]; + if (comptime Environment.isWindows) { + // copy self and then open it for writing + + var in_buf: bun.WPathBuffer = undefined; + strings.copyU8IntoU16(&in_buf, self_exeZ); + in_buf[self_exe.len] = 0; + const in = in_buf[0..self_exe.len :0]; + var out_buf: bun.WPathBuffer = undefined; + strings.copyU8IntoU16(&out_buf, zname); + out_buf[zname.len] = 0; + const out = out_buf[0..zname.len :0]; + + bun.copyFile(in, out) catch |err| { + Output.prettyErrorln("error: failed to copy bun executable into temporary file: {s}", .{@errorName(err)}); + Global.exit(1); + }; + + const file = bun.sys.ntCreateFile( + bun.invalid_fd, + out, + // access_mask + w.SYNCHRONIZE | w.GENERIC_WRITE | w.DELETE, + // create disposition + w.FILE_OPEN, + // create options + w.FILE_SYNCHRONOUS_IO_NONALERT | w.FILE_OPEN_REPARSE_POINT, + ).unwrap() catch |e| { + Output.prettyErrorln("error: failed to open temporary file to copy bun into\n{}", .{e}); + Global.exit(1); + }; + + break :brk file; + } + if (comptime Environment.isMac) { // if we're on a mac, use clonefile() if we can // failure is okay, clonefile is just a fast path. @@ -296,12 +332,12 @@ pub const StandaloneModuleGraph = struct { switch (err.getErrno()) { // try again .PERM, .AGAIN, .BUSY => continue, - else => {}, + else => break, } - } - Output.prettyErrorln("error: failed to open temporary file to copy bun into\n{}", .{err}); - Global.exit(1); + Output.prettyErrorln("error: failed to open temporary file to copy bun into\n{}", .{err}); + Global.exit(1); + } }, } } @@ -331,66 +367,60 @@ pub const StandaloneModuleGraph = struct { defer _ = Syscall.close(self_fd); - if (comptime Environment.isWindows) { - var in_buf: bun.WPathBuffer = undefined; - strings.copyU8IntoU16(&in_buf, self_exeZ); - const in = in_buf[0..self_exe.len :0]; - var out_buf: bun.WPathBuffer = undefined; - strings.copyU8IntoU16(&out_buf, zname); - const out = out_buf[0..zname.len :0]; - - bun.copyFile(in, out) catch |err| { - Output.prettyErrorln("error: failed to copy bun executable into temporary file: {s}", .{@errorName(err)}); - cleanup(zname, fd); - Global.exit(1); - }; - } else { - bun.copyFile(self_fd.cast(), fd.cast()) catch |err| { - Output.prettyErrorln("error: failed to copy bun executable into temporary file: {s}", .{@errorName(err)}); - cleanup(zname, fd); - Global.exit(1); - }; - } + bun.copyFile(self_fd.cast(), fd.cast()) catch |err| { + Output.prettyErrorln("error: failed to copy bun executable into temporary file: {s}", .{@errorName(err)}); + cleanup(zname, fd); + Global.exit(1); + }; break :brk fd; }; - const seek_position = @as(u64, @intCast(brk: { - const fstat = switch (Syscall.fstat(cloned_executable_fd)) { - .result => |res| res, + var total_byte_count: usize = undefined; + + if (Environment.isWindows) { + total_byte_count = bytes.len + 8 + (Syscall.setFileOffsetToEndWindows(cloned_executable_fd).unwrap() catch |err| { + Output.prettyErrorln("error: failed to seek to end of temporary file\n{}", .{err}); + cleanup(zname, cloned_executable_fd); + Global.exit(1); + }); + } else { + const seek_position = @as(u64, @intCast(brk: { + const fstat = switch (Syscall.fstat(cloned_executable_fd)) { + .result => |res| res, + .err => |err| { + Output.prettyErrorln("{}", .{err}); + cleanup(zname, cloned_executable_fd); + Global.exit(1); + }, + }; + + break :brk @max(fstat.size, 0); + })); + + total_byte_count = seek_position + bytes.len + 8; + + // From https://man7.org/linux/man-pages/man2/lseek.2.html + // + // lseek() allows the file offset to be set beyond the end of the + // file (but this does not change the size of the file). If data is + // later written at this point, subsequent reads of the data in the + // gap (a "hole") return null bytes ('\0') until data is actually + // written into the gap. + // + switch (Syscall.setFileOffset(cloned_executable_fd, seek_position)) { .err => |err| { - Output.prettyErrorln("{}", .{err}); + Output.prettyErrorln( + "{}\nwhile seeking to end of temporary file (pos: {d})", + .{ + err, + seek_position, + }, + ); cleanup(zname, cloned_executable_fd); Global.exit(1); }, - }; - - break :brk @max(fstat.size, 0); - })); - - const total_byte_count = seek_position + bytes.len + 8; - - // From https://man7.org/linux/man-pages/man2/lseek.2.html - // - // lseek() allows the file offset to be set beyond the end of the - // file (but this does not change the size of the file). If data is - // later written at this point, subsequent reads of the data in the - // gap (a "hole") return null bytes ('\0') until data is actually - - // written into the gap. - // - switch (Syscall.setFileOffset(cloned_executable_fd, seek_position)) { - .err => |err| { - Output.prettyErrorln( - "{}\nwhile seeking to end of temporary file (pos: {d})", - .{ - err, - seek_position, - }, - ); - cleanup(zname, cloned_executable_fd); - Global.exit(1); - }, - else => {}, + else => {}, + } } var remain = bytes; @@ -415,11 +445,50 @@ pub const StandaloneModuleGraph = struct { return cloned_executable_fd; } - pub fn toExecutable(allocator: std.mem.Allocator, output_files: []const bun.options.OutputFile, root_dir: std.fs.Dir, module_prefix: []const u8, outfile: []const u8) !void { + pub fn toExecutable( + allocator: std.mem.Allocator, + output_files: []const bun.options.OutputFile, + root_dir: std.fs.Dir, + module_prefix: []const u8, + outfile: []const u8, + ) !void { const bytes = try toBytes(allocator, module_prefix, output_files); if (bytes.len == 0) return; const fd = inject(bytes); + fd.assertKind(.system); + + if (Environment.isWindows) { + var outfile_buf: bun.OSPathBuffer = undefined; + const outfile_slice = brk: { + const outfile_w = bun.strings.toWPathNormalized(&outfile_buf, std.fs.path.basenameWindows(outfile)); + std.debug.assert(outfile_w.ptr == &outfile_buf); + const outfile_buf_u16 = bun.reinterpretSlice(u16, &outfile_buf); + if (!bun.strings.endsWithComptime(outfile, ".exe")) { + // append .exe + const suffix = comptime bun.strings.w(".exe"); + @memcpy(outfile_buf_u16[outfile_w.len..][0..suffix.len], suffix); + outfile_buf_u16[outfile_w.len + suffix.len] = 0; + break :brk outfile_buf_u16[0 .. outfile_w.len + suffix.len :0]; + } + outfile_buf_u16[outfile_w.len] = 0; + break :brk outfile_buf_u16[0..outfile_w.len :0]; + }; + + bun.C.moveOpenedFileAtLoose(fd, bun.toFD(root_dir.fd), outfile_slice, true).unwrap() catch |err| { + if (err == error.EISDIR) { + Output.errGeneric("{} is a directory. Please choose a different --outfile or delete the directory", .{std.unicode.fmtUtf16le(outfile_slice)}); + } else { + Output.err(err, "failed to move executable to result path", .{}); + } + + _ = bun.C.deleteOpenedFile(fd); + + Global.exit(1); + }; + return; + } + var buf: [bun.MAX_PATH_BYTES]u8 = undefined; const temp_location = bun.getFdPath(fd, &buf) catch |err| { Output.prettyErrorln("error: failed to get path for fd: {s}", .{@errorName(err)}); @@ -449,11 +518,6 @@ pub const StandaloneModuleGraph = struct { } } - if (comptime Environment.isWindows) { - Output.prettyError("TODO: windows support. sorry!!\n", .{}); - Global.exit(1); - } - bun.C.moveFileZWithHandle( fd, bun.toFD(std.fs.cwd().fd), @@ -475,7 +539,7 @@ pub const StandaloneModuleGraph = struct { } pub fn fromExecutable(allocator: std.mem.Allocator) !?StandaloneModuleGraph { - const self_exe = (openSelfExe(.{}) catch null) orelse return null; + const self_exe = bun.toLibUVOwnedFD(openSelf() catch return null); defer _ = Syscall.close(self_exe); var trailer_bytes: [4096]u8 = undefined; @@ -569,75 +633,103 @@ pub const StandaloneModuleGraph = struct { return try StandaloneModuleGraph.fromBytes(allocator, to_read, offsets); } - // this is based on the Zig standard library function, except it accounts for - fn openSelfExe(flags: std.fs.File.OpenFlags) std.fs.OpenSelfExeError!?bun.FileDescriptor { - // heuristic: `bun build --compile` won't be supported if the name is "bun" or "bunx". - // this is a cheap way to avoid the extra overhead of opening the executable - // and also just makes sense. - const argv = bun.argv(); - if (argv.len > 0) { - // const argv0_len = bun.len(argv[0]); - const argv0 = argv[0]; - if (argv0.len > 0) { - if (argv0.len == 3) { - if (bun.strings.eqlComptimeIgnoreLen(argv0, "bun")) { - return null; - } - } + const exe_suffix = if (Environment.isWindows) ".exe" else ""; - if (comptime Environment.isDebug) { - if (bun.strings.eqlComptime(argv0, "bun-debug")) { - return null; - } - } + fn isBuiltInExe(argv0: []const u8) bool { + if (argv0.len == 0) return false; - if (argv0.len == 4) { - if (bun.strings.eqlComptimeIgnoreLen(argv0, "bunx")) { - return null; - } - } + if (argv0.len == 3) { + if (bun.strings.eqlComptimeIgnoreLen(argv0, "bun" ++ exe_suffix)) { + return true; + } + } - if (comptime Environment.isDebug) { - if (bun.strings.eqlComptime(argv0, "bun-debugx")) { - return null; - } + if (argv0.len == 4) { + if (bun.strings.eqlComptimeIgnoreLen(argv0, "bunx" ++ exe_suffix)) { + return true; + } + + if (bun.strings.eqlComptimeIgnoreLen(argv0, "node" ++ exe_suffix)) { + return true; + } + } + + if (comptime Environment.isDebug) { + if (bun.strings.eqlComptime(argv0, "bun-debug")) { + return true; + } + if (bun.strings.eqlComptime(argv0, "bun-debugx")) { + return true; + } + } + + return false; + } + + fn openSelf() std.fs.OpenSelfExeError!bun.FileDescriptor { + // heuristic: `bun build --compile` won't be supported if the name is "bun", "bunx", or "node". + // this is a cheap way to avoid the extra overhead + // of opening the executable and also just makes sense. + if (!Environment.isWindows) { + const argv = bun.argv(); + if (argv.len > 0) { + if (isBuiltInExe(argv[0])) { + return error.FileNotFound; } } } - if (comptime Environment.isLinux) { - if (std.fs.openFileAbsoluteZ("/proc/self/exe", flags)) |easymode| { - return bun.toFD(easymode.handle); - } else |_| { - if (bun.argv().len > 0) { - // The user doesn't have /proc/ mounted, so now we just guess and hope for the best. - var whichbuf: [bun.MAX_PATH_BYTES]u8 = undefined; - if (bun.which( - &whichbuf, - bun.getenvZ("PATH") orelse return error.FileNotFound, - "", - bun.argv()[0], - )) |path| { - return bun.toFD((try std.fs.cwd().openFileZ(path, flags)).handle); + switch (Environment.os) { + .linux => { + if (std.fs.openFileAbsoluteZ("/proc/self/exe", .{})) |easymode| { + return bun.toFD(easymode.handle); + } else |_| { + if (bun.argv().len > 0) { + // The user doesn't have /proc/ mounted, so now we just guess and hope for the best. + var whichbuf: [bun.MAX_PATH_BYTES]u8 = undefined; + if (bun.which( + &whichbuf, + bun.getenvZ("PATH") orelse return error.FileNotFound, + "", + bun.argv()[0], + )) |path| { + return bun.toFD((try std.fs.cwd().openFileZ(path, .{})).handle); + } } + + return error.FileNotFound; } + }, + .mac => { + // Use of MAX_PATH_BYTES here is valid as the resulting path is immediately + // opened with no modification. + var buf: [bun.MAX_PATH_BYTES]u8 = undefined; + const self_exe_path = try std.fs.selfExePath(&buf); + buf[self_exe_path.len] = 0; + const file = try std.fs.openFileAbsoluteZ(buf[0..self_exe_path.len :0].ptr, .{}); + return bun.toFD(file.handle); + }, + .windows => { + const image_path_unicode_string = std.os.windows.peb().ProcessParameters.ImagePathName; + const image_path = image_path_unicode_string.Buffer[0 .. image_path_unicode_string.Length / 2]; - return error.FileNotFound; - } - } + var nt_path_buf: bun.WPathBuffer = undefined; + const nt_path = bun.strings.addNTPathPrefix(&nt_path_buf, image_path); - if (comptime Environment.isWindows) { - return bun.toFD((try std.fs.openSelfExe(flags)).handle); + return bun.sys.ntCreateFile( + bun.invalid_fd, + nt_path, + // access_mask + w.SYNCHRONIZE | w.GENERIC_READ, + // create disposition + w.FILE_OPEN, + // create options + w.FILE_SYNCHRONOUS_IO_NONALERT | w.FILE_OPEN_REPARSE_POINT, + ).unwrap() catch { + return error.FileNotFound; + }; + }, + else => @compileError("TODO"), } - // Use of MAX_PATH_BYTES here is valid as the resulting path is immediately - // opened with no modification. - var buf: [bun.MAX_PATH_BYTES]u8 = undefined; - const self_exe_path = try std.fs.selfExePath(&buf); - buf[self_exe_path.len] = 0; - const file = try std.fs.openFileAbsoluteZ(buf[0..self_exe_path.len :0].ptr, flags); - return @enumFromInt(file.handle); } }; - -const Output = bun.Output; -const Global = bun.Global; diff --git a/src/bun.js/node/types.zig b/src/bun.js/node/types.zig index 65a4030bb4..e829fc396d 100644 --- a/src/bun.js/node/types.zig +++ b/src/bun.js/node/types.zig @@ -146,7 +146,7 @@ pub fn Maybe(comptime ResultType: type) type { else => |err| @This(){ // always truncate .err = .{ - .errno = @truncate(@intFromEnum(err)), + .errno = translateToErrInt(err), .syscall = syscall, }, }, @@ -157,7 +157,7 @@ pub fn Maybe(comptime ResultType: type) type { return @This(){ // always truncate .err = .{ - .errno = @truncate(@intFromEnum(err)), + .errno = translateToErrInt(err), .syscall = syscall, }, }; @@ -169,7 +169,7 @@ pub fn Maybe(comptime ResultType: type) type { else => |err| @This(){ // always truncate .err = .{ - .errno = @truncate(@intFromEnum(err)), + .errno = translateToErrInt(err), .syscall = syscall, .fd = fd, }, @@ -186,7 +186,7 @@ pub fn Maybe(comptime ResultType: type) type { else => |err| @This(){ // always truncate .err = .{ - .errno = @truncate(@intFromEnum(err)), + .errno = translateToErrInt(err), .syscall = syscall, .path = bun.asByteSlice(path), }, @@ -196,6 +196,14 @@ 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)), + }; +} + pub const BlobOrStringOrBuffer = union(enum) { blob: JSC.WebCore.Blob, string_or_buffer: StringOrBuffer, diff --git a/src/bun.zig b/src/bun.zig index 50743be682..1824aad117 100644 --- a/src/bun.zig +++ b/src/bun.zig @@ -102,6 +102,10 @@ pub const FileDescriptor = enum(FileDescriptorInt) { pub fn format(fd: FileDescriptor, comptime fmt_: string, options_: std.fmt.FormatOptions, writer: anytype) !void { try FDImpl.format(FDImpl.decode(fd), fmt_, options_, writer); } + + pub fn assertKind(fd: FileDescriptor, kind: FDImpl.Kind) void { + std.debug.assert(FDImpl.decode(fd).kind == kind); + } }; pub const FDImpl = @import("./fd.zig").FDImpl; @@ -1091,9 +1095,7 @@ pub fn getFdPath(fd_: anytype, buf: *[@This().MAX_PATH_BYTES]u8) ![]u8 { const fd = toFD(fd_).cast(); if (comptime Environment.isWindows) { - var temp: [MAX_PATH_BYTES]u8 = undefined; - const temp_slice = try std.os.getFdPath(fd, &temp); - return path.normalizeBuf(temp_slice, buf, .loose); + return std.os.getFdPath(fd, buf); } if (comptime Environment.allow_assert) { @@ -1650,7 +1652,7 @@ pub const Generation = u16; pub const zstd = @import("./deps/zstd.zig"); pub const StringPointer = Schema.Api.StringPointer; -pub const StandaloneModuleGraph = @import("./standalone_bun.zig").StandaloneModuleGraph; +pub const StandaloneModuleGraph = @import("./StandaloneModuleGraph.zig").StandaloneModuleGraph; pub const String = @import("./string.zig").String; pub const SliceWithUnderlyingString = @import("./string.zig").SliceWithUnderlyingString; diff --git a/src/cli.zig b/src/cli.zig index ecb1cbed3a..3061942c3f 100644 --- a/src/cli.zig +++ b/src/cli.zig @@ -1279,31 +1279,28 @@ pub const Command = struct { } } - // there's a bug with openSelfExe() on Windows - if (comptime !bun.Environment.isWindows) { - // bun build --compile entry point - if (try bun.StandaloneModuleGraph.fromExecutable(bun.default_allocator)) |graph| { - var ctx = Command.Context{ - .args = std.mem.zeroes(Api.TransformOptions), - .log = log, - .start_time = start_time, - .allocator = bun.default_allocator, - }; + // bun build --compile entry point + if (try bun.StandaloneModuleGraph.fromExecutable(bun.default_allocator)) |graph| { + var ctx = Command.Context{ + .args = std.mem.zeroes(Api.TransformOptions), + .log = log, + .start_time = start_time, + .allocator = bun.default_allocator, + }; - ctx.args.target = Api.Target.bun; - if (bun.argv().len > 1) { - ctx.passthrough = bun.argv()[1..]; - } else { - ctx.passthrough = &[_]string{}; - } - - try @import("./bun_js.zig").Run.bootStandalone( - ctx, - graph.entryPoint().name, - graph, - ); - return; + ctx.args.target = Api.Target.bun; + if (bun.argv().len > 1) { + ctx.passthrough = bun.argv()[1..]; + } else { + ctx.passthrough = &[_]string{}; } + + try @import("./bun_js.zig").Run.bootStandalone( + ctx, + graph.entryPoint().name, + graph, + ); + return; } const tag = which(); diff --git a/src/cli/build_command.zig b/src/cli/build_command.zig index 6d380fa56b..27d1be6d61 100644 --- a/src/cli/build_command.zig +++ b/src/cli/build_command.zig @@ -350,6 +350,7 @@ pub const BuildCommand = struct { ); Output.flush(); + try bun.StandaloneModuleGraph.toExecutable( allocator, output_files, @@ -371,8 +372,9 @@ pub const BuildCommand = struct { Output.printElapsedStdoutTrim(@as(f64, @floatFromInt(compiled_elapsed))); - Output.prettyln(" compile {s}", .{ + Output.prettyln(" compile {s}{s}", .{ outfile, + if (Environment.isWindows and !strings.hasSuffixComptime(outfile, ".exe")) ".exe" else "", }); break :dump; diff --git a/src/fd.zig b/src/fd.zig index ed8ca31613..34495b8c4d 100644 --- a/src/fd.zig +++ b/src/fd.zig @@ -76,12 +76,12 @@ pub const FDImpl = packed struct { else => System, }; - const Value = if (env.os == .windows) + pub const Value = if (env.os == .windows) packed union { as_system: SystemAsInt, as_uv: UV } else packed union { as_system: SystemAsInt }; - const Kind = if (env.os == .windows) + pub const Kind = if (env.os == .windows) enum(u1) { system = 0, uv = 1 } else enum(u0) { system }; @@ -284,14 +284,41 @@ pub const FDImpl = packed struct { } pub fn format(this: FDImpl, comptime _: []const u8, _: std.fmt.FormatOptions, writer: anytype) !void { + if (!this.isValid()) { + try writer.writeAll("[invalid_fd]"); + return; + } switch (env.os) { else => { try writer.print("{d}", .{this.system()}); }, .windows => { switch (this.kind) { - .system => try writer.print("{d}[handle]", .{this.value.as_system}), - .uv => try writer.print("{d}[libuv]", .{this.value.as_system}), + .system => { + if (env.isDebug) { + const peb = std.os.windows.peb(); + const handle = this.system(); + if (handle == peb.ProcessParameters.hStdInput) { + return try writer.print("{d}[stdin handle]", .{this.value.as_system}); + } else if (handle == peb.ProcessParameters.hStdOutput) { + return try writer.print("{d}[stdout handle]", .{this.value.as_system}); + } else if (handle == peb.ProcessParameters.hStdError) { + return try writer.print("{d}[stderr handle]", .{this.value.as_system}); + } else if (handle == peb.ProcessParameters.CurrentDirectory.Handle) { + return try writer.print("{d}[cwd handle]", .{this.value.as_system}); + } else print_with_path: { + var fd_path: bun.WPathBuffer = undefined; + const path = std.os.windows.GetFinalPathNameByHandle(handle, .{ .volume_name = .Dos }, &fd_path) catch break :print_with_path; + return try writer.print("{d}[{}]", .{ + this.value.as_system, + std.unicode.fmtUtf16le(path), + }); + } + } + + try writer.print("{d}[handle]", .{this.value.as_system}); + }, + .uv => try writer.print("{d}[libuv]", .{this.value.as_uv}), } }, } diff --git a/src/main.zig b/src/main.zig index 8f42868a7f..349332e08b 100644 --- a/src/main.zig +++ b/src/main.zig @@ -21,6 +21,9 @@ extern fn bun_warn_avx_missing(url: [*:0]const u8) void; pub extern "C" var _environ: ?*anyopaque; pub extern "C" var environ: ?*anyopaque; +// TODO: when https://github.com/ziglang/zig/pull/18692 merges, use std.os.windows for this +extern fn SetConsoleMode(console_handle: *anyopaque, mode: u32) u32; + pub fn main() void { const bun = @import("root").bun; const Output = bun.Output; @@ -32,6 +35,7 @@ pub fn main() void { if (Environment.isRelease and Environment.isPosix) CrashReporter.start() catch unreachable; + if (Environment.isWindows) { environ = @ptrCast(std.os.environ.ptr); _environ = @ptrCast(std.os.environ.ptr); @@ -46,10 +50,12 @@ pub fn main() void { // https://learn.microsoft.com/en-us/windows/console/setconsoleoutputcp const CP_UTF8 = 65001; _ = w.kernel32.SetConsoleOutputCP(CP_UTF8); - // var mode: w.DWORD = undefined; - // if (w.kernel32.GetConsoleMode(bun.win32.STDOUT_FD)) { - // _ = w.kernel32.SetConsoleMode(bun.win32.STDOUT_FD, mode | std.os.windows.ENABLE_VIRTUAL_TERMINAL_PROCESSING); - // } + + var mode: w.DWORD = undefined; + const stdoutHandle = w.peb().ProcessParameters.hStdOutput; + if (w.kernel32.GetConsoleMode(stdoutHandle, &mode) != 0) { + _ = SetConsoleMode(stdoutHandle, mode | w.ENABLE_VIRTUAL_TERMINAL_PROCESSING); + } } bun.start_time = std.time.nanoTimestamp(); diff --git a/src/string_immutable.zig b/src/string_immutable.zig index 5bc0efd5d9..7b7e8bf4cf 100644 --- a/src/string_immutable.zig +++ b/src/string_immutable.zig @@ -832,6 +832,10 @@ 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 hasSuffixComptime(self: string, comptime alt: anytype) bool { return self.len >= alt.len and eqlComptimeCheckLenWithType(u8, self[self.len - alt.len ..], alt, false); } @@ -1626,10 +1630,17 @@ pub fn toNTPath(wbuf: []u16, utf8: []const u8) [:0]const u16 { return toWPathNormalized(wbuf, utf8); } - wbuf[0..4].* = [_]u16{ '\\', '?', '?', '\\' }; + wbuf[0..4].* = bun.windows.nt_object_prefix; return wbuf[0 .. toWPathNormalized(wbuf[4..], utf8).len + 4 :0]; } +pub fn addNTPathPrefix(wbuf: []u16, utf16: []const u16) [:0]const u16 { + wbuf[0..bun.windows.nt_object_prefix.len].* = bun.windows.nt_object_prefix; + @memcpy(wbuf[bun.windows.nt_object_prefix.len..][0..utf16.len], utf16); + wbuf[utf16.len + bun.windows.nt_object_prefix.len] = 0; + return wbuf[0 .. utf16.len + bun.windows.nt_object_prefix.len :0]; +} + // These are the same because they don't have rules like needing a trailing slash pub const toNTDir = toNTPath; diff --git a/src/sys.zig b/src/sys.zig index 3c1384b8d9..f978375452 100644 --- a/src/sys.zig +++ b/src/sys.zig @@ -413,7 +413,7 @@ fn normalizePathWindows( pub fn openDirAtWindows( dirFd: bun.FileDescriptor, - path: [:0]const u16, + path: []const u16, iterable: bool, no_follow: bool, ) Maybe(bun.FileDescriptor) { @@ -429,7 +429,7 @@ pub fn openDirAtWindows( }; var attr = w.OBJECT_ATTRIBUTES{ .Length = @sizeOf(w.OBJECT_ATTRIBUTES), - .RootDirectory = if (std.fs.path.isAbsoluteWindowsW(path)) + .RootDirectory = if (std.fs.path.isAbsoluteWindowsWTF16(path)) null else if (dirFd == bun.invalid_fd) std.fs.cwd().fd @@ -541,6 +541,21 @@ pub fn openatWindows(dir: bun.FileDescriptor, path: []const u16, flags: bun.Mode return ntCreateFile(dir, path, access_mask, creation, options); } +/// For this function to open an absolute path, it must start with "\??\". Otherwise +/// you need a reference file descriptor the "invalid_fd" file descriptor is used +/// to signify that the current working directory should be used. +/// +/// When using this function I highly recommend reading this first: +/// https://learn.microsoft.com/en-us/windows/win32/api/winternl/nf-winternl-ntcreatefile +/// +/// It is very very very easy to mess up flags here. Please review existing +/// examples to this call and the above function that maps unix flags to +/// the windows ones. +/// +/// It is very easy to waste HOURS on the subtle semantics of this function. +/// +/// In the zig standard library, messing up the input to their equivalent +/// will trigger `unreachable`. Here there will be a debug log with the path. pub fn ntCreateFile( dir: bun.FileDescriptor, path_maybe_leading_dot: []const u16, @@ -550,7 +565,10 @@ pub fn ntCreateFile( ) Maybe(bun.FileDescriptor) { var result: windows.HANDLE = undefined; + // Another problem re: normalization is that you can use relative paths, but no leading '.\' or './'' + // 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, "./")); const path_len_bytes = std.math.cast(u16, path.len * 2) orelse return .{ .err = .{ @@ -565,14 +583,21 @@ pub fn ntCreateFile( }; var attr = windows.OBJECT_ATTRIBUTES{ .Length = @sizeOf(windows.OBJECT_ATTRIBUTES), - .RootDirectory = if (std.fs.path.isAbsoluteWindowsWTF16(path)) + // From the Windows Documentation: + // + // [ObjectName] must be a fully qualified file specification or the name of a device object, + // unless it is the name of a file relative to the directory specified by RootDirectory. + // For example, \Device\Floppy1\myfile.dat or \??\B:\myfile.dat could be the fully qualified + // 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)) null else if (dir == bun.invalid_fd) std.fs.cwd().fd else dir.cast(), .Attributes = 0, // Note we do not use OBJ_CASE_INSENSITIVE here. - .ObjectName = &nt_name, .SecurityDescriptor = null, .SecurityQualityOfService = null, }; @@ -594,7 +619,19 @@ pub fn ntCreateFile( ); if (comptime Environment.allow_assert) { - log("NtCreateFile({d}, {}) = {s} (file) = {d}", .{ dir, bun.fmt.fmtUTF16(path), @tagName(rc), @intFromPtr(result) }); + if (rc == .INVALID_PARAMETER) { + // Double check what flags you are passing to this + // + // - access_mask probably needs w.SYNCHRONIZE, + // - options probably needs w.FILE_SYNCHRONOUS_IO_NONALERT + // - disposition probably needs w.FILE_OPEN + bun.Output.debugWarn("NtCreateFile({d}, {}) = {s} (file) = {d}\nYou are calling this function with the wrong flags!!!", .{ dir, bun.fmt.fmtUTF16(path), @tagName(rc), @intFromPtr(result) }); + } else if (rc == .OBJECT_PATH_SYNTAX_BAD or rc == .OBJECT_NAME_INVALID) { + // See above comment. For absolute paths you must have \??\ at the start. + bun.Output.debugWarn("NtCreateFile({d}, {}) = {s} (file) = {d}\nYou are calling this function without normalizing the path correctly!!!", .{ dir, bun.fmt.fmtUTF16(path), @tagName(rc), @intFromPtr(result) }); + } else { + log("NtCreateFile({d}, {}) = {s} (file) = {d}", .{ dir, bun.fmt.fmtUTF16(path), @tagName(rc), @intFromPtr(result) }); + } } switch (windows.Win32Error.fromNTStatus(rc)) { @@ -1767,6 +1804,18 @@ pub fn setFileOffset(fd: bun.FileDescriptor, offset: usize) Maybe(void) { } } +pub fn setFileOffsetToEndWindows(fd: bun.FileDescriptor) Maybe(usize) { + if (comptime Environment.isWindows) { + var new_ptr: std.os.windows.LARGE_INTEGER = undefined; + const rc = kernel32.SetFilePointerEx(fd.cast(), 0, &new_ptr, windows.FILE_END); + if (rc == windows.FALSE) { + return Maybe(usize).errnoSys(0, .lseek) orelse Maybe(usize){ .result = 0 }; + } + return Maybe(usize){ .result = @intCast(new_ptr) }; + } + @compileError("Not Implemented"); +} + pub fn pipe() Maybe([2]bun.FileDescriptor) { if (comptime Environment.isWindows) { @panic("TODO: Implement `pipe()` for Windows"); diff --git a/src/windows.zig b/src/windows.zig index b830321186..c74afad7c0 100644 --- a/src/windows.zig +++ b/src/windows.zig @@ -17,6 +17,7 @@ pub const FALSE = windows.FALSE; pub const TRUE = windows.TRUE; pub const INVALID_HANDLE_VALUE = windows.INVALID_HANDLE_VALUE; pub const FILE_BEGIN = windows.FILE_BEGIN; +pub const FILE_END = windows.FILE_END; pub const FILE_CURRENT = windows.FILE_CURRENT; pub const ULONG = windows.ULONG; pub const LARGE_INTEGER = windows.LARGE_INTEGER; @@ -62,6 +63,8 @@ pub const advapi32 = windows.advapi32; pub const INVALID_FILE_ATTRIBUTES: u32 = std.math.maxInt(u32); +pub const nt_object_prefix = [4]u16{ '\\', '?', '?', '\\' }; + const std = @import("std"); pub const HANDLE = win32.HANDLE; @@ -2999,7 +3002,27 @@ pub fn translateWinErrorToErrno(err: win32.Win32Error) bun.C.E { else => |t| { // if (bun.Environment.isDebug) { - bun.Output.warn("Called getLastErrno with {s} which does not have a mapping to errno.", .{@tagName(t)}); + bun.Output.warn("Called translateWinErrorToErrno with {s} which does not have a mapping to errno.", .{@tagName(t)}); + // } + return .UNKNOWN; + }, + }; +} + +pub fn translateNTStatusToErrno(err: win32.NTSTATUS) bun.C.E { + return switch (err) { + .SUCCESS => .SUCCESS, + .ACCESS_DENIED => .PERM, + .INVALID_HANDLE => .BADF, + .INVALID_PARAMETER => .INVAL, + .OBJECT_NAME_COLLISION => .EXIST, + .FILE_IS_A_DIRECTORY => .ISDIR, + .OBJECT_PATH_NOT_FOUND => .NOENT, + .OBJECT_NAME_NOT_FOUND => .NOENT, + + else => |t| { + // if (bun.Environment.isDebug) { + bun.Output.warn("Called translateNTStatusToErrno with {s} which does not have a mapping to errno.", .{@tagName(t)}); // } return .UNKNOWN; }, diff --git a/src/windows_c.zig b/src/windows_c.zig index 96eeec53e7..c2c1b05287 100644 --- a/src/windows_c.zig +++ b/src/windows_c.zig @@ -1281,7 +1281,23 @@ pub fn renameAtW( }; defer _ = bun.sys.close(src_fd); - var rc: w.NTSTATUS = undefined; + return moveOpenedFileAt(src_fd, new_dir_fd, new_path_w, replace_if_exists); +} + +const log = bun.Output.scoped(.SYS, false); + +/// With an open file source_fd, move it into the directory new_dir_fd with the name new_path_w. +/// Does not close the file descriptor. +/// +/// For this to succeed +/// - source_fd must have been opened with access_mask=w.DELETE +/// - new_path_w must be the name of a file. it cannot be a path relative to new_dir_fd. see moveOpenedFileAtLoose +pub fn moveOpenedFileAt( + src_fd: bun.FileDescriptor, + new_dir_fd: bun.FileDescriptor, + new_file_name: []const u16, + replace_if_exists: bool, +) Maybe(void) { // FILE_RENAME_INFORMATION_EX and FILE_RENAME_POSIX_SEMANTICS require >= win10_rs1, // but FILE_RENAME_IGNORE_READONLY_ATTRIBUTE requires >= win10_rs5. We check >= rs5 here // so that we only use POSIX_SEMANTICS when we know IGNORE_READONLY_ATTRIBUTE will also be @@ -1292,7 +1308,8 @@ pub fn renameAtW( 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; - const struct_len = @sizeOf(w.FILE_RENAME_INFORMATION_EX) - 1 + new_path_w.len * 2; + + const struct_len = @sizeOf(w.FILE_RENAME_INFORMATION_EX) - 1 + new_file_name.len * 2; if (struct_len > struct_buf_len) return Maybe(void).errno(bun.C.E.NAMETOOLONG, .NtSetInformationFile); const rename_info = @as(*w.FILE_RENAME_INFORMATION_EX, @ptrCast(&rename_info_buf)); @@ -1302,18 +1319,93 @@ pub fn renameAtW( if (replace_if_exists) flags |= w.FILE_RENAME_REPLACE_IF_EXISTS; rename_info.* = .{ .Flags = flags, - .RootDirectory = if (std.fs.path.isAbsoluteWindowsWTF16(new_path_w)) null else new_dir_fd.cast(), - .FileNameLength = @intCast(new_path_w.len * 2), // already checked error.NameTooLong + .RootDirectory = if (std.fs.path.isAbsoluteWindowsWTF16(new_file_name)) null else new_dir_fd.cast(), + .FileNameLength = @intCast(new_file_name.len * 2), // already checked error.NameTooLong .FileName = undefined, }; - @memcpy(@as([*]u16, &rename_info.FileName)[0..new_path_w.len], new_path_w); - rc = w.ntdll.NtSetInformationFile( + @memcpy(@as([*]u16, &rename_info.FileName)[0..new_file_name.len], new_file_name); + const rc = w.ntdll.NtSetInformationFile( src_fd.cast(), &io_status_block, rename_info, @intCast(struct_len), // already checked for error.NameTooLong .FileRenameInformationEx, ); + log("moveOpenedFileAt({} ->> {} '{}', {s}) = {s}", .{ src_fd, new_dir_fd, std.unicode.fmtUtf16le(new_file_name), if (replace_if_exists) "replace_if_exists" else "no flag", @tagName(rc) }); + + if (bun.Environment.isDebug) { + if (rc == .ACCESS_DENIED) { + bun.Output.debugWarn("moveOpenedFileAt was called on a file descriptor without access_mask=w.DELETE", .{}); + } + } + + return if (rc == .SUCCESS) + Maybe(void).success + else + Maybe(void).errno(rc, .NtSetInformationFile); +} + +/// Same as moveOpenedFileAt but allows new_path to be a path relative to new_dir_fd. +/// +/// Aka: moveOpenedFileAtLoose(fd, dir, ".\\a\\relative\\not-normalized-path.txt", false); +pub fn moveOpenedFileAtLoose( + src_fd: bun.FileDescriptor, + new_dir_fd: bun.FileDescriptor, + new_path: []const u16, + replace_if_exists: bool, +) Maybe(void) { + std.debug.assert(std.mem.indexOfScalar(u16, new_path, '/') == null); // Call bun.strings.toWPathNormalized first + + const without_leading_dot_slash = if (new_path.len >= 2 and new_path[0] == '.' and new_path[1] == '\\') + new_path[2..] + else + new_path; + + if (std.mem.lastIndexOfScalar(u16, new_path, '\\')) |last_slash| { + const dirname = new_path[0..last_slash]; + const fd = switch (bun.sys.openDirAtWindows(new_dir_fd, dirname, false, true)) { + .err => |e| return .{ .err = e }, + .result => |fd| fd, + }; + defer _ = bun.sys.close(fd); + + const basename = new_path[last_slash + 1 ..]; + return moveOpenedFileAt(src_fd, fd, basename, replace_if_exists); + } + + // easy mode + return moveOpenedFileAt(src_fd, new_dir_fd, without_leading_dot_slash, replace_if_exists); +} + +const FILE_DISPOSITION_DO_NOT_DELETE: w.ULONG = 0x00000000; +const FILE_DISPOSITION_DELETE: w.ULONG = 0x00000001; +const FILE_DISPOSITION_POSIX_SEMANTICS: w.ULONG = 0x00000002; +const FILE_DISPOSITION_FORCE_IMAGE_SECTION_CHECK: w.ULONG = 0x00000004; +const FILE_DISPOSITION_ON_CLOSE: w.ULONG = 0x00000008; +const FILE_DISPOSITION_IGNORE_READONLY_ATTRIBUTE: w.ULONG = 0x00000010; + +/// Extracted from standard library except this takes an open file descriptor +/// +/// NOTE: THE FILE MUST BE OPENED WITH ACCESS_MASK "DELETE" OR THIS WILL FAIL +pub fn deleteOpenedFile(fd: bun.FileDescriptor) Maybe(void) { + comptime std.debug.assert(builtin.target.os.version_range.windows.min.isAtLeast(.win10_rs5)); + var info = w.FILE_DISPOSITION_INFORMATION_EX{ + .Flags = FILE_DISPOSITION_DELETE | + FILE_DISPOSITION_POSIX_SEMANTICS | + FILE_DISPOSITION_IGNORE_READONLY_ATTRIBUTE, + }; + + var io: w.IO_STATUS_BLOCK = undefined; + const rc = w.ntdll.NtSetInformationFile( + fd.cast(), + &io, + &info, + @sizeOf(w.FILE_DISPOSITION_INFORMATION_EX), + .FileDispositionInformationEx, + ); + + log("deleteOpenedFile({}) = {s}", .{ fd, @tagName(rc) }); + return if (rc == .SUCCESS) Maybe(void).success else diff --git a/test/bundler/cli.test.ts b/test/bundler/cli.test.ts index 2fea1a070c..8573a28f21 100644 --- a/test/bundler/cli.test.ts +++ b/test/bundler/cli.test.ts @@ -1,4 +1,3 @@ -// @known-failing-on-windows: 1 failing import { bunEnv, bunExe } from "harness"; import { describe, expect, test } from "bun:test"; import fs from "node:fs";