From aafddc2ea13e40a8262d9378aeca2e097a37ac03 Mon Sep 17 00:00:00 2001 From: Andrew Kelley Date: Fri, 5 Dec 2025 19:08:37 -0800 Subject: update all occurrences of close() to close(io) --- lib/std/process.zig | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) (limited to 'lib/std/process.zig') diff --git a/lib/std/process.zig b/lib/std/process.zig index a0a26c766f..a8dede6ad4 100644 --- a/lib/std/process.zig +++ b/lib/std/process.zig @@ -1,12 +1,14 @@ -const std = @import("std.zig"); const builtin = @import("builtin"); +const native_os = builtin.os.tag; + +const std = @import("std.zig"); +const Io = std.Io; const fs = std.fs; const mem = std.mem; const math = std.math; -const Allocator = mem.Allocator; +const Allocator = std.mem.Allocator; const assert = std.debug.assert; const testing = std.testing; -const native_os = builtin.os.tag; const posix = std.posix; const windows = std.os.windows; const unicode = std.unicode; @@ -1571,9 +1573,9 @@ pub fn getUserInfo(name: []const u8) !UserInfo { /// TODO this reads /etc/passwd. But sometimes the user/id mapping is in something else /// like NIS, AD, etc. See `man nss` or look at an strace for `id myuser`. -pub fn posixGetUserInfo(name: []const u8) !UserInfo { +pub fn posixGetUserInfo(io: Io, name: []const u8) !UserInfo { const file = try std.fs.openFileAbsolute("/etc/passwd", .{}); - defer file.close(); + defer file.close(io); var buffer: [4096]u8 = undefined; var file_reader = file.reader(&buffer); return posixGetUserInfoPasswdStream(name, &file_reader.interface) catch |err| switch (err) { -- cgit v1.2.3 From 877032ec6a0007316f42658d12042f1473de4856 Mon Sep 17 00:00:00 2001 From: Andrew Kelley Date: Sat, 6 Dec 2025 20:43:57 -0800 Subject: std: reorganize realpath and process executable APIs --- lib/std/Io.zig | 4 +- lib/std/Io/Dir.zig | 39 +++++-- lib/std/Io/File.zig | 6 - lib/std/Io/Kqueue.zig | 4 +- lib/std/Io/Threaded.zig | 152 +++++++++++++++++++++++- lib/std/debug/SelfInfo/Elf.zig | 2 +- lib/std/debug/SelfInfo/Windows.zig | 2 +- lib/std/fs.zig | 232 ------------------------------------- lib/std/fs/test.zig | 55 ++++++++- lib/std/posix/test.zig | 43 ------- lib/std/process.zig | 98 +++++++++++++++- lib/std/zig/system.zig | 11 +- 12 files changed, 333 insertions(+), 315 deletions(-) (limited to 'lib/std/process.zig') diff --git a/lib/std/Io.zig b/lib/std/Io.zig index f783718cef..17fb75fe54 100644 --- a/lib/std/Io.zig +++ b/lib/std/Io.zig @@ -697,7 +697,6 @@ pub const VTable = struct { fileReadPositional: *const fn (?*anyopaque, File, data: [][]u8, offset: u64) File.ReadPositionalError!usize, fileSeekBy: *const fn (?*anyopaque, File, relative_offset: i64) File.SeekError!void, fileSeekTo: *const fn (?*anyopaque, File, absolute_offset: u64) File.SeekError!void, - openSelfExe: *const fn (?*anyopaque, File.OpenFlags) File.OpenSelfExeError!File, fileSync: *const fn (?*anyopaque, File) File.SyncError!void, fileIsTty: *const fn (?*anyopaque, File) Cancelable!bool, fileEnableAnsiEscapeCodes: *const fn (?*anyopaque, File) File.EnableAnsiEscapeCodesError!void, @@ -712,6 +711,9 @@ pub const VTable = struct { fileUnlock: *const fn (?*anyopaque, File) void, fileDowngradeLock: *const fn (?*anyopaque, File) File.DowngradeLockError!void, + processExecutableOpen: *const fn (?*anyopaque, File.OpenFlags) std.process.OpenExecutableError!File, + processExecutablePath: *const fn (?*anyopaque, buffer: []u8) std.process.ExecutablePathError!usize, + now: *const fn (?*anyopaque, Clock) Clock.Error!Timestamp, sleep: *const fn (?*anyopaque, Timeout) SleepError!void, diff --git a/lib/std/Io/Dir.zig b/lib/std/Io/Dir.zig index 9ae636d4a6..71ea3b465a 100644 --- a/lib/std/Io/Dir.zig +++ b/lib/std/Io/Dir.zig @@ -694,24 +694,29 @@ pub const RealPathError = error{ /// supported hosts are: Linux, macOS, and Windows. /// /// See also: -/// * `realpathAlloc`. +/// * `realPathAlloc`. pub fn realPath(dir: Dir, io: Io, sub_path: []const u8, out_buffer: []u8) RealPathError!usize { return io.vtable.dirRealPath(io.userdata, dir, sub_path, out_buffer); } pub const RealPathAllocError = RealPathError || Allocator.Error; -/// Same as `Dir.realpath` except caller must free the returned memory. -/// See also `Dir.realpath`. -pub fn realpathAlloc(self: Dir, allocator: Allocator, pathname: []const u8) RealPathAllocError![]u8 { - // Use of max_path_bytes here is valid as the realpath function does not - // have a variant that takes an arbitrary-size buffer. - // TODO(#4812): Consider reimplementing realpath or using the POSIX.1-2008 - // NULL out parameter (GNU's canonicalize_file_name) to handle overelong - // paths. musl supports passing NULL but restricts the output to PATH_MAX - // anyway. - var buf: [std.fs.max_path_bytes]u8 = undefined; - return allocator.dupe(u8, try self.realpath(pathname, &buf)); +/// Same as `realPath` except allocates result. +pub fn realPathAlloc(dir: Dir, io: Io, sub_path: []const u8, allocator: Allocator) RealPathAllocError![:0]u8 { + var buffer: [std.fs.max_path_bytes]u8 = undefined; + const n = try realPath(dir, io, sub_path, &buffer); + return allocator.dupeZ(u8, buffer[0..n]); +} + +pub fn realPathAbsolute(io: Io, path: []const u8, out_buffer: []u8) RealPathError!usize { + return io.vtable.dirRealPath(io.userdata, .cwd(), path, out_buffer); +} + +/// Same as `realPathAbsolute` except allocates result. +pub fn realPathAbsoluteAlloc(io: Io, path: []const u8, allocator: Allocator) RealPathAllocError![:0]u8 { + var buffer: [std.fs.max_path_bytes]u8 = undefined; + const n = try realPathAbsolute(io, path, &buffer); + return allocator.dupeZ(u8, buffer[0..n]); } pub const DeleteFileError = error{ @@ -975,6 +980,16 @@ pub fn readLink(dir: Dir, io: Io, sub_path: []const u8, buffer: []u8) ReadLinkEr return io.vtable.dirReadLink(io.userdata, dir, sub_path, buffer); } +/// Same as `readLink`, except it asserts the path is absolute. +/// +/// On Windows, `path` should be encoded as [WTF-8](https://wtf-8.codeberg.page/). +/// On WASI, `path` should be encoded as valid UTF-8. +/// On other platforms, `path` is an opaque sequence of bytes with no particular encoding. +pub fn readLinkAbsolute(io: Io, path: []const u8, buffer: []u8) ReadLinkError!usize { + assert(std.fs.path.isAbsolute(path)); + return io.vtable.dirReadLink(io.userdata, .cwd(), path, buffer); +} + pub const ReadFileAllocError = File.OpenError || File.ReadError || Allocator.Error || error{ /// File size reached or exceeded the provided limit. StreamTooLong, diff --git a/lib/std/Io/File.zig b/lib/std/Io/File.zig index a0349498c5..b24c0b5100 100644 --- a/lib/std/Io/File.zig +++ b/lib/std/Io/File.zig @@ -464,12 +464,6 @@ pub fn setTimestampsNow(file: File, io: Io) SetTimestampsError!void { return io.vtable.fileSetTimestampsNow(io.userdata, file); } -pub const OpenSelfExeError = OpenError || std.fs.SelfExePathError || LockError; - -pub fn openSelfExe(io: Io, flags: OpenFlags) OpenSelfExeError!File { - return io.vtable.openSelfExe(io.userdata, flags); -} - pub const ReadPositionalError = Reader.Error || error{Unseekable}; pub fn readPositional(file: File, io: Io, buffer: [][]u8, offset: u64) ReadPositionalError!usize { diff --git a/lib/std/Io/Kqueue.zig b/lib/std/Io/Kqueue.zig index 5b4f71da08..e34c862ae9 100644 --- a/lib/std/Io/Kqueue.zig +++ b/lib/std/Io/Kqueue.zig @@ -888,7 +888,7 @@ pub fn io(k: *Kqueue) Io { .fileReadPositional = fileReadPositional, .fileSeekBy = fileSeekBy, .fileSeekTo = fileSeekTo, - .openSelfExe = openSelfExe, + .openExecutable = openExecutable, .now = now, .sleep = sleep, @@ -1246,7 +1246,7 @@ fn fileSeekTo(userdata: ?*anyopaque, file: File, absolute_offset: u64) File.Seek _ = absolute_offset; @panic("TODO"); } -fn openSelfExe(userdata: ?*anyopaque, file: File.OpenFlags) File.OpenSelfExeError!File { +fn openExecutable(userdata: ?*anyopaque, file: File.OpenFlags) File.OpenExecutableError!File { const k: *Kqueue = @ptrCast(@alignCast(userdata)); _ = k; _ = file; diff --git a/lib/std/Io/Threaded.zig b/lib/std/Io/Threaded.zig index 43424e348b..fb76002201 100644 --- a/lib/std/Io/Threaded.zig +++ b/lib/std/Io/Threaded.zig @@ -717,7 +717,6 @@ pub fn io(t: *Threaded) Io { .fileReadPositional = fileReadPositional, .fileSeekBy = fileSeekBy, .fileSeekTo = fileSeekTo, - .openSelfExe = openSelfExe, .fileSync = fileSync, .fileIsTty = fileIsTty, .fileEnableAnsiEscapeCodes = fileEnableAnsiEscapeCodes, @@ -732,6 +731,9 @@ pub fn io(t: *Threaded) Io { .fileUnlock = fileUnlock, .fileDowngradeLock = fileDowngradeLock, + .processExecutableOpen = processExecutableOpen, + .processExecutablePath = processExecutablePath, + .now = now, .sleep = sleep, @@ -839,7 +841,6 @@ pub fn ioBasic(t: *Threaded) Io { .fileReadPositional = fileReadPositional, .fileSeekBy = fileSeekBy, .fileSeekTo = fileSeekTo, - .openSelfExe = openSelfExe, .fileSync = fileSync, .fileIsTty = fileIsTty, .fileEnableAnsiEscapeCodes = fileEnableAnsiEscapeCodes, @@ -854,6 +855,9 @@ pub fn ioBasic(t: *Threaded) Io { .fileUnlock = fileUnlock, .fileDowngradeLock = fileDowngradeLock, + .processExecutableOpen = processExecutableOpen, + .processExecutablePath = processExecutablePath, + .now = now, .sleep = sleep, @@ -5932,7 +5936,7 @@ fn fileSeekTo(userdata: ?*anyopaque, file: File, offset: u64) File.SeekError!voi } } -fn openSelfExe(userdata: ?*anyopaque, flags: File.OpenFlags) File.OpenSelfExeError!File { +fn processExecutableOpen(userdata: ?*anyopaque, flags: File.OpenFlags) std.process.OpenExecutableError!File { const t: *Threaded = @ptrCast(@alignCast(userdata)); switch (native_os) { .linux, .serenity => return dirOpenFilePosix(t, .{ .handle = posix.AT.FDCWD }, "/proc/self/exe", flags), @@ -5945,7 +5949,147 @@ fn openSelfExe(userdata: ?*anyopaque, flags: File.OpenFlags) File.OpenSelfExeErr const prefixed_path_w = try windows.wToPrefixedFileW(null, image_path_name); return dirOpenFileWtf16(t, null, prefixed_path_w.span(), flags); }, - else => @panic("TODO implement openSelfExe"), + else => @panic("TODO implement processExecutableOpen"), + } +} + +fn processExecutablePath(userdata: ?*anyopaque, out_buffer: []u8) std.process.ExecutablePathError!usize { + const t: *Threaded = @ptrCast(@alignCast(userdata)); + const max_path_bytes = std.fs.max_path_bytes; + + switch (native_os) { + .driverkit, + .ios, + .maccatalyst, + .macos, + .tvos, + .visionos, + .watchos, + => { + // Note that _NSGetExecutablePath() will return "a path" to + // the executable not a "real path" to the executable. + var symlink_path_buf: [max_path_bytes:0]u8 = undefined; + var u32_len: u32 = max_path_bytes + 1; // include the sentinel + const rc = std.c._NSGetExecutablePath(&symlink_path_buf, &u32_len); + if (rc != 0) return error.NameTooLong; + + var real_path_buf: [max_path_bytes]u8 = undefined; + const real_path = Io.Dir.realPathAbsolute(ioBasic(t), &symlink_path_buf, &real_path_buf) catch |err| switch (err) { + error.NetworkNotFound => unreachable, // Windows-only + else => |e| return e, + }; + if (real_path.len > out_buffer.len) return error.NameTooLong; + const result = out_buffer[0..real_path.len]; + @memcpy(result, real_path); + return result.len; + }, + .linux, .serenity => return Io.Dir.readLinkAbsolute(ioBasic(t), "/proc/self/exe", out_buffer) catch |err| switch (err) { + error.UnsupportedReparsePointType => unreachable, // Windows-only + error.NetworkNotFound => unreachable, // Windows-only + else => |e| return e, + }, + .illumos => return Io.Dir.readLinkAbsolute(ioBasic(t), "/proc/self/path/a.out", out_buffer) catch |err| switch (err) { + error.UnsupportedReparsePointType => unreachable, // Windows-only + error.NetworkNotFound => unreachable, // Windows-only + else => |e| return e, + }, + .freebsd, .dragonfly => { + const current_thread = Thread.getCurrent(t); + try current_thread.checkCancel(); + var mib: [4]c_int = .{ posix.CTL.KERN, posix.KERN.PROC, posix.KERN.PROC_PATHNAME, -1 }; + var out_len: usize = out_buffer.len; + try posix.sysctl(&mib, out_buffer.ptr, &out_len, null, 0); + return out_len; + }, + .netbsd => { + const current_thread = Thread.getCurrent(t); + try current_thread.checkCancel(); + var mib = [4]c_int{ posix.CTL.KERN, posix.KERN.PROC_ARGS, -1, posix.KERN.PROC_PATHNAME }; + var out_len: usize = out_buffer.len; + try posix.sysctl(&mib, out_buffer.ptr, &out_len, null, 0); + return out_len; + }, + .openbsd, .haiku => { + // OpenBSD doesn't support getting the path of a running process, so try to guess it + if (std.os.argv.len == 0) + return error.FileNotFound; + + const argv0 = std.mem.span(std.os.argv[0]); + if (std.mem.indexOf(u8, argv0, "/") != null) { + // argv[0] is a path (relative or absolute): use realpath(3) directly + var real_path_buf: [max_path_bytes]u8 = undefined; + const real_path = Io.Dir.realPathAbsolute(ioBasic(t), std.os.argv[0], &real_path_buf) catch |err| switch (err) { + error.NetworkNotFound => unreachable, // Windows-only + else => |e| return e, + }; + if (real_path.len > out_buffer.len) + return error.NameTooLong; + const result = out_buffer[0..real_path.len]; + @memcpy(result, real_path); + return result.len; + } else if (argv0.len != 0) { + // argv[0] is not empty (and not a path): search it inside PATH + const PATH = posix.getenvZ("PATH") orelse return error.FileNotFound; + var path_it = std.mem.tokenizeScalar(u8, PATH, std.fs.path.delimiter); + while (path_it.next()) |a_path| { + var resolved_path_buf: [max_path_bytes - 1:0]u8 = undefined; + const resolved_path = std.fmt.bufPrintSentinel(&resolved_path_buf, "{s}/{s}", .{ + a_path, std.os.argv[0], + }, 0) catch continue; + + var real_path_buf: [max_path_bytes]u8 = undefined; + if (Io.Dir.realPathAbsolute(ioBasic(t), resolved_path, &real_path_buf)) |real_path| { + // found a file, and hope it is the right file + if (real_path.len > out_buffer.len) + return error.NameTooLong; + const result = out_buffer[0..real_path.len]; + @memcpy(result, real_path); + return result.len; + } else |_| continue; + } + } + return error.FileNotFound; + }, + .windows => { + const current_thread = Thread.getCurrent(t); + try current_thread.checkCancel(); + const w = windows; + const image_path_unicode_string = &w.peb().ProcessParameters.ImagePathName; + const image_path_name = image_path_unicode_string.Buffer.?[0 .. image_path_unicode_string.Length / 2 :0]; + + // If ImagePathName is a symlink, then it will contain the path of the + // symlink, not the path that the symlink points to. We want the path + // that the symlink points to, though, so we need to get the realpath. + var path_name_w = try w.wToPrefixedFileW(null, image_path_name); + + const access_mask = w.GENERIC_READ | w.SYNCHRONIZE; + const share_access = w.FILE_SHARE_READ | w.FILE_SHARE_WRITE | w.FILE_SHARE_DELETE; + const creation = w.FILE_OPEN; + const h_file = blk: { + const res = w.OpenFile(path_name_w.span(), .{ + .dir = null, + .access_mask = access_mask, + .share_access = share_access, + .creation = creation, + .filter = .any, + }) catch |err| switch (err) { + error.WouldBlock => unreachable, + else => |e| return e, + }; + break :blk res; + }; + defer w.CloseHandle(h_file); + + const wide_slice = w.GetFinalPathNameByHandle(h_file, .{}, out_buffer); + + const len = std.unicode.calcWtf8Len(wide_slice); + if (len > out_buffer.len) + return error.NameTooLong; + + const end_index = std.unicode.wtf16LeToWtf8(out_buffer, wide_slice); + return end_index; + }, + else => @compileError("unsupported OS"), } } diff --git a/lib/std/debug/SelfInfo/Elf.zig b/lib/std/debug/SelfInfo/Elf.zig index 124768687c..213389bf04 100644 --- a/lib/std/debug/SelfInfo/Elf.zig +++ b/lib/std/debug/SelfInfo/Elf.zig @@ -329,7 +329,7 @@ const Module = struct { defer file.close(io); break :res std.debug.ElfFile.load(gpa, file, mod.build_id, &.native(mod.name)); } else res: { - const path = std.fs.selfExePathAlloc(gpa) catch |err| switch (err) { + const path = std.process.executablePathAlloc(io, gpa) catch |err| switch (err) { error.OutOfMemory => |e| return e, else => return error.ReadFailed, }; diff --git a/lib/std/debug/SelfInfo/Windows.zig b/lib/std/debug/SelfInfo/Windows.zig index c7f9d8c352..f0ac30cca2 100644 --- a/lib/std/debug/SelfInfo/Windows.zig +++ b/lib/std/debug/SelfInfo/Windows.zig @@ -434,7 +434,7 @@ const Module = struct { const pdb_file_open_result = if (fs.path.isAbsolute(path)) res: { break :res std.fs.cwd().openFile(io, path, .{}); } else res: { - const self_dir = fs.selfExeDirPathAlloc(gpa) catch |err| switch (err) { + const self_dir = std.process.executableDirPathAlloc(io, gpa) catch |err| switch (err) { error.OutOfMemory, error.Unexpected => |e| return e, else => return error.ReadFailed, }; diff --git a/lib/std/fs.zig b/lib/std/fs.zig index cb4daf7c50..78c924f8d9 100644 --- a/lib/std/fs.zig +++ b/lib/std/fs.zig @@ -25,13 +25,6 @@ pub const File = std.Io.File; pub const path = @import("fs/path.zig"); pub const wasi = @import("fs/wasi.zig"); -// TODO audit these APIs with respect to Dir and absolute paths - -pub const realpath = posix.realpath; -pub const realpathZ = posix.realpathZ; -pub const realpathW = posix.realpathW; -pub const realpathW2 = posix.realpathW2; - pub const getAppDataDir = @import("fs/get_app_data_dir.zig").getAppDataDir; pub const GetAppDataDirError = @import("fs/get_app_data_dir.zig").GetAppDataDirError; @@ -241,15 +234,6 @@ pub fn deleteTreeAbsolute(io: Io, absolute_path: []const u8) !void { return dir.deleteTree(path.basename(absolute_path)); } -/// Same as `Dir.readLink`, except it asserts the path is absolute. -/// On Windows, `pathname` should be encoded as [WTF-8](https://wtf-8.codeberg.page/). -/// On WASI, `pathname` should be encoded as valid UTF-8. -/// On other platforms, `pathname` is an opaque sequence of bytes with no particular encoding. -pub fn readLinkAbsolute(pathname: []const u8, buffer: *[max_path_bytes]u8) ![]u8 { - assert(path.isAbsolute(pathname)); - return posix.readlink(pathname, buffer); -} - /// Creates a symbolic link named `sym_link_path` which contains the string `target_path`. /// A symbolic link (also known as a soft link) may point to an existing file or to a nonexistent /// one; the latter case is known as a dangling link. @@ -287,222 +271,6 @@ pub fn symLinkAbsoluteW( return windows.CreateSymbolicLink(null, mem.span(sym_link_path_w), mem.span(target_path_w), flags.is_directory); } -// This is `posix.ReadLinkError || posix.RealPathError` with impossible errors excluded -pub const SelfExePathError = error{ - FileNotFound, - AccessDenied, - NameTooLong, - NotSupported, - NotDir, - SymLinkLoop, - InputOutput, - FileTooBig, - IsDir, - ProcessFdQuotaExceeded, - SystemFdQuotaExceeded, - NoDevice, - SystemResources, - NoSpaceLeft, - FileSystem, - BadPathName, - DeviceBusy, - SharingViolation, - PipeBusy, - NotLink, - PathAlreadyExists, - - /// On Windows, `\\server` or `\\server\share` was not found. - NetworkNotFound, - ProcessNotFound, - - /// On Windows, antivirus software is enabled by default. It can be - /// disabled, but Windows Update sometimes ignores the user's preference - /// and re-enables it. When enabled, antivirus software on Windows - /// intercepts file system operations and makes them significantly slower - /// in addition to possibly failing with this error code. - AntivirusInterference, - - /// On Windows, the volume does not contain a recognized file system. File - /// system drivers might not be loaded, or the volume may be corrupt. - UnrecognizedVolume, - - Canceled, -} || posix.SysCtlError; - -/// `selfExePath` except allocates the result on the heap. -/// Caller owns returned memory. -pub fn selfExePathAlloc(allocator: Allocator) ![]u8 { - // Use of max_path_bytes here is justified as, at least on one tested Linux - // system, readlink will completely fail to return a result larger than - // PATH_MAX even if given a sufficiently large buffer. This makes it - // fundamentally impossible to get the selfExePath of a program running in - // a very deeply nested directory chain in this way. - // TODO(#4812): Investigate other systems and whether it is possible to get - // this path by trying larger and larger buffers until one succeeds. - var buf: [max_path_bytes]u8 = undefined; - return allocator.dupe(u8, try selfExePath(&buf)); -} - -/// Get the path to the current executable. Follows symlinks. -/// If you only need the directory, use selfExeDirPath. -/// If you only want an open file handle, use openSelfExe. -/// This function may return an error if the current executable -/// was deleted after spawning. -/// Returned value is a slice of out_buffer. -/// On Windows, the result is encoded as [WTF-8](https://wtf-8.codeberg.page/). -/// On other platforms, the result is an opaque sequence of bytes with no particular encoding. -/// -/// On Linux, depends on procfs being mounted. If the currently executing binary has -/// been deleted, the file path looks something like `/a/b/c/exe (deleted)`. -/// TODO make the return type of this a null terminated pointer -pub fn selfExePath(out_buffer: []u8) SelfExePathError![]u8 { - if (is_darwin) { - // Note that _NSGetExecutablePath() will return "a path" to - // the executable not a "real path" to the executable. - var symlink_path_buf: [max_path_bytes:0]u8 = undefined; - var u32_len: u32 = max_path_bytes + 1; // include the sentinel - const rc = std.c._NSGetExecutablePath(&symlink_path_buf, &u32_len); - if (rc != 0) return error.NameTooLong; - - var real_path_buf: [max_path_bytes]u8 = undefined; - const real_path = std.posix.realpathZ(&symlink_path_buf, &real_path_buf) catch |err| switch (err) { - error.NetworkNotFound => unreachable, // Windows-only - else => |e| return e, - }; - if (real_path.len > out_buffer.len) return error.NameTooLong; - const result = out_buffer[0..real_path.len]; - @memcpy(result, real_path); - return result; - } - switch (native_os) { - .linux, .serenity => return posix.readlinkZ("/proc/self/exe", out_buffer) catch |err| switch (err) { - error.UnsupportedReparsePointType => unreachable, // Windows-only - error.NetworkNotFound => unreachable, // Windows-only - else => |e| return e, - }, - .illumos => return posix.readlinkZ("/proc/self/path/a.out", out_buffer) catch |err| switch (err) { - error.UnsupportedReparsePointType => unreachable, // Windows-only - error.NetworkNotFound => unreachable, // Windows-only - else => |e| return e, - }, - .freebsd, .dragonfly => { - var mib = [4]c_int{ posix.CTL.KERN, posix.KERN.PROC, posix.KERN.PROC_PATHNAME, -1 }; - var out_len: usize = out_buffer.len; - try posix.sysctl(&mib, out_buffer.ptr, &out_len, null, 0); - // TODO could this slice from 0 to out_len instead? - return mem.sliceTo(out_buffer, 0); - }, - .netbsd => { - var mib = [4]c_int{ posix.CTL.KERN, posix.KERN.PROC_ARGS, -1, posix.KERN.PROC_PATHNAME }; - var out_len: usize = out_buffer.len; - try posix.sysctl(&mib, out_buffer.ptr, &out_len, null, 0); - // TODO could this slice from 0 to out_len instead? - return mem.sliceTo(out_buffer, 0); - }, - .openbsd, .haiku => { - // OpenBSD doesn't support getting the path of a running process, so try to guess it - if (std.os.argv.len == 0) - return error.FileNotFound; - - const argv0 = mem.span(std.os.argv[0]); - if (mem.find(u8, argv0, "/") != null) { - // argv[0] is a path (relative or absolute): use realpath(3) directly - var real_path_buf: [max_path_bytes]u8 = undefined; - const real_path = posix.realpathZ(std.os.argv[0], &real_path_buf) catch |err| switch (err) { - error.NetworkNotFound => unreachable, // Windows-only - else => |e| return e, - }; - if (real_path.len > out_buffer.len) - return error.NameTooLong; - const result = out_buffer[0..real_path.len]; - @memcpy(result, real_path); - return result; - } else if (argv0.len != 0) { - // argv[0] is not empty (and not a path): search it inside PATH - const PATH = posix.getenvZ("PATH") orelse return error.FileNotFound; - var path_it = mem.tokenizeScalar(u8, PATH, path.delimiter); - while (path_it.next()) |a_path| { - var resolved_path_buf: [max_path_bytes - 1:0]u8 = undefined; - const resolved_path = std.fmt.bufPrintSentinel(&resolved_path_buf, "{s}/{s}", .{ - a_path, - std.os.argv[0], - }, 0) catch continue; - - var real_path_buf: [max_path_bytes]u8 = undefined; - if (posix.realpathZ(resolved_path, &real_path_buf)) |real_path| { - // found a file, and hope it is the right file - if (real_path.len > out_buffer.len) - return error.NameTooLong; - const result = out_buffer[0..real_path.len]; - @memcpy(result, real_path); - return result; - } else |_| continue; - } - } - return error.FileNotFound; - }, - .windows => { - const image_path_unicode_string = &windows.peb().ProcessParameters.ImagePathName; - const image_path_name = image_path_unicode_string.Buffer.?[0 .. image_path_unicode_string.Length / 2 :0]; - - // If ImagePathName is a symlink, then it will contain the path of the - // symlink, not the path that the symlink points to. We want the path - // that the symlink points to, though, so we need to get the realpath. - var pathname_w = try windows.wToPrefixedFileW(null, image_path_name); - - const wide_slice = try std.fs.cwd().realpathW2(pathname_w.span(), &pathname_w.data); - - const len = std.unicode.calcWtf8Len(wide_slice); - if (len > out_buffer.len) - return error.NameTooLong; - - const end_index = std.unicode.wtf16LeToWtf8(out_buffer, wide_slice); - return out_buffer[0..end_index]; - }, - else => @compileError("std.fs.selfExePath not supported for this target"), - } -} - -/// `selfExeDirPath` except allocates the result on the heap. -/// Caller owns returned memory. -pub fn selfExeDirPathAlloc(allocator: Allocator) ![]u8 { - // Use of max_path_bytes here is justified as, at least on one tested Linux - // system, readlink will completely fail to return a result larger than - // PATH_MAX even if given a sufficiently large buffer. This makes it - // fundamentally impossible to get the selfExeDirPath of a program running - // in a very deeply nested directory chain in this way. - // TODO(#4812): Investigate other systems and whether it is possible to get - // this path by trying larger and larger buffers until one succeeds. - var buf: [max_path_bytes]u8 = undefined; - return allocator.dupe(u8, try selfExeDirPath(&buf)); -} - -/// Get the directory path that contains the current executable. -/// Returned value is a slice of out_buffer. -/// On Windows, the result is encoded as [WTF-8](https://wtf-8.codeberg.page/). -/// On other platforms, the result is an opaque sequence of bytes with no particular encoding. -pub fn selfExeDirPath(out_buffer: []u8) SelfExePathError![]const u8 { - const self_exe_path = try selfExePath(out_buffer); - // Assume that the OS APIs return absolute paths, and therefore dirname - // will not return null. - return path.dirname(self_exe_path).?; -} - -/// `realpath`, except caller must free the returned memory. -/// On Windows, the result is encoded as [WTF-8](https://wtf-8.codeberg.page/). -/// On other platforms, the result is an opaque sequence of bytes with no particular encoding. -/// See also `Dir.realpath`. -pub fn realpathAlloc(allocator: Allocator, pathname: []const u8) ![]u8 { - // Use of max_path_bytes here is valid as the realpath function does not - // have a variant that takes an arbitrary-size buffer. - // TODO(#4812): Consider reimplementing realpath or using the POSIX.1-2008 - // NULL out parameter (GNU's canonicalize_file_name) to handle overelong - // paths. musl supports passing NULL but restricts the output to PATH_MAX - // anyway. - var buf: [max_path_bytes]u8 = undefined; - return allocator.dupe(u8, try posix.realpath(pathname, &buf)); -} - test { _ = AtomicFile; _ = Dir; diff --git a/lib/std/fs/test.zig b/lib/std/fs/test.zig index 7d566da0e9..1a600fb82c 100644 --- a/lib/std/fs/test.zig +++ b/lib/std/fs/test.zig @@ -4,6 +4,7 @@ const native_os = builtin.os.tag; const std = @import("../std.zig"); const Io = std.Io; const testing = std.testing; +const expect = std.testing.expect; const fs = std.fs; const mem = std.mem; const wasi = std.os.wasi; @@ -1177,21 +1178,22 @@ test "renameAbsolute" { dir.close(io); } -test "openSelfExe" { +test "openExecutable" { if (native_os == .wasi) return error.SkipZigTest; const io = testing.io; - const self_exe_file = try std.fs.openSelfExe(.{}); + const self_exe_file = try std.fs.openExecutable(.{}); self_exe_file.close(io); } -test "selfExePath" { +test "executablePath" { if (native_os == .wasi) return error.SkipZigTest; + const io = testing.io; var buf: [fs.max_path_bytes]u8 = undefined; - const buf_self_exe_path = try std.fs.selfExePath(&buf); - const alloc_self_exe_path = try std.fs.selfExePathAlloc(testing.allocator); + const buf_self_exe_path = try std.process.executablePath(io, &buf); + const alloc_self_exe_path = try std.process.executablePathAlloc(io, testing.allocator); defer testing.allocator.free(alloc_self_exe_path); try testing.expectEqualSlices(u8, buf_self_exe_path, alloc_self_exe_path); } @@ -2371,3 +2373,46 @@ test "File.Writer sendfile with buffered contents" { try testing.expectEqualStrings("abcd", try check_r.interface.take(4)); try testing.expectError(error.EndOfStream, check_r.interface.takeByte()); } + +test "readlink on Windows" { + if (native_os != .windows) return error.SkipZigTest; + + try testReadlink("C:\\ProgramData", "C:\\Users\\All Users"); + try testReadlink("C:\\Users\\Default", "C:\\Users\\Default User"); + try testReadlink("C:\\Users", "C:\\Documents and Settings"); +} + +fn testReadlink(target_path: []const u8, symlink_path: []const u8) !void { + var buffer: [fs.max_path_bytes]u8 = undefined; + const given = try Dir.readLinkAbsolute(symlink_path, buffer[0..]); + try expect(mem.eql(u8, target_path, given)); +} + +test "readlinkat" { + var tmp = tmpDir(.{}); + defer tmp.cleanup(); + + // create file + try tmp.dir.writeFile(.{ .sub_path = "file.txt", .data = "nonsense" }); + + // create a symbolic link + if (native_os == .windows) { + std.os.windows.CreateSymbolicLink( + tmp.dir.fd, + &[_]u16{ 'l', 'i', 'n', 'k' }, + &[_:0]u16{ 'f', 'i', 'l', 'e', '.', 't', 'x', 't' }, + false, + ) catch |err| switch (err) { + // Symlink requires admin privileges on windows, so this test can legitimately fail. + error.AccessDenied => return error.SkipZigTest, + else => return err, + }; + } else { + try posix.symlinkat("file.txt", tmp.dir.fd, "link"); + } + + // read the link + var buffer: [fs.max_path_bytes]u8 = undefined; + const read_link = try tmp.dir.readLink("link", &buffer); + try expect(mem.eql(u8, "file.txt", read_link)); +} diff --git a/lib/std/posix/test.zig b/lib/std/posix/test.zig index 19313e3ff7..0071a72a26 100644 --- a/lib/std/posix/test.zig +++ b/lib/std/posix/test.zig @@ -111,20 +111,6 @@ test "open smoke test" { } } -test "readlink on Windows" { - if (native_os != .windows) return error.SkipZigTest; - - try testReadlink("C:\\ProgramData", "C:\\Users\\All Users"); - try testReadlink("C:\\Users\\Default", "C:\\Users\\Default User"); - try testReadlink("C:\\Users", "C:\\Documents and Settings"); -} - -fn testReadlink(target_path: []const u8, symlink_path: []const u8) !void { - var buffer: [fs.max_path_bytes]u8 = undefined; - const given = try posix.readlink(symlink_path, buffer[0..]); - try expect(mem.eql(u8, target_path, given)); -} - fn getLinkInfo(fd: posix.fd_t) !struct { posix.ino_t, posix.nlink_t } { if (native_os == .linux) { const stx = try linux.wrapped.statx( @@ -216,35 +202,6 @@ test "fstatat" { // try expectEqual(stat.blocks, statat.blocks); } -test "readlinkat" { - var tmp = tmpDir(.{}); - defer tmp.cleanup(); - - // create file - try tmp.dir.writeFile(.{ .sub_path = "file.txt", .data = "nonsense" }); - - // create a symbolic link - if (native_os == .windows) { - std.os.windows.CreateSymbolicLink( - tmp.dir.fd, - &[_]u16{ 'l', 'i', 'n', 'k' }, - &[_:0]u16{ 'f', 'i', 'l', 'e', '.', 't', 'x', 't' }, - false, - ) catch |err| switch (err) { - // Symlink requires admin privileges on windows, so this test can legitimately fail. - error.AccessDenied => return error.SkipZigTest, - else => return err, - }; - } else { - try posix.symlinkat("file.txt", tmp.dir.fd, "link"); - } - - // read the link - var buffer: [fs.max_path_bytes]u8 = undefined; - const read_link = try posix.readlinkat(tmp.dir.fd, "link", buffer[0..]); - try expect(mem.eql(u8, "file.txt", read_link)); -} - test "getrandom" { var buf_a: [50]u8 = undefined; var buf_b: [50]u8 = undefined; diff --git a/lib/std/process.zig b/lib/std/process.zig index a8dede6ad4..77f91587fe 100644 --- a/lib/std/process.zig +++ b/lib/std/process.zig @@ -3,6 +3,7 @@ const native_os = builtin.os.tag; const std = @import("std.zig"); const Io = std.Io; +const File = std.Io.File; const fs = std.fs; const mem = std.mem; const math = std.math; @@ -12,6 +13,7 @@ const testing = std.testing; const posix = std.posix; const windows = std.os.windows; const unicode = std.unicode; +const max_path_bytes = std.fs.max_path_bytes; pub const Child = @import("process/Child.zig"); pub const abort = posix.abort; @@ -37,7 +39,7 @@ pub const GetCwdAllocError = Allocator.Error || error{CurrentWorkingDirectoryUnl pub fn getCwdAlloc(allocator: Allocator) GetCwdAllocError![]u8 { // The use of max_path_bytes here is just a heuristic: most paths will fit // in stack_buf, avoiding an extra allocation in the common case. - var stack_buf: [fs.max_path_bytes]u8 = undefined; + var stack_buf: [max_path_bytes]u8 = undefined; var heap_buf: ?[]u8 = null; defer if (heap_buf) |buf| allocator.free(buf); @@ -2112,3 +2114,97 @@ pub fn fatal(comptime format: []const u8, format_arguments: anytype) noreturn { std.log.err(format, format_arguments); exit(1); } + +pub const ExecutablePathBaseError = error{ + FileNotFound, + AccessDenied, + NotSupported, + NotDir, + SymLinkLoop, + InputOutput, + FileTooBig, + IsDir, + ProcessFdQuotaExceeded, + SystemFdQuotaExceeded, + NoDevice, + SystemResources, + NoSpaceLeft, + FileSystem, + BadPathName, + DeviceBusy, + SharingViolation, + PipeBusy, + NotLink, + PathAlreadyExists, + /// On Windows, `\\server` or `\\server\share` was not found. + NetworkNotFound, + ProcessNotFound, + /// On Windows, antivirus software is enabled by default. It can be + /// disabled, but Windows Update sometimes ignores the user's preference + /// and re-enables it. When enabled, antivirus software on Windows + /// intercepts file system operations and makes them significantly slower + /// in addition to possibly failing with this error code. + AntivirusInterference, + /// On Windows, the volume does not contain a recognized file system. File + /// system drivers might not be loaded, or the volume may be corrupt. + UnrecognizedVolume, + PermissionDenied, +} || Io.Cancelable || Io.UnexpectedError; + +pub const ExecutablePathAllocError = ExecutablePathBaseError || Allocator.Error; + +pub fn executablePathAlloc(io: Io, allocator: Allocator) ExecutablePathAllocError![:0]u8 { + var buffer: [max_path_bytes]u8 = undefined; + const n = executablePath(io, &buffer) catch |err| switch (err) { + error.NameTooLong => unreachable, + else => |e| return e, + }; + return allocator.dupeZ(u8, buffer[0..n]); +} + +pub const ExecutablePathError = ExecutablePathBaseError || error{NameTooLong}; + +/// Get the path to the current executable, following symlinks. +/// +/// This function may return an error if the current executable +/// was deleted after spawning. +/// +/// Returned value is a slice of out_buffer. +/// +/// On Windows, the result is encoded as [WTF-8](https://wtf-8.codeberg.page/). +/// On other platforms, the result is an opaque sequence of bytes with no particular encoding. +/// +/// On Linux, depends on procfs being mounted. If the currently executing binary has +/// been deleted, the file path looks something like "/a/b/c/exe (deleted)". +/// +/// See also: +/// * `executableDirPath` - to obtain only the directory +/// * `openExecutable` - to obtain only an open file handle +pub fn executablePath(io: Io, out_buffer: []u8) ExecutablePathError!usize { + return io.vtable.processExecutablePath(io.userdata, out_buffer); +} + +/// Get the directory path that contains the current executable. +/// +/// Returns index into `out_buffer`. +/// +/// On Windows, the result is encoded as [WTF-8](https://wtf-8.codeberg.page/). +/// On other platforms, the result is an opaque sequence of bytes with no particular encoding. +pub fn executableDirPath(out_buffer: []u8) ExecutablePathError!usize { + const n = try executablePath(out_buffer); + // Assert that the OS APIs return absolute paths, and therefore dirname + // will not return null. + return std.fs.path.dirname(out_buffer[0..n]).?; +} + +/// Same as `executableDirPath` except allocates the result. +pub fn executableDirPathAlloc(allocator: Allocator) ![]u8 { + var buffer: [max_path_bytes]u8 = undefined; + return allocator.dupe(u8, try executableDirPath(&buffer)); +} + +pub const OpenExecutableError = File.OpenError || ExecutablePathError || File.LockError; + +pub fn openExecutable(io: Io, flags: File.OpenFlags) OpenExecutableError!File { + return io.vtable.processExecutableOpen(io.userdata, flags); +} diff --git a/lib/std/zig/system.zig b/lib/std/zig/system.zig index cc74da956a..5c110a576d 100644 --- a/lib/std/zig/system.zig +++ b/lib/std/zig/system.zig @@ -209,7 +209,6 @@ pub const DetectError = error{ DeviceBusy, OSVersionDetectionFail, Unexpected, - ProcessNotFound, } || Io.Cancelable; /// Given a `Target.Query`, which specifies in detail which parts of the @@ -422,7 +421,6 @@ pub fn resolveTargetQuery(io: Io, query: Target.Query) DetectError!Target { error.SocketUnconnected => return error.Unexpected, error.AccessDenied, - error.ProcessNotFound, error.SymLinkLoop, error.ProcessFdQuotaExceeded, error.SystemFdQuotaExceeded, @@ -553,7 +551,6 @@ pub const AbiAndDynamicLinkerFromFileError = error{ SystemResources, ProcessFdQuotaExceeded, SystemFdQuotaExceeded, - ProcessNotFound, IsDir, WouldBlock, InputOutput, @@ -693,8 +690,10 @@ fn abiAndDynamicLinkerFromFile( // So far, no luck. Next we try to see if the information is // present in the symlink data for the dynamic linker path. - var link_buf: [posix.PATH_MAX]u8 = undefined; - const link_name = posix.readlink(dl_path, &link_buf) catch |err| switch (err) { + var link_buffer: [posix.PATH_MAX]u8 = undefined; + const link_name = if (Io.Dir.readLinkAbsolute(io, dl_path, &link_buffer)) |n| + link_buffer[0..n] + else |err| switch (err) { error.NameTooLong => unreachable, error.BadPathName => unreachable, // Windows only error.UnsupportedReparsePointType => unreachable, // Windows only @@ -839,7 +838,6 @@ fn glibcVerFromRPath(io: Io, rpath: []const u8) !std.SemanticVersion { error.NotDir => return error.GLibCNotFound, error.IsDir => return error.GLibCNotFound, - error.ProcessNotFound => |e| return e, error.ProcessFdQuotaExceeded => |e| return e, error.SystemFdQuotaExceeded => |e| return e, error.SystemResources => |e| return e, @@ -1103,7 +1101,6 @@ fn detectAbiAndDynamicLinker(io: Io, cpu: Target.Cpu, os: Target.Os, query: Targ error.SymLinkLoop, error.ProcessFdQuotaExceeded, error.SystemFdQuotaExceeded, - error.ProcessNotFound, error.Canceled, => |e| return e, -- cgit v1.2.3 From 916998315967f73c91e682e9ea05dd3232818654 Mon Sep 17 00:00:00 2001 From: Andrew Kelley Date: Sat, 6 Dec 2025 21:06:30 -0800 Subject: std.fs: migrate most of the API elsewhere --- lib/std/Io/Dir.zig | 173 +++++++++++++++++++--- lib/std/Io/File.zig | 22 +-- lib/std/crypto/Certificate/Bundle.zig | 2 +- lib/std/fs.zig | 266 +--------------------------------- lib/std/fs/test.zig | 29 ++-- lib/std/os.zig | 5 + lib/std/process.zig | 2 +- lib/std/std.zig | 2 +- lib/std/zig/system.zig | 2 +- lib/std/zig/system/linux.zig | 2 +- 10 files changed, 193 insertions(+), 312 deletions(-) (limited to 'lib/std/process.zig') diff --git a/lib/std/Io/Dir.zig b/lib/std/Io/Dir.zig index 71ea3b465a..3b552a556a 100644 --- a/lib/std/Io/Dir.zig +++ b/lib/std/Io/Dir.zig @@ -1,4 +1,5 @@ const Dir = @This(); +const root = @import("root"); const builtin = @import("builtin"); const native_os = builtin.os.tag; @@ -11,6 +12,61 @@ const Allocator = std.mem.Allocator; handle: Handle, +pub const path = std.fs.path; + +/// The maximum length of a file path that the operating system will accept. +/// +/// Paths, including those returned from file system operations, may be longer +/// than this length, but such paths cannot be successfully passed back in +/// other file system operations. However, all path components returned by file +/// system operations are assumed to fit into a `u8` array of this length. +/// +/// The byte count includes room for a null sentinel byte. +/// +/// * On Windows, `[]u8` file paths are encoded as +/// [WTF-8](https://wtf-8.codeberg.page/). +/// * On WASI, `[]u8` file paths are encoded as valid UTF-8. +/// * On other platforms, `[]u8` file paths are opaque sequences of bytes with +/// no particular encoding. +pub const max_path_bytes = switch (native_os) { + .linux, .driverkit, .ios, .maccatalyst, .macos, .tvos, .visionos, .watchos, .freebsd, .openbsd, .netbsd, .dragonfly, .haiku, .illumos, .plan9, .emscripten, .wasi, .serenity => std.posix.PATH_MAX, + // Each WTF-16LE code unit may be expanded to 3 WTF-8 bytes. + // If it would require 4 WTF-8 bytes, then there would be a surrogate + // pair in the WTF-16LE, and we (over)account 3 bytes for it that way. + // +1 for the null byte at the end, which can be encoded in 1 byte. + .windows => std.os.windows.PATH_MAX_WIDE * 3 + 1, + else => if (@hasDecl(root, "os") and @hasDecl(root.os, "PATH_MAX")) + root.os.PATH_MAX + else + @compileError("PATH_MAX not implemented for " ++ @tagName(native_os)), +}; + +/// This represents the maximum size of a `[]u8` file name component that +/// the platform's common file systems support. File name components returned by file system +/// operations are likely to fit into a `u8` array of this length, but +/// (depending on the platform) this assumption may not hold for every configuration. +/// The byte count does not include a null sentinel byte. +/// On Windows, `[]u8` file name components are encoded as [WTF-8](https://wtf-8.codeberg.page/). +/// On WASI, file name components are encoded as valid UTF-8. +/// On other platforms, `[]u8` components are an opaque sequence of bytes with no particular encoding. +pub const max_name_bytes = switch (native_os) { + .linux, .driverkit, .ios, .maccatalyst, .macos, .tvos, .visionos, .watchos, .freebsd, .openbsd, .netbsd, .dragonfly, .illumos, .serenity => std.posix.NAME_MAX, + // Haiku's NAME_MAX includes the null terminator, so subtract one. + .haiku => std.posix.NAME_MAX - 1, + // Each WTF-16LE character may be expanded to 3 WTF-8 bytes. + // If it would require 4 WTF-8 bytes, then there would be a surrogate + // pair in the WTF-16LE, and we (over)account 3 bytes for it that way. + .windows => std.os.windows.NAME_MAX * 3, + // For WASI, the MAX_NAME will depend on the host OS, so it needs to be + // as large as the largest max_name_bytes (Windows) in order to work on any host OS. + // TODO determine if this is a reasonable approach + .wasi => std.os.windows.NAME_MAX * 3, + else => if (@hasDecl(root, "os") and @hasDecl(root.os, "NAME_MAX")) + root.os.NAME_MAX + else + @compileError("NAME_MAX not implemented for " ++ @tagName(native_os)), +}; + pub const Entry = struct { name: []const u8, kind: File.Kind, @@ -148,7 +204,7 @@ pub const SelectiveWalker = struct { }) |entry| { self.name_buffer.shrinkRetainingCapacity(dirname_len); if (self.name_buffer.items.len != 0) { - try self.name_buffer.append(self.allocator, std.fs.path.sep); + try self.name_buffer.append(self.allocator, path.sep); dirname_len += 1; } try self.name_buffer.ensureUnusedCapacity(self.allocator, entry.name.len + 1); @@ -252,7 +308,7 @@ pub const Walker = struct { /// Returns 1 for a direct child of the initial directory, 2 for an entry /// within a direct child of the initial directory, etc. pub fn depth(self: Walker.Entry) usize { - return std.mem.countScalar(u8, self.path, std.fs.path.sep) + 1; + return std.mem.countScalar(u8, self.path, path.sep) + 1; } }; @@ -340,6 +396,11 @@ pub fn access(dir: Dir, io: Io, sub_path: []const u8, options: AccessOptions) Ac return io.vtable.dirAccess(io.userdata, dir, sub_path, options); } +pub fn accessAbsolute(io: Io, absolute_path: []const u8, options: AccessOptions) AccessError!void { + assert(path.isAbsolute(absolute_path)); + return access(.cwd(), io, absolute_path, options); +} + pub const OpenError = error{ FileNotFound, NotDir, @@ -379,6 +440,11 @@ pub fn openDir(dir: Dir, io: Io, sub_path: []const u8, options: OpenOptions) Ope return io.vtable.dirOpenDir(io.userdata, dir, sub_path, options); } +pub fn openDirAbsolute(io: Io, absolute_path: []const u8, options: OpenOptions) OpenError!Dir { + assert(path.isAbsolute(absolute_path)); + return openDir(.cwd(), io, absolute_path, options); +} + pub fn close(dir: Dir, io: Io) void { return io.vtable.dirClose(io.userdata, dir); } @@ -396,6 +462,11 @@ pub fn openFile(dir: Dir, io: Io, sub_path: []const u8, flags: File.OpenFlags) F return io.vtable.dirOpenFile(io.userdata, dir, sub_path, flags); } +pub fn openFileAbsolute(io: Io, absolute_path: []const u8, flags: File.OpenFlags) File.OpenError!File { + assert(path.isAbsolute(absolute_path)); + return openFile(.cwd(), io, absolute_path, flags); +} + /// Creates, opens, or overwrites a file with write access. /// /// Allocates a resource to be dellocated with `File.close`. @@ -407,6 +478,10 @@ pub fn createFile(dir: Dir, io: Io, sub_path: []const u8, flags: File.CreateFlag return io.vtable.dirCreateFile(io.userdata, dir, sub_path, flags); } +pub fn createFileAbsolute(io: Io, absolute_path: []const u8, flags: File.CreateFlags) File.OpenError!File { + return createFile(.cwd(), io, absolute_path, flags); +} + pub const WriteFileOptions = struct { /// On Windows, `sub_path` should be encoded as [WTF-8](https://wtf-8.codeberg.page/). /// On WASI, `sub_path` should be encoded as valid UTF-8. @@ -476,7 +551,7 @@ pub fn updateFile( } } - if (std.fs.path.dirname(dest_path)) |dirname| { + if (path.dirname(dest_path)) |dirname| { try dest_dir.makePath(io, dirname, .default_dir); } @@ -556,6 +631,21 @@ pub fn makeDir(dir: Dir, io: Io, sub_path: []const u8, permissions: Permissions) return io.vtable.dirMake(io.userdata, dir, sub_path, permissions); } +/// Create a new directory, based on an absolute path. +/// +/// Asserts that the path is absolute. See `makeDir` for a function that +/// operates on both absolute and relative paths. +/// +/// On Windows, `absolute_path` should be encoded as [WTF-8](https://wtf-8.codeberg.page/). +/// On WASI, `absolute_path` should be encoded as valid UTF-8. +/// On other platforms, `absolute_path` is an opaque sequence of bytes with no particular encoding. +pub fn makeDirAbsolute(io: Io, absolute_path: []const u8, permissions: Permissions) MakeError!void { + assert(path.isAbsolute(absolute_path)); + return makeDir(.cwd(), io, absolute_path, permissions); +} + +test makeDirAbsolute {} + pub const MakePathError = MakeError || StatPathError; /// Creates parent directories with default permissions as necessary to ensure @@ -703,19 +793,20 @@ pub const RealPathAllocError = RealPathError || Allocator.Error; /// Same as `realPath` except allocates result. pub fn realPathAlloc(dir: Dir, io: Io, sub_path: []const u8, allocator: Allocator) RealPathAllocError![:0]u8 { - var buffer: [std.fs.max_path_bytes]u8 = undefined; + var buffer: [max_path_bytes]u8 = undefined; const n = try realPath(dir, io, sub_path, &buffer); return allocator.dupeZ(u8, buffer[0..n]); } -pub fn realPathAbsolute(io: Io, path: []const u8, out_buffer: []u8) RealPathError!usize { - return io.vtable.dirRealPath(io.userdata, .cwd(), path, out_buffer); +pub fn realPathAbsolute(io: Io, absolute_path: []const u8, out_buffer: []u8) RealPathError!usize { + assert(path.isAbsolute(absolute_path)); + return io.vtable.dirRealPath(io.userdata, .cwd(), absolute_path, out_buffer); } /// Same as `realPathAbsolute` except allocates result. -pub fn realPathAbsoluteAlloc(io: Io, path: []const u8, allocator: Allocator) RealPathAllocError![:0]u8 { - var buffer: [std.fs.max_path_bytes]u8 = undefined; - const n = try realPathAbsolute(io, path, &buffer); +pub fn realPathAbsoluteAlloc(io: Io, absolute_path: []const u8, allocator: Allocator) RealPathAllocError![:0]u8 { + var buffer: [max_path_bytes]u8 = undefined; + const n = try realPathAbsolute(io, absolute_path, &buffer); return allocator.dupeZ(u8, buffer[0..n]); } @@ -754,6 +845,13 @@ pub fn deleteFile(dir: Dir, io: Io, sub_path: []const u8) DeleteFileError!void { return io.vtable.dirDeleteFile(io.userdata, dir, sub_path); } +pub fn deleteFileAbsolute(io: Io, absolute_path: []const u8) DeleteFileError!void { + assert(path.isAbsolute(absolute_path)); + return deleteFile(.cwd(), io, absolute_path); +} + +test deleteFileAbsolute {} + pub const DeleteDirError = error{ DirNotEmpty, FileNotFound, @@ -785,6 +883,16 @@ pub fn deleteDir(dir: Dir, io: Io, sub_path: []const u8) DeleteDirError!void { return io.vtable.dirDeleteDir(io.userdata, dir, sub_path); } +/// Same as `deleteDir` except the path is absolute. +/// +/// On Windows, `dir_path` should be encoded as [WTF-8](https://wtf-8.codeberg.page/). +/// On WASI, `dir_path` should be encoded as valid UTF-8. +/// On other platforms, `dir_path` is an opaque sequence of bytes with no particular encoding. +pub fn deleteDirAbsolute(io: Io, absolute_path: []const u8) DeleteDirError!void { + assert(path.isAbsolute(absolute_path)); + return deleteDir(.cwd(), io, absolute_path); +} + pub const RenameError = error{ /// In WASI, this error may occur when the file descriptor does /// not hold the required rights to rename a resource by path relative to it. @@ -893,6 +1001,17 @@ pub fn symLink( return io.vtable.dirSymLink(io.userdata, dir, target_path, sym_link_path, flags); } +pub fn symLinkAbsolute( + io: Io, + target_path: []const u8, + sym_link_path: []const u8, + flags: SymLinkFlags, +) SymLinkError!void { + assert(path.isAbsolute(target_path)); + assert(path.isAbsolute(sym_link_path)); + return symLink(.cwd(), io, target_path, sym_link_path, flags); +} + /// Same as `symLink`, except tries to create the symbolic link until it /// succeeds or encounters an error other than `error.PathAlreadyExists`. /// @@ -913,15 +1032,15 @@ pub fn symLinkAtomic( else => |e| return e, } - const dirname = std.fs.path.dirname(sym_link_path) orelse "."; + const dirname = path.dirname(sym_link_path) orelse "."; const rand_len = @sizeOf(u64) * 2; const temp_path_len = dirname.len + 1 + rand_len; - var temp_path_buf: [std.fs.max_path_bytes]u8 = undefined; + var temp_path_buf: [max_path_bytes]u8 = undefined; if (temp_path_len > temp_path_buf.len) return error.NameTooLong; @memcpy(temp_path_buf[0..dirname.len], dirname); - temp_path_buf[dirname.len] = std.fs.path.sep; + temp_path_buf[dirname.len] = path.sep; const temp_path = temp_path_buf[0..temp_path_len]; @@ -985,8 +1104,8 @@ pub fn readLink(dir: Dir, io: Io, sub_path: []const u8, buffer: []u8) ReadLinkEr /// On Windows, `path` should be encoded as [WTF-8](https://wtf-8.codeberg.page/). /// On WASI, `path` should be encoded as valid UTF-8. /// On other platforms, `path` is an opaque sequence of bytes with no particular encoding. -pub fn readLinkAbsolute(io: Io, path: []const u8, buffer: []u8) ReadLinkError!usize { - assert(std.fs.path.isAbsolute(path)); +pub fn readLinkAbsolute(io: Io, absolute_path: []const u8, buffer: []u8) ReadLinkError!usize { + assert(path.isAbsolute(absolute_path)); return io.vtable.dirReadLink(io.userdata, .cwd(), path, buffer); } @@ -1298,7 +1417,7 @@ fn deleteTreeMinStackSizeWithKindHint(parent: Dir, io: Io, sub_path: []const u8, // Valid use of max_path_bytes because dir_name_buf will only // ever store a single path component that was returned from the // filesystem. - var dir_name_buf: [std.fs.max_path_bytes]u8 = undefined; + var dir_name_buf: [max_path_bytes]u8 = undefined; var dir_name: []const u8 = sub_path; // Here we must avoid recursion, in order to provide O(1) memory guarantee of this function. @@ -1521,6 +1640,26 @@ pub fn copyFile( try atomic_file.finish(); } +/// Same as `copyFile`, except asserts that both `source_path` and `dest_path` +/// are absolute. +/// +/// On Windows, both paths should be encoded as [WTF-8](https://wtf-8.codeberg.page/). +/// On WASI, both paths should be encoded as valid UTF-8. +/// On other platforms, both paths are an opaque sequence of bytes with no particular encoding. +pub fn copyFileAbsolute( + source_path: []const u8, + dest_path: []const u8, + io: Io, + options: CopyFileOptions, +) !void { + assert(path.isAbsolute(source_path)); + assert(path.isAbsolute(dest_path)); + const my_cwd = cwd(); + return copyFile(my_cwd, source_path, my_cwd, dest_path, io, options); +} + +test copyFileAbsolute {} + pub const AtomicFileOptions = struct { permissions: File.Permissions = .default_file, make_path: bool = false, @@ -1538,13 +1677,13 @@ pub const AtomicFileOptions = struct { /// On WASI, `dest_path` should be encoded as valid UTF-8. /// On other platforms, `dest_path` is an opaque sequence of bytes with no particular encoding. pub fn atomicFile(parent: Dir, io: Io, dest_path: []const u8, options: AtomicFileOptions) !File.Atomic { - if (std.fs.path.dirname(dest_path)) |dirname| { + if (path.dirname(dest_path)) |dirname| { const dir = if (options.make_path) try parent.makeOpenPath(io, dirname, .{}) else try parent.openDir(io, dirname, .{}); - return .init(std.fs.path.basename(dest_path), options.permissions, dir, true, options.write_buffer); + return .init(path.basename(dest_path), options.permissions, dir, true, options.write_buffer); } else { return .init(dest_path, options.permissions, parent, false, options.write_buffer); } diff --git a/lib/std/Io/File.zig b/lib/std/Io/File.zig index b24c0b5100..8e71f648e2 100644 --- a/lib/std/Io/File.zig +++ b/lib/std/Io/File.zig @@ -489,22 +489,6 @@ pub const WriteFileStreamingError = error{ SystemResources, } || Io.Cancelable || Io.UnexpectedError; -/// Opens a file for reading or writing, without attempting to create a new -/// file, based on an absolute path. -/// -/// Returns an open resource to be released with `close`. -/// -/// Asserts that the path is absolute. See `Dir.openFile` for a function that -/// operates on both absolute and relative paths. -/// -/// On Windows, `absolute_path` should be encoded as [WTF-8](https://wtf-8.codeberg.page/). -/// On WASI, `absolute_path` should be encoded as valid UTF-8. -/// On other platforms, `absolute_path` is an opaque sequence of bytes with no particular encoding. -pub fn openAbsolute(io: Io, absolute_path: []const u8, flags: OpenFlags) OpenError!File { - assert(std.fs.path.isAbsolute(absolute_path)); - return Io.Dir.cwd().openFile(io, absolute_path, flags); -} - pub const SeekError = error{ Unseekable, /// The file descriptor does not hold the required rights to seek on it. @@ -579,3 +563,9 @@ pub const DowngradeLockError = Io.Cancelable || Io.UnexpectedError; pub fn downgradeLock(file: File, io: Io) LockError!void { return io.vtable.fileDowngradeLock(io.userdata, file); } + +test { + _ = Reader; + _ = Writer; + _ = Atomic; +} diff --git a/lib/std/crypto/Certificate/Bundle.zig b/lib/std/crypto/Certificate/Bundle.zig index eb60ad37a8..d4a9f1e757 100644 --- a/lib/std/crypto/Certificate/Bundle.zig +++ b/lib/std/crypto/Certificate/Bundle.zig @@ -221,7 +221,7 @@ pub fn addCertsFromFilePathAbsolute( now: Io.Timestamp, abs_file_path: []const u8, ) AddCertsFromFilePathError!void { - var file = try fs.openFileAbsolute(abs_file_path, .{}); + var file = try Io.Dir.openFileAbsolute(io, abs_file_path, .{}); defer file.close(io); var file_reader = file.reader(io, &.{}); return addCertsFromFile(cb, gpa, &file_reader, now.toSeconds()); diff --git a/lib/std/fs.zig b/lib/std/fs.zig index 78c924f8d9..5f2d36323a 100644 --- a/lib/std/fs.zig +++ b/lib/std/fs.zig @@ -1,280 +1,28 @@ //! File System. -const builtin = @import("builtin"); -const native_os = builtin.os.tag; const std = @import("std.zig"); -const Io = std.Io; -const root = @import("root"); -const mem = std.mem; -const base64 = std.base64; -const crypto = std.crypto; -const Allocator = std.mem.Allocator; -const assert = std.debug.assert; -const posix = std.posix; -const windows = std.os.windows; - -const is_darwin = native_os.isDarwin(); - -/// Deprecated. -pub const AtomicFile = std.Io.File.Atomic; -/// Deprecated. -pub const Dir = std.Io.Dir; -/// Deprecated. -pub const File = std.Io.File; +/// Deprecated, use `std.Io.Dir.path`. pub const path = @import("fs/path.zig"); pub const wasi = @import("fs/wasi.zig"); pub const getAppDataDir = @import("fs/get_app_data_dir.zig").getAppDataDir; pub const GetAppDataDirError = @import("fs/get_app_data_dir.zig").GetAppDataDirError; -/// The maximum length of a file path that the operating system will accept. -/// -/// Paths, including those returned from file system operations, may be longer -/// than this length, but such paths cannot be successfully passed back in -/// other file system operations. However, all path components returned by file -/// system operations are assumed to fit into a `u8` array of this length. -/// -/// The byte count includes room for a null sentinel byte. -/// -/// * On Windows, `[]u8` file paths are encoded as -/// [WTF-8](https://wtf-8.codeberg.page/). -/// * On WASI, `[]u8` file paths are encoded as valid UTF-8. -/// * On other platforms, `[]u8` file paths are opaque sequences of bytes with -/// no particular encoding. -pub const max_path_bytes = switch (native_os) { - .linux, .driverkit, .ios, .maccatalyst, .macos, .tvos, .visionos, .watchos, .freebsd, .openbsd, .netbsd, .dragonfly, .haiku, .illumos, .plan9, .emscripten, .wasi, .serenity => posix.PATH_MAX, - // Each WTF-16LE code unit may be expanded to 3 WTF-8 bytes. - // If it would require 4 WTF-8 bytes, then there would be a surrogate - // pair in the WTF-16LE, and we (over)account 3 bytes for it that way. - // +1 for the null byte at the end, which can be encoded in 1 byte. - .windows => windows.PATH_MAX_WIDE * 3 + 1, - else => if (@hasDecl(root, "os") and @hasDecl(root.os, "PATH_MAX")) - root.os.PATH_MAX - else - @compileError("PATH_MAX not implemented for " ++ @tagName(native_os)), -}; - -/// This represents the maximum size of a `[]u8` file name component that -/// the platform's common file systems support. File name components returned by file system -/// operations are likely to fit into a `u8` array of this length, but -/// (depending on the platform) this assumption may not hold for every configuration. -/// The byte count does not include a null sentinel byte. -/// On Windows, `[]u8` file name components are encoded as [WTF-8](https://wtf-8.codeberg.page/). -/// On WASI, file name components are encoded as valid UTF-8. -/// On other platforms, `[]u8` components are an opaque sequence of bytes with no particular encoding. -pub const max_name_bytes = switch (native_os) { - .linux, .driverkit, .ios, .maccatalyst, .macos, .tvos, .visionos, .watchos, .freebsd, .openbsd, .netbsd, .dragonfly, .illumos, .serenity => posix.NAME_MAX, - // Haiku's NAME_MAX includes the null terminator, so subtract one. - .haiku => posix.NAME_MAX - 1, - // Each WTF-16LE character may be expanded to 3 WTF-8 bytes. - // If it would require 4 WTF-8 bytes, then there would be a surrogate - // pair in the WTF-16LE, and we (over)account 3 bytes for it that way. - .windows => windows.NAME_MAX * 3, - // For WASI, the MAX_NAME will depend on the host OS, so it needs to be - // as large as the largest max_name_bytes (Windows) in order to work on any host OS. - // TODO determine if this is a reasonable approach - .wasi => windows.NAME_MAX * 3, - else => if (@hasDecl(root, "os") and @hasDecl(root.os, "NAME_MAX")) - root.os.NAME_MAX - else - @compileError("NAME_MAX not implemented for " ++ @tagName(native_os)), -}; - pub const base64_alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_".*; /// Base64 encoder, replacing the standard `+/` with `-_` so that it can be used in a file name on any filesystem. -pub const base64_encoder = base64.Base64Encoder.init(base64_alphabet, null); +pub const base64_encoder = std.base64.Base64Encoder.init(base64_alphabet, null); /// Base64 decoder, replacing the standard `+/` with `-_` so that it can be used in a file name on any filesystem. -pub const base64_decoder = base64.Base64Decoder.init(base64_alphabet, null); - -/// Same as `Dir.copyFile`, except asserts that both `source_path` and `dest_path` -/// are absolute. See `Dir.copyFile` for a function that operates on both -/// absolute and relative paths. -/// On Windows, both paths should be encoded as [WTF-8](https://wtf-8.codeberg.page/). -/// On WASI, both paths should be encoded as valid UTF-8. -/// On other platforms, both paths are an opaque sequence of bytes with no particular encoding. -pub fn copyFileAbsolute( - source_path: []const u8, - dest_path: []const u8, - args: Dir.CopyFileOptions, -) !void { - assert(path.isAbsolute(source_path)); - assert(path.isAbsolute(dest_path)); - const my_cwd = cwd(); - return Dir.copyFile(my_cwd, source_path, my_cwd, dest_path, args); -} - -test copyFileAbsolute {} - -/// Create a new directory, based on an absolute path. -/// Asserts that the path is absolute. See `Dir.makeDir` for a function that operates -/// on both absolute and relative paths. -/// On Windows, `absolute_path` should be encoded as [WTF-8](https://wtf-8.codeberg.page/). -/// On WASI, `absolute_path` should be encoded as valid UTF-8. -/// On other platforms, `absolute_path` is an opaque sequence of bytes with no particular encoding. -pub fn makeDirAbsolute(absolute_path: []const u8) !void { - assert(path.isAbsolute(absolute_path)); - return posix.mkdir(absolute_path, Dir.default_mode); -} - -test makeDirAbsolute {} - -/// Same as `makeDirAbsolute` except the parameter is null-terminated. -pub fn makeDirAbsoluteZ(absolute_path_z: [*:0]const u8) !void { - assert(path.isAbsoluteZ(absolute_path_z)); - return posix.mkdirZ(absolute_path_z, Dir.default_mode); -} - -test makeDirAbsoluteZ {} - -/// Same as `Dir.deleteDir` except the path is absolute. -/// On Windows, `dir_path` should be encoded as [WTF-8](https://wtf-8.codeberg.page/). -/// On WASI, `dir_path` should be encoded as valid UTF-8. -/// On other platforms, `dir_path` is an opaque sequence of bytes with no particular encoding. -pub fn deleteDirAbsolute(dir_path: []const u8) !void { - assert(path.isAbsolute(dir_path)); - return posix.rmdir(dir_path); -} - -/// Same as `deleteDirAbsolute` except the path parameter is null-terminated. -pub fn deleteDirAbsoluteZ(dir_path: [*:0]const u8) !void { - assert(path.isAbsoluteZ(dir_path)); - return posix.rmdirZ(dir_path); -} - -/// Deprecated in favor of `Io.Dir.cwd`. -pub fn cwd() Io.Dir { - return .cwd(); -} +pub const base64_decoder = std.base64.Base64Decoder.init(base64_alphabet, null); -pub fn defaultWasiCwd() std.os.wasi.fd_t { - // Expect the first preopen to be current working directory. - return 3; -} - -/// Opens a directory at the given path. The directory is a system resource that remains -/// open until `close` is called on the result. -/// See `openDirAbsoluteZ` for a function that accepts a null-terminated path. -/// -/// Asserts that the path parameter has no null bytes. -/// On Windows, `absolute_path` should be encoded as [WTF-8](https://wtf-8.codeberg.page/). -/// On WASI, `absolute_path` should be encoded as valid UTF-8. -/// On other platforms, `absolute_path` is an opaque sequence of bytes with no particular encoding. -pub fn openDirAbsolute(absolute_path: []const u8, flags: Dir.OpenOptions) File.OpenError!Dir { - assert(path.isAbsolute(absolute_path)); - return cwd().openDir(absolute_path, flags); -} - -/// Deprecated in favor of `Io.File.openAbsolute`. -pub fn openFileAbsolute(absolute_path: []const u8, flags: File.OpenFlags) Io.File.OpenError!Io.File { - var threaded: Io.Threaded = .init_single_threaded; - const io = threaded.ioBasic(); - return Io.File.openAbsolute(io, absolute_path, flags); -} - -/// Test accessing `path`. -/// Be careful of Time-Of-Check-Time-Of-Use race conditions when using this function. -/// For example, instead of testing if a file exists and then opening it, just -/// open it and handle the error for file not found. -/// See `accessAbsoluteZ` for a function that accepts a null-terminated path. -/// On Windows, `absolute_path` should be encoded as [WTF-8](https://wtf-8.codeberg.page/). -/// On WASI, `absolute_path` should be encoded as valid UTF-8. -/// On other platforms, `absolute_path` is an opaque sequence of bytes with no particular encoding. -pub fn accessAbsolute(absolute_path: []const u8, flags: Io.Dir.AccessOptions) Dir.AccessError!void { - assert(path.isAbsolute(absolute_path)); - try cwd().access(absolute_path, flags); -} -/// Creates, opens, or overwrites a file with write access, based on an absolute path. -/// Call `File.close` to release the resource. -/// Asserts that the path is absolute. See `Dir.createFile` for a function that -/// operates on both absolute and relative paths. -/// Asserts that the path parameter has no null bytes. See `createFileAbsoluteC` for a function -/// that accepts a null-terminated path. -/// On Windows, `absolute_path` should be encoded as [WTF-8](https://wtf-8.codeberg.page/). -/// On WASI, `absolute_path` should be encoded as valid UTF-8. -/// On other platforms, `absolute_path` is an opaque sequence of bytes with no particular encoding. -pub fn createFileAbsolute(absolute_path: []const u8, flags: File.CreateFlags) File.OpenError!File { - assert(path.isAbsolute(absolute_path)); - return cwd().createFile(absolute_path, flags); -} - -/// Delete a file name and possibly the file it refers to, based on an absolute path. -/// Asserts that the path is absolute. See `Dir.deleteFile` for a function that -/// operates on both absolute and relative paths. -/// Asserts that the path parameter has no null bytes. -/// On Windows, `absolute_path` should be encoded as [WTF-8](https://wtf-8.codeberg.page/). -/// On WASI, `absolute_path` should be encoded as valid UTF-8. -/// On other platforms, `absolute_path` is an opaque sequence of bytes with no particular encoding. -pub fn deleteFileAbsolute(absolute_path: []const u8) Dir.DeleteFileError!void { - assert(path.isAbsolute(absolute_path)); - return cwd().deleteFile(absolute_path); -} - -/// Removes a symlink, file, or directory. -/// This is equivalent to `Dir.deleteTree` with the base directory. -/// Asserts that the path is absolute. See `Dir.deleteTree` for a function that -/// operates on both absolute and relative paths. -/// Asserts that the path parameter has no null bytes. -/// On Windows, `absolute_path` should be encoded as [WTF-8](https://wtf-8.codeberg.page/). -/// On WASI, `absolute_path` should be encoded as valid UTF-8. -/// On other platforms, `absolute_path` is an opaque sequence of bytes with no particular encoding. -pub fn deleteTreeAbsolute(io: Io, absolute_path: []const u8) !void { - assert(path.isAbsolute(absolute_path)); - const dirname = path.dirname(absolute_path) orelse return error{ - /// Attempt to remove the root file system path. - /// This error is unreachable if `absolute_path` is relative. - CannotDeleteRootDirectory, - }.CannotDeleteRootDirectory; - - var dir = try cwd().openDir(dirname, .{}); - defer dir.close(io); - - return dir.deleteTree(path.basename(absolute_path)); -} - -/// Creates a symbolic link named `sym_link_path` which contains the string `target_path`. -/// A symbolic link (also known as a soft link) may point to an existing file or to a nonexistent -/// one; the latter case is known as a dangling link. -/// If `sym_link_path` exists, it will not be overwritten. -/// See also `symLinkAbsoluteZ` and `symLinkAbsoluteW`. -/// On Windows, both paths should be encoded as [WTF-8](https://wtf-8.codeberg.page/). -/// On WASI, both paths should be encoded as valid UTF-8. -/// On other platforms, both paths are an opaque sequence of bytes with no particular encoding. -pub fn symLinkAbsolute( - target_path: []const u8, - sym_link_path: []const u8, - flags: Dir.SymLinkFlags, -) !void { - assert(path.isAbsolute(target_path)); - assert(path.isAbsolute(sym_link_path)); - if (native_os == .windows) { - const target_path_w = try windows.sliceToPrefixedFileW(null, target_path); - const sym_link_path_w = try windows.sliceToPrefixedFileW(null, sym_link_path); - return windows.CreateSymbolicLink(null, sym_link_path_w.span(), target_path_w.span(), flags.is_directory); - } - return posix.symlink(target_path, sym_link_path); -} - -/// Windows-only. Same as `symLinkAbsolute` except the parameters are null-terminated, WTF16 LE encoded. -/// Note that this function will by default try creating a symbolic link to a file. If you would -/// like to create a symbolic link to a directory, specify this with `SymLinkFlags{ .is_directory = true }`. -/// See also `symLinkAbsolute`, `symLinkAbsoluteZ`. -pub fn symLinkAbsoluteW( - target_path_w: [*:0]const u16, - sym_link_path_w: [*:0]const u16, - flags: Dir.SymLinkFlags, -) !void { - assert(path.isAbsoluteWindowsW(target_path_w)); - assert(path.isAbsoluteWindowsW(sym_link_path_w)); - return windows.CreateSymbolicLink(null, mem.span(sym_link_path_w), mem.span(target_path_w), flags.is_directory); -} +/// Deprecated, use `std.Io.Dir.max_path_bytes`. +pub const max_path_bytes = std.Io.Dir.max_path_bytes; +/// Deprecated, use `std.Io.Dir.max_name_bytes`. +pub const max_name_bytes = std.Io.Dir.max_name_bytes; test { - _ = AtomicFile; - _ = Dir; - _ = File; _ = path; _ = @import("fs/test.zig"); _ = @import("fs/get_app_data_dir.zig"); diff --git a/lib/std/fs/test.zig b/lib/std/fs/test.zig index 1a600fb82c..f4bdecf89d 100644 --- a/lib/std/fs/test.zig +++ b/lib/std/fs/test.zig @@ -2087,7 +2087,7 @@ test "'.' and '..' in absolute functions" { try fs.copyFileAbsolute(created_file_path, copied_file_path, .{}); const renamed_file_path = try fs.path.join(allocator, &.{ subdir_path, "../rename" }); try fs.renameAbsolute(copied_file_path, renamed_file_path); - const renamed_file = try fs.openFileAbsolute(renamed_file_path, .{}); + const renamed_file = try Dir.openFileAbsolute(renamed_file_path, .{}); renamed_file.close(io); try fs.deleteFileAbsolute(renamed_file_path); @@ -2202,20 +2202,19 @@ test "invalid UTF-8/WTF-8 paths" { try testing.expectError(expected_err, fs.rename(ctx.dir, invalid_path, ctx.dir, invalid_path)); if (native_os != .wasi and ctx.path_type != .relative) { - try testing.expectError(expected_err, fs.copyFileAbsolute(invalid_path, invalid_path, .{})); - try testing.expectError(expected_err, fs.makeDirAbsolute(invalid_path)); - try testing.expectError(expected_err, fs.deleteDirAbsolute(invalid_path)); - try testing.expectError(expected_err, fs.renameAbsolute(invalid_path, invalid_path)); - try testing.expectError(expected_err, fs.openDirAbsolute(invalid_path, .{})); - try testing.expectError(expected_err, fs.openFileAbsolute(invalid_path, .{})); - try testing.expectError(expected_err, fs.accessAbsolute(invalid_path, .{})); - try testing.expectError(expected_err, fs.createFileAbsolute(invalid_path, .{})); - try testing.expectError(expected_err, fs.deleteFileAbsolute(invalid_path)); - try testing.expectError(expected_err, fs.deleteTreeAbsolute(invalid_path)); - var readlink_buf: [fs.max_path_bytes]u8 = undefined; - try testing.expectError(expected_err, fs.readLinkAbsolute(invalid_path, &readlink_buf)); - try testing.expectError(expected_err, fs.symLinkAbsolute(invalid_path, invalid_path, .{})); - try testing.expectError(expected_err, fs.realpathAlloc(testing.allocator, invalid_path)); + try testing.expectError(expected_err, Dir.copyFileAbsolute(invalid_path, invalid_path, .{})); + try testing.expectError(expected_err, Dir.makeDirAbsolute(invalid_path)); + try testing.expectError(expected_err, Dir.deleteDirAbsolute(invalid_path)); + try testing.expectError(expected_err, Dir.renameAbsolute(invalid_path, invalid_path)); + try testing.expectError(expected_err, Dir.openDirAbsolute(invalid_path, .{})); + try testing.expectError(expected_err, Dir.openFileAbsolute(invalid_path, .{})); + try testing.expectError(expected_err, Dir.accessAbsolute(invalid_path, .{})); + try testing.expectError(expected_err, Dir.createFileAbsolute(invalid_path, .{})); + try testing.expectError(expected_err, Dir.deleteFileAbsolute(invalid_path)); + var readlink_buf: [Dir.max_path_bytes]u8 = undefined; + try testing.expectError(expected_err, Dir.readLinkAbsolute(invalid_path, &readlink_buf)); + try testing.expectError(expected_err, Dir.symLinkAbsolute(invalid_path, invalid_path, .{})); + try testing.expectError(expected_err, Dir.realpathAlloc(testing.allocator, invalid_path)); } } }.impl); diff --git a/lib/std/os.zig b/lib/std/os.zig index 77a3833c2b..667d743f3d 100644 --- a/lib/std/os.zig +++ b/lib/std/os.zig @@ -72,3 +72,8 @@ pub fn fstat_wasi(fd: posix.fd_t) FstatError!wasi.filestat_t { else => |err| return posix.unexpectedErrno(err), } } + +pub fn defaultWasiCwd() std.os.wasi.fd_t { + // Expect the first preopen to be current working directory. + return 3; +} diff --git a/lib/std/process.zig b/lib/std/process.zig index 77f91587fe..5d60190c2b 100644 --- a/lib/std/process.zig +++ b/lib/std/process.zig @@ -1576,7 +1576,7 @@ pub fn getUserInfo(name: []const u8) !UserInfo { /// TODO this reads /etc/passwd. But sometimes the user/id mapping is in something else /// like NIS, AD, etc. See `man nss` or look at an strace for `id myuser`. pub fn posixGetUserInfo(io: Io, name: []const u8) !UserInfo { - const file = try std.fs.openFileAbsolute("/etc/passwd", .{}); + const file = try Io.Dir.openFileAbsolute(io, "/etc/passwd", .{}); defer file.close(io); var buffer: [4096]u8 = undefined; var file_reader = file.reader(&buffer); diff --git a/lib/std/std.zig b/lib/std/std.zig index 84e402f52b..106811859b 100644 --- a/lib/std/std.zig +++ b/lib/std/std.zig @@ -115,7 +115,7 @@ pub const Options = struct { enable_segfault_handler: bool = debug.default_enable_segfault_handler, /// Function used to implement `std.fs.cwd` for WASI. - wasiCwd: fn () os.wasi.fd_t = fs.defaultWasiCwd, + wasiCwd: fn () os.wasi.fd_t = os.defaultWasiCwd, /// The current log level. log_level: log.Level = log.default_level, diff --git a/lib/std/zig/system.zig b/lib/std/zig/system.zig index 5c110a576d..d3bafc16f2 100644 --- a/lib/std/zig/system.zig +++ b/lib/std/zig/system.zig @@ -1024,7 +1024,7 @@ fn detectAbiAndDynamicLinker(io: Io, cpu: Target.Cpu, os: Target.Os, query: Targ }; while (true) { - const file = fs.openFileAbsolute(file_name, .{}) catch |err| switch (err) { + const file = Io.Dir.openFileAbsolute(io, file_name, .{}) catch |err| switch (err) { error.NoSpaceLeft => return error.Unexpected, error.NameTooLong => return error.Unexpected, error.PathAlreadyExists => return error.Unexpected, diff --git a/lib/std/zig/system/linux.zig b/lib/std/zig/system/linux.zig index 668e5a1d99..60f4d7bfec 100644 --- a/lib/std/zig/system/linux.zig +++ b/lib/std/zig/system/linux.zig @@ -444,7 +444,7 @@ inline fn getAArch64CpuFeature(comptime feat_reg: []const u8) u64 { } pub fn detectNativeCpuAndFeatures(io: Io) ?Target.Cpu { - var file = fs.openFileAbsolute("/proc/cpuinfo", .{}) catch |err| switch (err) { + var file = Io.Dir.openFileAbsolute(io, "/proc/cpuinfo", .{}) catch |err| switch (err) { else => return null, }; defer file.close(io); -- cgit v1.2.3 From 9ccd68de0b79c3723bd11071fd836bc24ff25b33 Mon Sep 17 00:00:00 2001 From: Andrew Kelley Date: Mon, 8 Dec 2025 16:13:51 -0800 Subject: std: move abort and exit from posix into process and delete the unit tests that called fork() no forking allowed in the std lib, including unit tests, except to implement child process spawning. --- lib/compiler/translate-c/main.zig | 2 +- lib/std/Build.zig | 9 ++-- lib/std/Build/Cache/Path.zig | 4 +- lib/std/Build/Step/ConfigHeader.zig | 3 +- lib/std/Build/Step/ObjCopy.zig | 3 +- lib/std/Build/Step/Run.zig | 7 +-- lib/std/Build/Step/UpdateSourceFiles.zig | 2 +- lib/std/Build/Step/WriteFile.zig | 6 +-- lib/std/debug.zig | 12 +++-- lib/std/fs/test.zig | 45 ++++++++++------- lib/std/os/linux/IoUring.zig | 26 ---------- lib/std/posix.zig | 87 -------------------------------- lib/std/posix/test.zig | 67 ------------------------ lib/std/process.zig | 86 ++++++++++++++++++++++++++++++- lib/std/process/Child.zig | 2 +- lib/std/start.zig | 6 +-- lib/std/tar.zig | 16 +++--- lib/std/zip.zig | 6 +-- src/Compilation.zig | 10 ++-- src/Package/Fetch.zig | 5 +- src/main.zig | 11 ++-- 21 files changed, 167 insertions(+), 248 deletions(-) (limited to 'lib/std/process.zig') diff --git a/lib/compiler/translate-c/main.zig b/lib/compiler/translate-c/main.zig index d0a873fd78..d140145032 100644 --- a/lib/compiler/translate-c/main.zig +++ b/lib/compiler/translate-c/main.zig @@ -253,7 +253,7 @@ fn translate(d: *aro.Driver, tc: *aro.Toolchain, args: [][:0]u8, zig_integration if (d.output_name) |path| blk: { if (std.mem.eql(u8, path, "-")) break :blk; if (std.fs.path.dirname(path)) |dirname| { - Io.Dir.cwd().makePath(dirname) catch |err| + Io.Dir.cwd().makePath(io, dirname) catch |err| return d.fatal("failed to create path to '{s}': {s}", .{ path, aro.Driver.errorDescription(err) }); } out_file = Io.Dir.cwd().createFile(io, path, .{}) catch |err| { diff --git a/lib/std/Build.zig b/lib/std/Build.zig index 3d0e8b8dbc..1eff8813e5 100644 --- a/lib/std/Build.zig +++ b/lib/std/Build.zig @@ -1706,7 +1706,7 @@ pub fn truncateFile(b: *Build, dest_path: []const u8) (Io.Dir.MakeError || Io.Di var src_file = cwd.createFile(io, dest_path, .{}) catch |err| switch (err) { error.FileNotFound => blk: { if (fs.path.dirname(dest_path)) |dirname| { - try cwd.makePath(dirname); + try cwd.makePath(io, dirname); } break :blk try cwd.createFile(io, dest_path, .{}); }, @@ -2634,13 +2634,12 @@ pub const InstallDir = union(enum) { /// source of API breakage in the future, so keep that in mind when using this /// function. pub fn makeTempPath(b: *Build) []const u8 { + const io = b.graph.io; const rand_int = std.crypto.random.int(u64); const tmp_dir_sub_path = "tmp" ++ fs.path.sep_str ++ std.fmt.hex(rand_int); const result_path = b.cache_root.join(b.allocator, &.{tmp_dir_sub_path}) catch @panic("OOM"); - b.cache_root.handle.makePath(tmp_dir_sub_path) catch |err| { - std.debug.print("unable to make tmp path '{s}': {s}\n", .{ - result_path, @errorName(err), - }); + b.cache_root.handle.makePath(io, tmp_dir_sub_path) catch |err| { + std.debug.print("unable to make tmp path '{s}': {t}\n", .{ result_path, err }); }; return result_path; } diff --git a/lib/std/Build/Cache/Path.zig b/lib/std/Build/Cache/Path.zig index 93e0c0d792..941948a9cd 100644 --- a/lib/std/Build/Cache/Path.zig +++ b/lib/std/Build/Cache/Path.zig @@ -128,14 +128,14 @@ pub fn access(p: Path, sub_path: []const u8, flags: Io.Dir.AccessOptions) !void return p.root_dir.handle.access(joined_path, flags); } -pub fn makePath(p: Path, sub_path: []const u8) !void { +pub fn makePath(p: Path, io: Io, sub_path: []const u8) !void { var buf: [fs.max_path_bytes]u8 = undefined; const joined_path = if (p.sub_path.len == 0) sub_path else p: { break :p std.fmt.bufPrint(&buf, "{s}" ++ fs.path.sep_str ++ "{s}", .{ p.sub_path, sub_path, }) catch return error.NameTooLong; }; - return p.root_dir.handle.makePath(joined_path); + return p.root_dir.handle.makePath(io, joined_path); } pub fn toString(p: Path, allocator: Allocator) Allocator.Error![]u8 { diff --git a/lib/std/Build/Step/ConfigHeader.zig b/lib/std/Build/Step/ConfigHeader.zig index ea7d9d99ff..f377959610 100644 --- a/lib/std/Build/Step/ConfigHeader.zig +++ b/lib/std/Build/Step/ConfigHeader.zig @@ -184,6 +184,7 @@ fn make(step: *Step, options: Step.MakeOptions) !void { const gpa = b.allocator; const arena = b.allocator; + const io = b.graph.io; var man = b.graph.cache.obtain(); defer man.deinit(); @@ -257,7 +258,7 @@ fn make(step: *Step, options: Step.MakeOptions) !void { const sub_path = b.pathJoin(&.{ "o", &digest, config_header.include_path }); const sub_path_dirname = std.fs.path.dirname(sub_path).?; - b.cache_root.handle.makePath(sub_path_dirname) catch |err| { + b.cache_root.handle.makePath(io, sub_path_dirname) catch |err| { return step.fail("unable to make path '{f}{s}': {s}", .{ b.cache_root, sub_path_dirname, @errorName(err), }); diff --git a/lib/std/Build/Step/ObjCopy.zig b/lib/std/Build/Step/ObjCopy.zig index 4aa1c0a9dc..b81f59b9a1 100644 --- a/lib/std/Build/Step/ObjCopy.zig +++ b/lib/std/Build/Step/ObjCopy.zig @@ -143,6 +143,7 @@ pub fn getOutputSeparatedDebug(objcopy: *const ObjCopy) ?std.Build.LazyPath { fn make(step: *Step, options: Step.MakeOptions) !void { const prog_node = options.progress_node; const b = step.owner; + const io = b.graph.io; const objcopy: *ObjCopy = @fieldParentPtr("step", step); try step.singleUnchangingWatchInput(objcopy.input_file); @@ -176,7 +177,7 @@ fn make(step: *Step, options: Step.MakeOptions) !void { const cache_path = "o" ++ fs.path.sep_str ++ digest; const full_dest_path = try b.cache_root.join(b.allocator, &.{ cache_path, objcopy.basename }); const full_dest_path_debug = try b.cache_root.join(b.allocator, &.{ cache_path, b.fmt("{s}.debug", .{objcopy.basename}) }); - b.cache_root.handle.makePath(cache_path) catch |err| { + b.cache_root.handle.makePath(io, cache_path) catch |err| { return step.fail("unable to make path {s}: {s}", .{ cache_path, @errorName(err) }); }; diff --git a/lib/std/Build/Step/Run.zig b/lib/std/Build/Step/Run.zig index af6bc20438..54e77bd614 100644 --- a/lib/std/Build/Step/Run.zig +++ b/lib/std/Build/Step/Run.zig @@ -973,7 +973,7 @@ fn make(step: *Step, options: Step.MakeOptions) !void { .output_directory => output_sub_path, else => unreachable, }; - b.cache_root.handle.makePath(output_sub_dir_path) catch |err| { + b.cache_root.handle.makePath(io, output_sub_dir_path) catch |err| { return step.fail("unable to make path '{f}{s}': {s}", .{ b.cache_root, output_sub_dir_path, @errorName(err), }); @@ -1005,7 +1005,7 @@ fn make(step: *Step, options: Step.MakeOptions) !void { .output_directory => output_sub_path, else => unreachable, }; - b.cache_root.handle.makePath(output_sub_dir_path) catch |err| { + b.cache_root.handle.makePath(io, output_sub_dir_path) catch |err| { return step.fail("unable to make path '{f}{s}': {s}", .{ b.cache_root, output_sub_dir_path, @errorName(err), }); @@ -1241,6 +1241,7 @@ fn runCommand( const b = step.owner; const arena = b.allocator; const gpa = options.gpa; + const io = b.graph.io; const cwd: ?[]const u8 = if (run.cwd) |lazy_cwd| lazy_cwd.getPath2(b, step) else null; @@ -1470,7 +1471,7 @@ fn runCommand( const sub_path = b.pathJoin(&output_components); const sub_path_dirname = fs.path.dirname(sub_path).?; - b.cache_root.handle.makePath(sub_path_dirname) catch |err| { + b.cache_root.handle.makePath(io, sub_path_dirname) catch |err| { return step.fail("unable to make path '{f}{s}': {s}", .{ b.cache_root, sub_path_dirname, @errorName(err), }); diff --git a/lib/std/Build/Step/UpdateSourceFiles.zig b/lib/std/Build/Step/UpdateSourceFiles.zig index f5d95182e9..44c6ae1ed4 100644 --- a/lib/std/Build/Step/UpdateSourceFiles.zig +++ b/lib/std/Build/Step/UpdateSourceFiles.zig @@ -78,7 +78,7 @@ fn make(step: *Step, options: Step.MakeOptions) !void { var any_miss = false; for (usf.output_source_files.items) |output_source_file| { if (fs.path.dirname(output_source_file.sub_path)) |dirname| { - b.build_root.handle.makePath(dirname) catch |err| { + b.build_root.handle.makePath(io, dirname) catch |err| { return step.fail("unable to make path '{f}{s}': {t}", .{ b.build_root, dirname, err }); }; } diff --git a/lib/std/Build/Step/WriteFile.zig b/lib/std/Build/Step/WriteFile.zig index 85dc9b3fa2..21346959e7 100644 --- a/lib/std/Build/Step/WriteFile.zig +++ b/lib/std/Build/Step/WriteFile.zig @@ -268,7 +268,7 @@ fn make(step: *Step, options: Step.MakeOptions) !void { for (write_file.files.items) |file| { if (fs.path.dirname(file.sub_path)) |dirname| { - cache_dir.makePath(dirname) catch |err| { + cache_dir.makePath(io, dirname) catch |err| { return step.fail("unable to make path '{f}{s}{c}{s}': {t}", .{ b.cache_root, cache_path, fs.path.sep, dirname, err, }); @@ -303,7 +303,7 @@ fn make(step: *Step, options: Step.MakeOptions) !void { const dest_dirname = dir.sub_path; if (dest_dirname.len != 0) { - cache_dir.makePath(dest_dirname) catch |err| { + cache_dir.makePath(io, dest_dirname) catch |err| { return step.fail("unable to make path '{f}{s}{c}{s}': {s}", .{ b.cache_root, cache_path, fs.path.sep, dest_dirname, @errorName(err), }); @@ -318,7 +318,7 @@ fn make(step: *Step, options: Step.MakeOptions) !void { const src_entry_path = try src_dir_path.join(arena, entry.path); const dest_path = b.pathJoin(&.{ dest_dirname, entry.path }); switch (entry.kind) { - .directory => try cache_dir.makePath(dest_path), + .directory => try cache_dir.makePath(io, dest_path), .file => { const prev_status = Io.Dir.updateFile( src_entry_path.root_dir.handle, diff --git a/lib/std/debug.zig b/lib/std/debug.zig index 5df0eef2d5..7ede172d86 100644 --- a/lib/std/debug.zig +++ b/lib/std/debug.zig @@ -522,7 +522,7 @@ pub fn defaultPanic( } @trap(); }, - .cuda, .amdhsa => std.posix.abort(), + .cuda, .amdhsa => std.process.abort(), .plan9 => { var status: [std.os.plan9.ERRMAX]u8 = undefined; const len = @min(msg.len, status.len - 1); @@ -575,12 +575,13 @@ pub fn defaultPanic( // A panic happened while trying to print a previous panic message. // We're still holding the mutex but that's fine as we're going to // call abort(). - File.stderr().writeStreamingAll("aborting due to recursive panic\n") catch {}; + const stderr, _ = lockStderrWriter(&.{}); + stderr.writeAll("aborting due to recursive panic\n") catch {}; }, else => {}, // Panicked while printing the recursive panic message. } - posix.abort(); + std.process.abort(); } /// Must be called only after adding 1 to `panicking`. There are three callsites. @@ -1596,7 +1597,8 @@ pub fn defaultHandleSegfault(addr: ?usize, name: []const u8, opt_ctx: ?CpuContex // A segfault happened while trying to print a previous panic message. // We're still holding the mutex but that's fine as we're going to // call abort(). - File.stderr().writeAll("aborting due to recursive panic\n") catch {}; + const stderr, _ = lockStderrWriter(&.{}); + stderr().writeAll("aborting due to recursive panic\n") catch {}; }, else => {}, // Panicked while printing the recursive panic message. } @@ -1604,7 +1606,7 @@ pub fn defaultHandleSegfault(addr: ?usize, name: []const u8, opt_ctx: ?CpuContex // We cannot allow the signal handler to return because when it runs the original instruction // again, the memory may be mapped and undefined behavior would occur rather than repeating // the segfault. So we simply abort here. - posix.abort(); + std.process.abort(); } pub fn dumpStackPointerAddr(prefix: []const u8) void { diff --git a/lib/std/fs/test.zig b/lib/std/fs/test.zig index e044a97620..bc84500419 100644 --- a/lib/std/fs/test.zig +++ b/lib/std/fs/test.zig @@ -674,7 +674,7 @@ test "Dir.Iterator but dir is deleted during iteration" { var iterator = subdir.iterate(); // Create something to iterate over within the subdir - try tmp.dir.makePath("subdir" ++ fs.path.sep_str ++ "b"); + try tmp.dir.makePath(io, "subdir" ++ fs.path.sep_str ++ "b"); // Then, before iterating, delete the directory that we're iterating. // This is a contrived reproduction, but this could happen outside of the program, in another thread, etc. @@ -1196,7 +1196,7 @@ test "deleteTree does not follow symlinks" { var tmp = tmpDir(.{}); defer tmp.cleanup(); - try tmp.dir.makePath("b"); + try tmp.dir.makePath(io, "b"); { var a = try tmp.dir.makeOpenPath("a", .{}); defer a.close(io); @@ -1211,6 +1211,8 @@ test "deleteTree does not follow symlinks" { } test "deleteTree on a symlink" { + const io = testing.io; + var tmp = tmpDir(.{}); defer tmp.cleanup(); @@ -1223,7 +1225,7 @@ test "deleteTree on a symlink" { try tmp.dir.access("file", .{}); // Symlink to a directory - try tmp.dir.makePath("dir"); + try tmp.dir.makePath(io, "dir"); try setupSymlink(tmp.dir, "dir", "dirlink", .{ .is_directory = true }); try tmp.dir.deleteTree("dirlink"); @@ -1238,7 +1240,7 @@ test "makePath, put some files in it, deleteTree" { const allocator = ctx.arena.allocator(); const dir_path = try ctx.transformPath("os_test_tmp"); - try ctx.dir.makePath(try fs.path.join(allocator, &.{ "os_test_tmp", "b", "c" })); + try ctx.dir.makePath(io, try fs.path.join(allocator, &.{ "os_test_tmp", "b", "c" })); try ctx.dir.writeFile(.{ .sub_path = try fs.path.join(allocator, &.{ "os_test_tmp", "b", "c", "file.txt" }), .data = "nonsense", @@ -1261,7 +1263,7 @@ test "makePath, put some files in it, deleteTreeMinStackSize" { const allocator = ctx.arena.allocator(); const dir_path = try ctx.transformPath("os_test_tmp"); - try ctx.dir.makePath(try fs.path.join(allocator, &.{ "os_test_tmp", "b", "c" })); + try ctx.dir.makePath(io, try fs.path.join(allocator, &.{ "os_test_tmp", "b", "c" })); try ctx.dir.writeFile(.{ .sub_path = try fs.path.join(allocator, &.{ "os_test_tmp", "b", "c", "file.txt" }), .data = "nonsense", @@ -1280,21 +1282,25 @@ test "makePath, put some files in it, deleteTreeMinStackSize" { test "makePath in a directory that no longer exists" { if (native_os == .windows) return error.SkipZigTest; // Windows returns FileBusy if attempting to remove an open dir + const io = testing.io; + var tmp = tmpDir(.{}); defer tmp.cleanup(); try tmp.parent_dir.deleteTree(&tmp.sub_path); - try testing.expectError(error.FileNotFound, tmp.dir.makePath("sub-path")); + try testing.expectError(error.FileNotFound, tmp.dir.makePath(io, "sub-path")); } test "makePath but sub_path contains pre-existing file" { + const io = testing.io; + var tmp = tmpDir(.{}); defer tmp.cleanup(); try tmp.dir.makeDir("foo"); try tmp.dir.writeFile(.{ .sub_path = "foo/bar", .data = "" }); - try testing.expectError(error.NotDir, tmp.dir.makePath("foo/bar/baz")); + try testing.expectError(error.NotDir, tmp.dir.makePath(io, "foo/bar/baz")); } fn expectDir(io: Io, dir: Dir, path: []const u8) !void { @@ -1314,7 +1320,7 @@ test "makepath existing directories" { try tmpA.makeDir("B"); const testPath = "A" ++ fs.path.sep_str ++ "B" ++ fs.path.sep_str ++ "C"; - try tmp.dir.makePath(testPath); + try tmp.dir.makePath(io, testPath); try expectDir(io, tmp.dir, testPath); } @@ -1328,7 +1334,7 @@ test "makepath through existing valid symlink" { try tmp.dir.makeDir("realfolder"); try setupSymlink(tmp.dir, "." ++ fs.path.sep_str ++ "realfolder", "working-symlink", .{}); - try tmp.dir.makePath("working-symlink" ++ fs.path.sep_str ++ "in-realfolder"); + try tmp.dir.makePath(io, "working-symlink" ++ fs.path.sep_str ++ "in-realfolder"); try expectDir(io, tmp.dir, "realfolder" ++ fs.path.sep_str ++ "in-realfolder"); } @@ -1344,7 +1350,7 @@ test "makepath relative walks" { }); defer testing.allocator.free(relPath); - try tmp.dir.makePath(relPath); + try tmp.dir.makePath(io, relPath); // How .. is handled is different on Windows than non-Windows switch (native_os) { @@ -1383,7 +1389,7 @@ test "makepath ignores '.'" { }); defer testing.allocator.free(expectedPath); - try tmp.dir.makePath(dotPath); + try tmp.dir.makePath(io, dotPath); try expectDir(io, tmp.dir, expectedPath); } @@ -1550,10 +1556,11 @@ test "setEndPos" { test "access file" { try testWithAllSupportedPathTypes(struct { fn impl(ctx: *TestContext) !void { + const io = ctx.io; const dir_path = try ctx.transformPath("os_test_tmp"); const file_path = try ctx.transformPath("os_test_tmp" ++ fs.path.sep_str ++ "file.txt"); - try ctx.dir.makePath(dir_path); + try ctx.dir.makePath(io, dir_path); try testing.expectError(error.FileNotFound, ctx.dir.access(file_path, .{})); try ctx.dir.writeFile(.{ .sub_path = file_path, .data = "" }); @@ -1569,7 +1576,7 @@ test "sendfile" { var tmp = tmpDir(.{}); defer tmp.cleanup(); - try tmp.dir.makePath("os_test_tmp"); + try tmp.dir.makePath(io, "os_test_tmp"); var dir = try tmp.dir.openDir(io, "os_test_tmp", .{}); defer dir.close(io); @@ -1616,7 +1623,7 @@ test "sendfile with buffered data" { var tmp = tmpDir(.{}); defer tmp.cleanup(); - try tmp.dir.makePath("os_test_tmp"); + try tmp.dir.makePath(io, "os_test_tmp"); var dir = try tmp.dir.openDir(io, "os_test_tmp", .{}); defer dir.close(io); @@ -1894,7 +1901,7 @@ test "walker" { }); for (expected_paths.keys()) |key| { - try tmp.dir.makePath(key); + try tmp.dir.makePath(io, key); } var walker = try tmp.dir.walk(testing.allocator); @@ -1956,7 +1963,7 @@ test "selective walker, skip entries that start with ." { }); for (paths_to_create) |path| { - try tmp.dir.makePath(path); + try tmp.dir.makePath(io, path); } var walker = try tmp.dir.walkSelectively(testing.allocator); @@ -1991,6 +1998,8 @@ test "selective walker, skip entries that start with ." { } test "walker without fully iterating" { + const io = testing.io; + var tmp = tmpDir(.{ .iterate = true }); defer tmp.cleanup(); @@ -2000,8 +2009,8 @@ test "walker without fully iterating" { // Create 2 directories inside the tmp directory, but then only iterate once before breaking. // This ensures that walker doesn't try to close the initial directory when not fully iterating. - try tmp.dir.makePath("a"); - try tmp.dir.makePath("b"); + try tmp.dir.makePath(io, "a"); + try tmp.dir.makePath(io, "b"); var num_walked: usize = 0; while (try walker.next()) |_| { diff --git a/lib/std/os/linux/IoUring.zig b/lib/std/os/linux/IoUring.zig index 3c9a313a4a..72b8369d68 100644 --- a/lib/std/os/linux/IoUring.zig +++ b/lib/std/os/linux/IoUring.zig @@ -4090,32 +4090,6 @@ test "openat_direct/close_direct" { try ring.unregister_files(); } -test "waitid" { - try skipKernelLessThan(.{ .major = 6, .minor = 7, .patch = 0 }); - - var ring = IoUring.init(16, 0) catch |err| switch (err) { - error.SystemOutdated => return error.SkipZigTest, - error.PermissionDenied => return error.SkipZigTest, - else => return err, - }; - defer ring.deinit(); - - const pid = try posix.fork(); - if (pid == 0) { - posix.exit(7); - } - - var siginfo: posix.siginfo_t = undefined; - _ = try ring.waitid(0, .PID, pid, &siginfo, posix.W.EXITED, 0); - - try testing.expectEqual(1, try ring.submit()); - - const cqe_waitid = try ring.copy_cqe(); - try testing.expectEqual(0, cqe_waitid.res); - try testing.expectEqual(pid, siginfo.fields.common.first.piduid.pid); - try testing.expectEqual(7, siginfo.fields.common.second.sigchld.status); -} - /// For use in tests. Returns SkipZigTest if kernel version is less than required. inline fn skipKernelLessThan(required: std.SemanticVersion) !void { if (!is_linux) return error.SkipZigTest; diff --git a/lib/std/posix.zig b/lib/std/posix.zig index f4aa970413..42429a4993 100644 --- a/lib/std/posix.zig +++ b/lib/std/posix.zig @@ -615,66 +615,6 @@ fn getRandomBytesDevURandom(buf: []u8) GetRandomError!void { } } -/// Causes abnormal process termination. -/// If linking against libc, this calls the abort() libc function. Otherwise -/// it raises SIGABRT followed by SIGKILL and finally lo -/// Invokes the current signal handler for SIGABRT, if any. -pub fn abort() noreturn { - @branchHint(.cold); - // MSVCRT abort() sometimes opens a popup window which is undesirable, so - // even when linking libc on Windows we use our own abort implementation. - // See https://github.com/ziglang/zig/issues/2071 for more details. - if (native_os == .windows) { - if (builtin.mode == .Debug and windows.peb().BeingDebugged != 0) { - @breakpoint(); - } - windows.ntdll.RtlExitUserProcess(3); - } - if (!builtin.link_libc and native_os == .linux) { - // The Linux man page says that the libc abort() function - // "first unblocks the SIGABRT signal", but this is a footgun - // for user-defined signal handlers that want to restore some state in - // some program sections and crash in others. - // So, the user-installed SIGABRT handler is run, if present. - raise(.ABRT) catch {}; - - // Disable all signal handlers. - const filledset = linux.sigfillset(); - sigprocmask(SIG.BLOCK, &filledset, null); - - // Only one thread may proceed to the rest of abort(). - if (!builtin.single_threaded) { - const global = struct { - var abort_entered: bool = false; - }; - while (@cmpxchgWeak(bool, &global.abort_entered, false, true, .seq_cst, .seq_cst)) |_| {} - } - - // Install default handler so that the tkill below will terminate. - const sigact = Sigaction{ - .handler = .{ .handler = SIG.DFL }, - .mask = sigemptyset(), - .flags = 0, - }; - sigaction(.ABRT, &sigact, null); - - _ = linux.tkill(linux.gettid(), .ABRT); - - var sigabrtmask = sigemptyset(); - sigaddset(&sigabrtmask, .ABRT); - sigprocmask(SIG.UNBLOCK, &sigabrtmask, null); - - // Beyond this point should be unreachable. - @as(*allowzero volatile u8, @ptrFromInt(0)).* = 0; - raise(.KILL) catch {}; - exit(127); // Pid 1 might not be signalled in some containers. - } - switch (native_os) { - .uefi, .wasi, .emscripten, .cuda, .amdhsa => @trap(), - else => system.abort(), - } -} - pub const RaiseError = UnexpectedError; pub fn raise(sig: SIG) RaiseError!void { @@ -715,33 +655,6 @@ pub fn kill(pid: pid_t, sig: SIG) KillError!void { } } -/// Exits all threads of the program with the specified status code. -pub fn exit(status: u8) noreturn { - if (builtin.link_libc) { - std.c.exit(status); - } - if (native_os == .windows) { - windows.ntdll.RtlExitUserProcess(status); - } - if (native_os == .wasi) { - wasi.proc_exit(status); - } - if (native_os == .linux and !builtin.single_threaded) { - linux.exit_group(status); - } - if (native_os == .uefi) { - const uefi = std.os.uefi; - // exit() is only available if exitBootServices() has not been called yet. - // This call to exit should not fail, so we catch-ignore errors. - if (uefi.system_table.boot_services) |bs| { - bs.exit(uefi.handle, @enumFromInt(status), null) catch {}; - } - // If we can't exit, reboot the system instead. - uefi.system_table.runtime_services.resetSystem(.cold, @enumFromInt(status), null); - } - system.exit(status); -} - pub const ReadError = std.Io.File.Reader.Error; /// Returns the number of bytes that were read, which can be less than diff --git a/lib/std/posix/test.zig b/lib/std/posix/test.zig index dc63be6e14..169c5a70c2 100644 --- a/lib/std/posix/test.zig +++ b/lib/std/posix/test.zig @@ -667,73 +667,6 @@ test "writev longer than IOV_MAX" { try testing.expectEqual(@as(usize, posix.IOV_MAX), amt); } -test "POSIX file locking with fcntl" { - if (native_os == .windows or native_os == .wasi) { - // Not POSIX. - return error.SkipZigTest; - } - - if (true) { - // https://github.com/ziglang/zig/issues/11074 - return error.SkipZigTest; - } - - const io = testing.io; - - var tmp = tmpDir(.{}); - defer tmp.cleanup(); - - // Create a temporary lock file - var file = try tmp.dir.createFile(io, "lock", .{ .read = true }); - defer file.close(io); - try file.setEndPos(2); - const fd = file.handle; - - // Place an exclusive lock on the first byte, and a shared lock on the second byte: - var struct_flock = std.mem.zeroInit(posix.Flock, .{ .type = posix.F.WRLCK }); - _ = try posix.fcntl(fd, posix.F.SETLK, @intFromPtr(&struct_flock)); - struct_flock.start = 1; - struct_flock.type = posix.F.RDLCK; - _ = try posix.fcntl(fd, posix.F.SETLK, @intFromPtr(&struct_flock)); - - // Check the locks in a child process: - const pid = try posix.fork(); - if (pid == 0) { - // child expects be denied the exclusive lock: - struct_flock.start = 0; - struct_flock.type = posix.F.WRLCK; - try expectError(error.Locked, posix.fcntl(fd, posix.F.SETLK, @intFromPtr(&struct_flock))); - // child expects to get the shared lock: - struct_flock.start = 1; - struct_flock.type = posix.F.RDLCK; - _ = try posix.fcntl(fd, posix.F.SETLK, @intFromPtr(&struct_flock)); - // child waits for the exclusive lock in order to test deadlock: - struct_flock.start = 0; - struct_flock.type = posix.F.WRLCK; - _ = try posix.fcntl(fd, posix.F.SETLKW, @intFromPtr(&struct_flock)); - // child exits without continuing: - posix.exit(0); - } else { - // parent waits for child to get shared lock: - std.Thread.sleep(1 * std.time.ns_per_ms); - // parent expects deadlock when attempting to upgrade the shared lock to exclusive: - struct_flock.start = 1; - struct_flock.type = posix.F.WRLCK; - try expectError(error.DeadLock, posix.fcntl(fd, posix.F.SETLKW, @intFromPtr(&struct_flock))); - // parent releases exclusive lock: - struct_flock.start = 0; - struct_flock.type = posix.F.UNLCK; - _ = try posix.fcntl(fd, posix.F.SETLK, @intFromPtr(&struct_flock)); - // parent releases shared lock: - struct_flock.start = 1; - struct_flock.type = posix.F.UNLCK; - _ = try posix.fcntl(fd, posix.F.SETLK, @intFromPtr(&struct_flock)); - // parent waits for child: - const result = posix.waitpid(pid, 0); - try expect(result.status == 0 * 256); - } -} - test "rename smoke test" { if (native_os == .wasi) return error.SkipZigTest; if (native_os == .windows) return error.SkipZigTest; diff --git a/lib/std/process.zig b/lib/std/process.zig index 5d60190c2b..f7ecf5fdc2 100644 --- a/lib/std/process.zig +++ b/lib/std/process.zig @@ -16,8 +16,6 @@ const unicode = std.unicode; const max_path_bytes = std.fs.max_path_bytes; pub const Child = @import("process/Child.zig"); -pub const abort = posix.abort; -pub const exit = posix.exit; pub const changeCurDir = posix.chdir; pub const changeCurDirZ = posix.chdirZ; @@ -2208,3 +2206,87 @@ pub const OpenExecutableError = File.OpenError || ExecutablePathError || File.Lo pub fn openExecutable(io: Io, flags: File.OpenFlags) OpenExecutableError!File { return io.vtable.processExecutableOpen(io.userdata, flags); } + +/// Causes abnormal process termination. +/// +/// If linking against libc, this calls `std.c.abort`. Otherwise it raises +/// SIGABRT followed by SIGKILL. +/// +/// Invokes the current signal handler for SIGABRT, if any. +pub fn abort() noreturn { + @branchHint(.cold); + // MSVCRT abort() sometimes opens a popup window which is undesirable, so + // even when linking libc on Windows we use our own abort implementation. + // See https://github.com/ziglang/zig/issues/2071 for more details. + if (native_os == .windows) { + if (builtin.mode == .Debug and windows.peb().BeingDebugged != 0) { + @breakpoint(); + } + windows.ntdll.RtlExitUserProcess(3); + } + if (!builtin.link_libc and native_os == .linux) { + // The Linux man page says that the libc abort() function + // "first unblocks the SIGABRT signal", but this is a footgun + // for user-defined signal handlers that want to restore some state in + // some program sections and crash in others. + // So, the user-installed SIGABRT handler is run, if present. + posix.raise(.ABRT) catch {}; + + // Disable all signal handlers. + const filledset = std.os.linux.sigfillset(); + posix.sigprocmask(posix.SIG.BLOCK, &filledset, null); + + // Only one thread may proceed to the rest of abort(). + if (!builtin.single_threaded) { + const global = struct { + var abort_entered: bool = false; + }; + while (@cmpxchgWeak(bool, &global.abort_entered, false, true, .seq_cst, .seq_cst)) |_| {} + } + + // Install default handler so that the tkill below will terminate. + const sigact: posix.Sigaction = .{ + .handler = .{ .handler = posix.SIG.DFL }, + .mask = posix.sigemptyset(), + .flags = 0, + }; + posix.sigaction(.ABRT, &sigact, null); + + _ = std.os.linux.tkill(std.os.linux.gettid(), .ABRT); + + var sigabrtmask = posix.sigemptyset(); + posix.sigaddset(&sigabrtmask, .ABRT); + posix.sigprocmask(posix.SIG.UNBLOCK, &sigabrtmask, null); + + // Beyond this point should be unreachable. + @as(*allowzero volatile u8, @ptrFromInt(0)).* = 0; + posix.raise(.KILL) catch {}; + exit(127); // Pid 1 might not be signalled in some containers. + } + switch (native_os) { + .uefi, .wasi, .emscripten, .cuda, .amdhsa => @trap(), + else => posix.system.abort(), + } +} + +/// Exits all threads of the program with the specified status code. +pub fn exit(status: u8) noreturn { + if (builtin.link_libc) { + std.c.exit(status); + } else switch (native_os) { + .windows => windows.ntdll.RtlExitUserProcess(status), + .wasi => std.os.wasi.proc_exit(status), + .linux => if (!builtin.single_threaded) std.os.linux.exit_group(status), + .uefi => { + const uefi = std.os.uefi; + // exit() is only available if exitBootServices() has not been called yet. + // This call to exit should not fail, so we catch-ignore errors. + if (uefi.system_table.boot_services) |bs| { + bs.exit(uefi.handle, @enumFromInt(status), null) catch {}; + } + // If we can't exit, reboot the system instead. + uefi.system_table.runtime_services.resetSystem(.cold, @enumFromInt(status), null); + }, + else => posix.system.exit(status), + } +} diff --git a/lib/std/process/Child.zig b/lib/std/process/Child.zig index 33faeef061..4521cd511f 100644 --- a/lib/std/process/Child.zig +++ b/lib/std/process/Child.zig @@ -1050,7 +1050,7 @@ fn forkChildErrReport(fd: i32, err: ChildProcess.SpawnError) noreturn { // The _exit(2) function does nothing but make the exit syscall, unlike exit(3) std.c._exit(1); } - posix.exit(1); + posix.system.exit(1); } fn writeIntFd(fd: i32, value: ErrInt) !void { diff --git a/lib/std/start.zig b/lib/std/start.zig index a5bec41231..64a1c17175 100644 --- a/lib/std/start.zig +++ b/lib/std/start.zig @@ -110,7 +110,7 @@ fn main2() callconv(.c) c_int { } fn _start2() callconv(.withStackAlign(.c, 1)) noreturn { - std.posix.exit(callMain()); + std.process.exit(callMain()); } fn spirvMain2() callconv(.kernel) void { @@ -118,7 +118,7 @@ fn spirvMain2() callconv(.kernel) void { } fn wWinMainCRTStartup2() callconv(.c) noreturn { - std.posix.exit(callMain()); + std.process.exit(callMain()); } //////////////////////////////////////////////////////////////////////////////// @@ -627,7 +627,7 @@ fn posixCallMainAndExit(argc_argv_ptr: [*]usize) callconv(.c) noreturn { for (slice) |func| func(); } - std.posix.exit(callMainWithArgs(argc, argv, envp)); + std.process.exit(callMainWithArgs(argc, argv, envp)); } fn expandStackSize(phdrs: []elf.Phdr) void { diff --git a/lib/std/tar.zig b/lib/std/tar.zig index 8a0bbb342f..b2a5306458 100644 --- a/lib/std/tar.zig +++ b/lib/std/tar.zig @@ -606,7 +606,7 @@ pub fn pipeToFileSystem(io: Io, dir: Io.Dir, reader: *Io.Reader, options: PipeOp switch (file.kind) { .directory => { if (file_name.len > 0 and !options.exclude_empty_directories) { - try dir.makePath(file_name); + try dir.makePath(io, file_name); } }, .file => { @@ -625,7 +625,7 @@ pub fn pipeToFileSystem(io: Io, dir: Io.Dir, reader: *Io.Reader, options: PipeOp }, .sym_link => { const link_name = file.link_name; - createDirAndSymlink(dir, link_name, file_name) catch |err| { + createDirAndSymlink(io, dir, link_name, file_name) catch |err| { const d = options.diagnostics orelse return error.UnableToCreateSymLink; try d.errors.append(d.allocator, .{ .unable_to_create_sym_link = .{ .code = err, @@ -642,7 +642,7 @@ fn createDirAndFile(io: Io, dir: Io.Dir, file_name: []const u8, mode: Io.File.Mo const fs_file = dir.createFile(io, file_name, .{ .exclusive = true, .mode = mode }) catch |err| { if (err == error.FileNotFound) { if (std.fs.path.dirname(file_name)) |dir_name| { - try dir.makePath(dir_name); + try dir.makePath(io, dir_name); return try dir.createFile(io, file_name, .{ .exclusive = true, .mode = mode }); } } @@ -652,11 +652,11 @@ fn createDirAndFile(io: Io, dir: Io.Dir, file_name: []const u8, mode: Io.File.Mo } // Creates a symbolic link at path `file_name` which points to `link_name`. -fn createDirAndSymlink(dir: Io.Dir, link_name: []const u8, file_name: []const u8) !void { +fn createDirAndSymlink(io: Io, dir: Io.Dir, link_name: []const u8, file_name: []const u8) !void { dir.symLink(link_name, file_name, .{}) catch |err| { if (err == error.FileNotFound) { if (std.fs.path.dirname(file_name)) |dir_name| { - try dir.makePath(dir_name); + try dir.makePath(io, dir_name); return try dir.symLink(link_name, file_name, .{}); } } @@ -885,15 +885,15 @@ test "create file and symlink" { file = try createDirAndFile(io, root.dir, "a/b/c/file2", default_mode); file.close(io); - createDirAndSymlink(root.dir, "a/b/c/file2", "symlink1") catch |err| { + createDirAndSymlink(io, root.dir, "a/b/c/file2", "symlink1") catch |err| { // On Windows when developer mode is not enabled if (err == error.AccessDenied) return error.SkipZigTest; return err; }; - try createDirAndSymlink(root.dir, "../../../file1", "d/e/f/symlink2"); + try createDirAndSymlink(io, root.dir, "../../../file1", "d/e/f/symlink2"); // Danglink symlnik, file created later - try createDirAndSymlink(root.dir, "../../../g/h/i/file4", "j/k/l/symlink3"); + try createDirAndSymlink(io, root.dir, "../../../g/h/i/file4", "j/k/l/symlink3"); file = try createDirAndFile(io, root.dir, "g/h/i/file4", default_mode); file.close(io); } diff --git a/lib/std/zip.zig b/lib/std/zip.zig index 9d08847092..acb3fc65ab 100644 --- a/lib/std/zip.zig +++ b/lib/std/zip.zig @@ -464,6 +464,8 @@ pub const Iterator = struct { filename_buf: []u8, dest: Io.Dir, ) !void { + const io = stream.io; + if (filename_buf.len < self.filename_len) return error.ZipInsufficientBuffer; switch (self.compression_method) { @@ -552,12 +554,10 @@ pub const Iterator = struct { if (filename[filename.len - 1] == '/') { if (self.uncompressed_size != 0) return error.ZipBadDirectorySize; - try dest.makePath(filename[0 .. filename.len - 1]); + try dest.makePath(io, filename[0 .. filename.len - 1]); return; } - const io = stream.io; - const out_file = blk: { if (std.fs.path.dirname(filename)) |dirname| { var parent_dir = try dest.makeOpenPath(dirname, .{}); diff --git a/src/Compilation.zig b/src/Compilation.zig index 3a48705880..280d34cdbf 100644 --- a/src/Compilation.zig +++ b/src/Compilation.zig @@ -3180,7 +3180,7 @@ pub fn update(comp: *Compilation, main_progress_node: std.Progress.Node) UpdateE const s = fs.path.sep_str; const tmp_dir_sub_path = "tmp" ++ s ++ std.fmt.hex(tmp_dir_rand_int); const o_sub_path = "o" ++ s ++ hex_digest; - renameTmpIntoCache(comp.dirs.local_cache, tmp_dir_sub_path, o_sub_path) catch |err| { + renameTmpIntoCache(io, comp.dirs.local_cache, tmp_dir_sub_path, o_sub_path) catch |err| { return comp.setMiscFailure( .rename_results, "failed to rename compilation results ('{f}{s}') into local cache ('{f}{s}'): {t}", @@ -3399,17 +3399,19 @@ fn flush( /// implementation at the bottom of this function. /// This function is only called when CacheMode is `whole`. fn renameTmpIntoCache( + io: Io, cache_directory: Cache.Directory, tmp_dir_sub_path: []const u8, o_sub_path: []const u8, ) !void { var seen_eaccess = false; while (true) { - fs.rename( + Io.Dir.rename( cache_directory.handle, tmp_dir_sub_path, cache_directory.handle, o_sub_path, + io, ) catch |err| switch (err) { // On Windows, rename fails with `AccessDenied` rather than `PathAlreadyExists`. // See https://github.com/ziglang/zig/issues/8362 @@ -3427,7 +3429,7 @@ fn renameTmpIntoCache( continue; }, error.FileNotFound => { - try cache_directory.handle.makePath("o"); + try cache_directory.handle.makePath(io, "o"); continue; }, else => |e| return e, @@ -5816,7 +5818,7 @@ pub fn translateC( const o_sub_path = "o" ++ fs.path.sep_str ++ hex_digest; if (comp.verbose_cimport) log.info("renaming {s} to {s}", .{ tmp_sub_path, o_sub_path }); - try renameTmpIntoCache(comp.dirs.local_cache, tmp_sub_path, o_sub_path); + try renameTmpIntoCache(io, comp.dirs.local_cache, tmp_sub_path, o_sub_path); return .{ .digest = bin_digest, diff --git a/src/Package/Fetch.zig b/src/Package/Fetch.zig index a651107bf5..e54dcb9914 100644 --- a/src/Package/Fetch.zig +++ b/src/Package/Fetch.zig @@ -1414,6 +1414,7 @@ fn unpackGitPack(f: *Fetch, out_dir: Io.Dir, resource: *Resource.Git) anyerror!U fn recursiveDirectoryCopy(f: *Fetch, dir: Io.Dir, tmp_dir: Io.Dir) anyerror!void { const gpa = f.arena.child_allocator; + const io = f.job_queue.io; // Recursive directory copy. var it = try dir.walk(gpa); defer it.deinit(); @@ -1428,7 +1429,7 @@ fn recursiveDirectoryCopy(f: *Fetch, dir: Io.Dir, tmp_dir: Io.Dir) anyerror!void .{}, ) catch |err| switch (err) { error.FileNotFound => { - if (fs.path.dirname(entry.path)) |dirname| try tmp_dir.makePath(dirname); + if (fs.path.dirname(entry.path)) |dirname| try tmp_dir.makePath(io, dirname); try dir.copyFile(entry.path, tmp_dir, entry.path, .{}); }, else => |e| return e, @@ -1441,7 +1442,7 @@ fn recursiveDirectoryCopy(f: *Fetch, dir: Io.Dir, tmp_dir: Io.Dir) anyerror!void // the destination directory, fail with an error instead. tmp_dir.symLink(link_name, entry.path, .{}) catch |err| switch (err) { error.FileNotFound => { - if (fs.path.dirname(entry.path)) |dirname| try tmp_dir.makePath(dirname); + if (fs.path.dirname(entry.path)) |dirname| try tmp_dir.makePath(io, dirname); try tmp_dir.symLink(link_name, entry.path, .{}); }, else => |e| return e, diff --git a/src/main.zig b/src/main.zig index fe48efa2fd..a063591b64 100644 --- a/src/main.zig +++ b/src/main.zig @@ -3382,7 +3382,7 @@ fn buildOutputType( const dump_path = try std.fmt.allocPrint(arena, "tmp" ++ sep ++ "{x}-dump-stdin{s}", .{ std.crypto.random.int(u64), ext.canonicalName(target), }); - try dirs.local_cache.handle.makePath("tmp"); + try dirs.local_cache.handle.makePath(io, "tmp"); // Note that in one of the happy paths, execve() is used to switch to // clang in which case any cleanup logic that exists for this temporary @@ -4773,7 +4773,7 @@ fn cmdInit(gpa: Allocator, arena: Allocator, io: Io, args: []const []const u8) ! var ok_count: usize = 0; for (template_paths) |template_path| { - if (templates.write(arena, Io.Dir.cwd(), sanitized_root_name, template_path, fingerprint)) |_| { + if (templates.write(arena, io, Io.Dir.cwd(), sanitized_root_name, template_path, fingerprint)) |_| { std.log.info("created {s}", .{template_path}); ok_count += 1; } else |err| switch (err) { @@ -7394,20 +7394,21 @@ const Templates = struct { fn write( templates: *Templates, arena: Allocator, + io: Io, out_dir: Io.Dir, root_name: []const u8, template_path: []const u8, fingerprint: Package.Fingerprint, ) !void { if (fs.path.dirname(template_path)) |dirname| { - out_dir.makePath(dirname) catch |err| { - fatal("unable to make path '{s}': {s}", .{ dirname, @errorName(err) }); + out_dir.makePath(io, dirname) catch |err| { + fatal("unable to make path '{s}': {t}", .{ dirname, err }); }; } const max_bytes = 10 * 1024 * 1024; const contents = templates.dir.readFileAlloc(template_path, arena, .limited(max_bytes)) catch |err| { - fatal("unable to read template file '{s}': {s}", .{ template_path, @errorName(err) }); + fatal("unable to read template file '{s}': {t}", .{ template_path, err }); }; templates.buffer.clearRetainingCapacity(); try templates.buffer.ensureUnusedCapacity(contents.len); -- cgit v1.2.3 From ffcbd48a1220ce6d652ee762001d88baa385de49 Mon Sep 17 00:00:00 2001 From: Andrew Kelley Date: Tue, 9 Dec 2025 22:10:12 -0800 Subject: std: rework TTY detection and printing This commit sketches an idea for how to deal with detection of file streams as being terminals. When a File stream is a terminal, writes through the stream should have their escapes stripped unless the programmer explicitly enables terminal escapes. Furthermore, the programmer needs a convenient API for intentionally outputting escapes into the stream. In particular it should be possible to set colors that are silently discarded when the stream is not a terminal. This commit makes `Io.File.Writer` track the terminal mode in the already-existing `mode` field, making it the appropriate place to implement escape stripping. `Io.lockStderrWriter` returns a `*Io.File.Writer` with terminal detection already done by default. This is a higher-level application layer stream for writing to stderr. Meanwhile, `std.debug.lockStderrWriter` also returns a `*Io.File.Writer` but a lower-level one that is hard-coded to use a static single-threaded `std.Io.Threaded` instance. This is the same instance that is used for collecting debug information and iterating the unwind info. --- lib/std/Io.zig | 22 ++-- lib/std/Io/File/Writer.zig | 243 +++++++++++++++++++++++++++++++++++++-- lib/std/Io/Threaded.zig | 47 +++++--- lib/std/Io/tty.zig | 135 ---------------------- lib/std/Progress.zig | 5 +- lib/std/debug.zig | 226 ++++++++++++++++-------------------- lib/std/heap/debug_allocator.zig | 96 +++------------- lib/std/log.zig | 64 ++++++++--- lib/std/process.zig | 8 +- src/main.zig | 2 - 10 files changed, 448 insertions(+), 400 deletions(-) delete mode 100644 lib/std/Io/tty.zig (limited to 'lib/std/process.zig') diff --git a/lib/std/Io.zig b/lib/std/Io.zig index 833cc4ec0f..fdd813536c 100644 --- a/lib/std/Io.zig +++ b/lib/std/Io.zig @@ -82,8 +82,6 @@ pub const Limit = enum(usize) { pub const Reader = @import("Io/Reader.zig"); pub const Writer = @import("Io/Writer.zig"); -pub const tty = @import("Io/tty.zig"); - pub fn poll( gpa: Allocator, comptime StreamEnum: type, @@ -535,7 +533,6 @@ test { _ = net; _ = Reader; _ = Writer; - _ = tty; _ = Evented; _ = Threaded; _ = @import("Io/test.zig"); @@ -720,6 +717,9 @@ pub const VTable = struct { processExecutableOpen: *const fn (?*anyopaque, File.OpenFlags) std.process.OpenExecutableError!File, processExecutablePath: *const fn (?*anyopaque, buffer: []u8) std.process.ExecutablePathError!usize, + lockStderrWriter: *const fn (?*anyopaque, buffer: []u8) Cancelable!*File.Writer, + tryLockStderrWriter: *const fn (?*anyopaque, buffer: []u8) ?*File.Writer, + unlockStderrWriter: *const fn (?*anyopaque) void, now: *const fn (?*anyopaque, Clock) Clock.Error!Timestamp, sleep: *const fn (?*anyopaque, Timeout) SleepError!void, @@ -740,10 +740,6 @@ pub const VTable = struct { netInterfaceNameResolve: *const fn (?*anyopaque, *const net.Interface.Name) net.Interface.Name.ResolveError!net.Interface, netInterfaceName: *const fn (?*anyopaque, net.Interface) net.Interface.NameError!net.Interface.Name, netLookup: *const fn (?*anyopaque, net.HostName, *Queue(net.HostName.LookupResult), net.HostName.LookupOptions) net.HostName.LookupError!void, - - lockStderrWriter: *const fn (?*anyopaque, buffer: []u8) Cancelable!*Writer, - tryLockStderrWriter: *const fn (?*anyopaque, buffer: []u8) ?*Writer, - unlockStderrWriter: *const fn (?*anyopaque) void, }; pub const Cancelable = error{ @@ -2186,13 +2182,17 @@ pub fn select(io: Io, s: anytype) Cancelable!SelectUnion(@TypeOf(s)) { /// /// See also: /// * `tryLockStderrWriter` -pub fn lockStderrWriter(io: Io, buffer: []u8) Cancelable!*Writer { - return io.vtable.lockStderrWriter(io.userdata, buffer); +pub fn lockStderrWriter(io: Io, buffer: []u8) Cancelable!*File.Writer { + const result = try io.vtable.lockStderrWriter(io.userdata, buffer); + result.io = io; + return result; } /// Same as `lockStderrWriter` but uncancelable and non-blocking. -pub fn tryLockStderrWriter(io: Io, buffer: []u8) ?*Writer { - return io.vtable.tryLockStderrWriter(io.userdata, buffer); +pub fn tryLockStderrWriter(io: Io, buffer: []u8) ?*File.Writer { + const result = io.vtable.tryLockStderrWriter(io.userdata, buffer) orelse return null; + result.io = io; + return result; } pub fn unlockStderrWriter(io: Io) void { diff --git a/lib/std/Io/File/Writer.zig b/lib/std/Io/File/Writer.zig index ec58824c06..0e995908c4 100644 --- a/lib/std/Io/File/Writer.zig +++ b/lib/std/Io/File/Writer.zig @@ -1,4 +1,6 @@ const Writer = @This(); +const builtin = @import("builtin"); +const is_windows = builtin.os.tag == .windows; const std = @import("../../std.zig"); const Io = std.Io; @@ -16,7 +18,144 @@ write_file_err: ?WriteFileError = null, seek_err: ?SeekError = null, interface: Io.Writer, -pub const Mode = File.Reader.Mode; +pub const Mode = union(enum) { + /// Uses `Io.VTable.fileWriteFileStreaming` if possible. Not a terminal. + /// `setColor` does nothing. + streaming, + /// Uses `Io.VTable.fileWriteFilePositional` if possible. Not a terminal. + /// `setColor` does nothing. + positional, + /// Avoids `Io.VTable.fileWriteFileStreaming`. Not a terminal. `setColor` + /// does nothing. + streaming_simple, + /// Avoids `Io.VTable.fileWriteFilePositional`. Not a terminal. `setColor` + /// does nothing. + positional_simple, + /// It's a terminal. Writes are escaped so as to strip escape sequences. + /// Color is enabled. + terminal_escaped, + /// It's a terminal. Colors are enabled via calling + /// SetConsoleTextAttribute. Writes are not escaped. + terminal_winapi: TerminalWinapi, + /// Indicates writing cannot continue because of a seek failure. + failure, + + pub fn toStreaming(m: @This()) @This() { + return switch (m) { + .positional, .streaming => .streaming, + .positional_simple, .streaming_simple => .streaming_simple, + inline else => |_, x| x, + }; + } + + pub fn toSimple(m: @This()) @This() { + return switch (m) { + .positional, .positional_simple => .positional_simple, + .streaming, .streaming_simple => .streaming_simple, + inline else => |x| x, + }; + } + + pub fn toUnescaped(m: @This()) @This() { + return switch (m) { + .terminal_escaped => .streaming_simple, + inline else => |x| x, + }; + } + + pub const TerminalWinapi = if (!is_windows) noreturn else struct { + handle: File.Handle, + reset_attributes: u16, + }; + + /// Detect suitable TTY configuration options for the given file (commonly + /// stdout/stderr). + /// + /// Will attempt to enable ANSI escape code support if necessary/possible. + pub fn detect(io: Io, file: File, want_color: bool, fallback: Mode) Io.Cancelable!Mode { + if (!want_color) return if (try file.isTty(io)) .terminal_escaped else fallback; + + if (file.enableAnsiEscapeCodes(io)) |_| { + return .terminal_escaped; + } else |err| switch (err) { + error.Canceled => return error.Canceled, + error.NotTerminalDevice, error.Unexpected => {}, + } + + if (is_windows and file.isTty(io)) { + const windows = std.os.windows; + var info: windows.CONSOLE_SCREEN_BUFFER_INFO = undefined; + if (windows.kernel32.GetConsoleScreenBufferInfo(file.handle, &info) != windows.FALSE) { + return .{ .terminal_winapi = .{ + .handle = file.handle, + .reset_attributes = info.wAttributes, + } }; + } + return .terminal_escaped; + } + + return fallback; + } + + pub const SetColorError = std.os.windows.SetConsoleTextAttributeError || Io.Writer.Error; + + pub fn setColor(mode: Mode, io_w: *Io.Writer, color: Color) Mode.SetColorError!void { + switch (mode) { + .streaming, .positional, .streaming_simple, .positional_simple, .failure => return, + .terminal_escaped => { + const color_string = switch (color) { + .black => "\x1b[30m", + .red => "\x1b[31m", + .green => "\x1b[32m", + .yellow => "\x1b[33m", + .blue => "\x1b[34m", + .magenta => "\x1b[35m", + .cyan => "\x1b[36m", + .white => "\x1b[37m", + .bright_black => "\x1b[90m", + .bright_red => "\x1b[91m", + .bright_green => "\x1b[92m", + .bright_yellow => "\x1b[93m", + .bright_blue => "\x1b[94m", + .bright_magenta => "\x1b[95m", + .bright_cyan => "\x1b[96m", + .bright_white => "\x1b[97m", + .bold => "\x1b[1m", + .dim => "\x1b[2m", + .reset => "\x1b[0m", + }; + try io_w.writeAll(color_string); + }, + .terminal_winapi => |ctx| { + const windows = std.os.windows; + const attributes: windows.WORD = switch (color) { + .black => 0, + .red => windows.FOREGROUND_RED, + .green => windows.FOREGROUND_GREEN, + .yellow => windows.FOREGROUND_RED | windows.FOREGROUND_GREEN, + .blue => windows.FOREGROUND_BLUE, + .magenta => windows.FOREGROUND_RED | windows.FOREGROUND_BLUE, + .cyan => windows.FOREGROUND_GREEN | windows.FOREGROUND_BLUE, + .white => windows.FOREGROUND_RED | windows.FOREGROUND_GREEN | windows.FOREGROUND_BLUE, + .bright_black => windows.FOREGROUND_INTENSITY, + .bright_red => windows.FOREGROUND_RED | windows.FOREGROUND_INTENSITY, + .bright_green => windows.FOREGROUND_GREEN | windows.FOREGROUND_INTENSITY, + .bright_yellow => windows.FOREGROUND_RED | windows.FOREGROUND_GREEN | windows.FOREGROUND_INTENSITY, + .bright_blue => windows.FOREGROUND_BLUE | windows.FOREGROUND_INTENSITY, + .bright_magenta => windows.FOREGROUND_RED | windows.FOREGROUND_BLUE | windows.FOREGROUND_INTENSITY, + .bright_cyan => windows.FOREGROUND_GREEN | windows.FOREGROUND_BLUE | windows.FOREGROUND_INTENSITY, + .bright_white, .bold => windows.FOREGROUND_RED | windows.FOREGROUND_GREEN | windows.FOREGROUND_BLUE | windows.FOREGROUND_INTENSITY, + // "dim" is not supported using basic character attributes, but let's still make it do *something*. + // This matches the old behavior of TTY.Color before the bright variants were added. + .dim => windows.FOREGROUND_INTENSITY, + .reset => ctx.reset_attributes, + }; + try io_w.flush(); + try windows.SetConsoleTextAttribute(ctx.handle, attributes); + }, + } + } +}; pub const Error = error{ DiskQuota, @@ -74,6 +213,16 @@ pub fn initStreaming(file: File, io: Io, buffer: []u8) Writer { }; } +/// Detects if `file` is terminal and sets the mode accordingly. +pub fn initDetect(file: File, io: Io, buffer: []u8) Io.Cancelable!Writer { + return .{ + .io = io, + .file = file, + .interface = initInterface(buffer), + .mode = try .detect(io, file, true, .positional), + }; +} + pub fn initInterface(buffer: []u8) Io.Writer { return .{ .vtable = &.{ @@ -99,8 +248,9 @@ pub fn moveToReader(w: *Writer) File.Reader { pub fn drain(io_w: *Io.Writer, data: []const []const u8, splat: usize) Io.Writer.Error!usize { const w: *Writer = @alignCast(@fieldParentPtr("interface", io_w)); switch (w.mode) { - .positional, .positional_reading => return drainPositional(w, data, splat), - .streaming, .streaming_reading => return drainStreaming(w, data, splat), + .positional, .positional_simple => return drainPositional(w, data, splat), + .streaming, .streaming_simple, .terminal_winapi => return drainStreaming(w, data, splat), + .terminal_escaped => return drainEscaping(w, data, splat), .failure => return error.WriteFailed, } } @@ -141,13 +291,38 @@ fn drainStreaming(w: *Writer, data: []const []const u8, splat: usize) Io.Writer. return w.interface.consume(n); } +fn findTerminalEscape(buffer: []const u8) ?usize { + return std.mem.findScalar(u8, buffer, 0x1b); +} + +fn drainEscaping(w: *Writer, data: []const []const u8, splat: usize) Io.Writer.Error!usize { + const io = w.io; + const header = w.interface.buffered(); + if (findTerminalEscape(header)) |i| { + _ = i; + @panic("TODO strip terminal escape sequence"); + } + for (data) |d| { + if (findTerminalEscape(d)) |i| { + _ = i; + @panic("TODO strip terminal escape sequence"); + } + } + const n = io.vtable.fileWriteStreaming(io.userdata, w.file, header, data, splat) catch |err| { + w.err = err; + return error.WriteFailed; + }; + w.pos += n; + return w.interface.consume(n); +} + pub fn sendFile(io_w: *Io.Writer, file_reader: *Io.File.Reader, limit: Io.Limit) Io.Writer.FileError!usize { const w: *Writer = @alignCast(@fieldParentPtr("interface", io_w)); switch (w.mode) { .positional => return sendFilePositional(w, file_reader, limit), - .positional_reading => return error.Unimplemented, + .positional_simple => return error.Unimplemented, .streaming => return sendFileStreaming(w, file_reader, limit), - .streaming_reading => return error.Unimplemented, + .streaming_simple, .terminal_escaped, .terminal_winapi => return error.Unimplemented, .failure => return error.WriteFailed, } } @@ -214,10 +389,10 @@ pub fn seekToUnbuffered(w: *Writer, offset: u64) SeekError!void { assert(w.interface.buffered().len == 0); const io = w.io; switch (w.mode) { - .positional, .positional_reading => { + .positional, .positional_simple => { w.pos = offset; }, - .streaming, .streaming_reading => { + .streaming, .streaming_simple, .terminal_escaped, .terminal_winapi => { if (w.seek_err) |err| return err; io.vtable.fileSeekTo(io.userdata, w.file, offset) catch |err| { w.seek_err = err; @@ -243,15 +418,65 @@ pub fn end(w: *Writer) EndError!void { try w.interface.flush(); switch (w.mode) { .positional, - .positional_reading, + .positional_simple, => w.file.setLength(io, w.pos) catch |err| switch (err) { error.NonResizable => return, else => |e| return e, }, .streaming, - .streaming_reading, + .streaming_simple, .failure, => {}, } } + +pub const Color = enum { + black, + red, + green, + yellow, + blue, + magenta, + cyan, + white, + bright_black, + bright_red, + bright_green, + bright_yellow, + bright_blue, + bright_magenta, + bright_cyan, + bright_white, + dim, + bold, + reset, +}; + +pub const SetColorError = Mode.SetColorError; + +pub fn setColor(w: *Writer, color: Color) SetColorError!void { + return w.mode.setColor(&w.interface, color); +} + +pub fn disableEscape(w: *Writer) Mode { + const prev = w.mode; + w.mode = w.mode.toUnescaped(); + return prev; +} + +pub fn restoreEscape(w: *Writer, mode: Mode) void { + w.mode = mode; +} + +pub fn writeAllUnescaped(w: *Writer, bytes: []const u8) Io.Error!void { + const prev_mode = w.disableEscape(); + defer w.restoreEscape(prev_mode); + return w.interface.writeAll(bytes); +} + +pub fn printUnescaped(w: *Writer, comptime fmt: []const u8, args: anytype) Io.Error!void { + const prev_mode = w.disableEscape(); + defer w.restoreEscape(prev_mode); + return w.interface.print(fmt, args); +} diff --git a/lib/std/Io/Threaded.zig b/lib/std/Io/Threaded.zig index 9d6d9f979e..c2a3e15f15 100644 --- a/lib/std/Io/Threaded.zig +++ b/lib/std/Io/Threaded.zig @@ -77,7 +77,13 @@ use_sendfile: UseSendfile = .default, use_copy_file_range: UseCopyFileRange = .default, use_fcopyfile: UseFcopyfile = .default, -stderr_writer: Io.Writer, +stderr_writer: File.Writer = .{ + .io = undefined, + .interface = Io.File.Writer.initInterface(&.{}), + .file = if (is_windows) undefined else .stderr(), + .mode = undefined, +}, +stderr_writer_initialized: bool = false, pub const RobustCancel = if (std.Thread.use_pthreads or native_os == .linux) enum { enabled, @@ -737,6 +743,9 @@ pub fn io(t: *Threaded) Io { .processExecutableOpen = processExecutableOpen, .processExecutablePath = processExecutablePath, + .lockStderrWriter = lockStderrWriter, + .tryLockStderrWriter = tryLockStderrWriter, + .unlockStderrWriter = unlockStderrWriter, .now = now, .sleep = sleep, @@ -864,6 +873,9 @@ pub fn ioBasic(t: *Threaded) Io { .processExecutableOpen = processExecutableOpen, .processExecutablePath = processExecutablePath, + .lockStderrWriter = lockStderrWriter, + .tryLockStderrWriter = tryLockStderrWriter, + .unlockStderrWriter = unlockStderrWriter, .now = now, .sleep = sleep, @@ -9516,33 +9528,42 @@ fn netLookupFallible( return error.OptionUnsupported; } -fn lockStderrWriter(userdata: ?*anyopaque, buffer: []u8) Io.Cancelable!*Io.Writer { +fn lockStderrWriter(userdata: ?*anyopaque, buffer: []u8) Io.Cancelable!*File.Writer { const t: *Threaded = @ptrCast(@alignCast(userdata)); // Only global mutex since this is Threaded. Io.stderr_thread_mutex.lock(); - if (is_windows) t.stderr_writer.file = .stderr(); + if (!t.stderr_writer_initialized) { + if (is_windows) t.stderr_writer.file = .stderr(); + t.stderr_writer.mode = try .detect(ioBasic(t), t.stderr_writer.file, true, .streaming_simple); + t.stderr_writer_initialized = true; + } std.Progress.clearWrittenWithEscapeCodes(&t.stderr_writer) catch {}; - t.stderr_writer.flush() catch {}; - t.stderr_writer.buffer = buffer; + t.stderr_writer.interface.flush() catch {}; + t.stderr_writer.interface.buffer = buffer; return &t.stderr_writer; } -fn tryLockStderrWriter(userdata: ?*anyopaque, buffer: []u8) ?*Io.Writer { +fn tryLockStderrWriter(userdata: ?*anyopaque, buffer: []u8) ?*File.Writer { const t: *Threaded = @ptrCast(@alignCast(userdata)); // Only global mutex since this is Threaded. if (!Io.stderr_thread_mutex.tryLock()) return null; - std.Progress.clearWrittenWithEscapeCodes(t.io()) catch {}; - if (is_windows) t.stderr_writer.file = .stderr(); - t.stderr_writer.flush() catch {}; - t.stderr_writer.buffer = buffer; + if (!t.stderr_writer_initialized) { + if (is_windows) t.stderr_writer.file = .stderr(); + t.stderr_writer.mode = File.Writer.Mode.detect(ioBasic(t), t.stderr_writer.file, true, .streaming_simple) catch + return null; + t.stderr_writer_initialized = true; + } + std.Progress.clearWrittenWithEscapeCodes(&t.stderr_writer) catch {}; + t.stderr_writer.interface.flush() catch {}; + t.stderr_writer.interface.buffer = buffer; return &t.stderr_writer; } fn unlockStderrWriter(userdata: ?*anyopaque) void { const t: *Threaded = @ptrCast(@alignCast(userdata)); - t.stderr_writer.flush() catch {}; - t.stderr_writer.end = 0; - t.stderr_writer.buffer = &.{}; + t.stderr_writer.interface.flush() catch {}; + t.stderr_writer.interface.end = 0; + t.stderr_writer.interface.buffer = &.{}; Io.stderr_thread_mutex.unlock(); } diff --git a/lib/std/Io/tty.zig b/lib/std/Io/tty.zig deleted file mode 100644 index 65f1b7dad5..0000000000 --- a/lib/std/Io/tty.zig +++ /dev/null @@ -1,135 +0,0 @@ -const builtin = @import("builtin"); -const native_os = builtin.os.tag; - -const std = @import("std"); -const Io = std.Io; -const File = std.Io.File; -const process = std.process; -const windows = std.os.windows; - -pub const Color = enum { - black, - red, - green, - yellow, - blue, - magenta, - cyan, - white, - bright_black, - bright_red, - bright_green, - bright_yellow, - bright_blue, - bright_magenta, - bright_cyan, - bright_white, - dim, - bold, - reset, -}; - -/// Provides simple functionality for manipulating the terminal in some way, -/// such as coloring text, etc. -pub const Config = union(enum) { - no_color, - escape_codes, - windows_api: if (native_os == .windows) WindowsContext else noreturn, - - /// Detect suitable TTY configuration options for the given file (commonly stdout/stderr). - /// This includes feature checks for ANSI escape codes and the Windows console API, as well as - /// respecting the `NO_COLOR` and `CLICOLOR_FORCE` environment variables to override the default. - /// Will attempt to enable ANSI escape code support if necessary/possible. - pub fn detect(io: Io, file: File) Config { - const force_color: ?bool = if (builtin.os.tag == .wasi) - null // wasi does not support environment variables - else if (process.hasNonEmptyEnvVarConstant("NO_COLOR")) - false - else if (process.hasNonEmptyEnvVarConstant("CLICOLOR_FORCE")) - true - else - null; - - if (force_color == false) return .no_color; - - if (file.enableAnsiEscapeCodes(io)) |_| { - return .escape_codes; - } else |_| {} - - if (native_os == .windows and file.isTty()) { - var info: windows.CONSOLE_SCREEN_BUFFER_INFO = undefined; - if (windows.kernel32.GetConsoleScreenBufferInfo(file.handle, &info) == windows.FALSE) { - return if (force_color == true) .escape_codes else .no_color; - } - return .{ .windows_api = .{ - .handle = file.handle, - .reset_attributes = info.wAttributes, - } }; - } - - return if (force_color == true) .escape_codes else .no_color; - } - - pub const WindowsContext = struct { - handle: File.Handle, - reset_attributes: u16, - }; - - pub const SetColorError = std.os.windows.SetConsoleTextAttributeError || Io.Writer.Error; - - pub fn setColor(conf: Config, w: *Io.Writer, color: Color) SetColorError!void { - nosuspend switch (conf) { - .no_color => return, - .escape_codes => { - const color_string = switch (color) { - .black => "\x1b[30m", - .red => "\x1b[31m", - .green => "\x1b[32m", - .yellow => "\x1b[33m", - .blue => "\x1b[34m", - .magenta => "\x1b[35m", - .cyan => "\x1b[36m", - .white => "\x1b[37m", - .bright_black => "\x1b[90m", - .bright_red => "\x1b[91m", - .bright_green => "\x1b[92m", - .bright_yellow => "\x1b[93m", - .bright_blue => "\x1b[94m", - .bright_magenta => "\x1b[95m", - .bright_cyan => "\x1b[96m", - .bright_white => "\x1b[97m", - .bold => "\x1b[1m", - .dim => "\x1b[2m", - .reset => "\x1b[0m", - }; - try w.writeAll(color_string); - }, - .windows_api => |ctx| { - const attributes = switch (color) { - .black => 0, - .red => windows.FOREGROUND_RED, - .green => windows.FOREGROUND_GREEN, - .yellow => windows.FOREGROUND_RED | windows.FOREGROUND_GREEN, - .blue => windows.FOREGROUND_BLUE, - .magenta => windows.FOREGROUND_RED | windows.FOREGROUND_BLUE, - .cyan => windows.FOREGROUND_GREEN | windows.FOREGROUND_BLUE, - .white => windows.FOREGROUND_RED | windows.FOREGROUND_GREEN | windows.FOREGROUND_BLUE, - .bright_black => windows.FOREGROUND_INTENSITY, - .bright_red => windows.FOREGROUND_RED | windows.FOREGROUND_INTENSITY, - .bright_green => windows.FOREGROUND_GREEN | windows.FOREGROUND_INTENSITY, - .bright_yellow => windows.FOREGROUND_RED | windows.FOREGROUND_GREEN | windows.FOREGROUND_INTENSITY, - .bright_blue => windows.FOREGROUND_BLUE | windows.FOREGROUND_INTENSITY, - .bright_magenta => windows.FOREGROUND_RED | windows.FOREGROUND_BLUE | windows.FOREGROUND_INTENSITY, - .bright_cyan => windows.FOREGROUND_GREEN | windows.FOREGROUND_BLUE | windows.FOREGROUND_INTENSITY, - .bright_white, .bold => windows.FOREGROUND_RED | windows.FOREGROUND_GREEN | windows.FOREGROUND_BLUE | windows.FOREGROUND_INTENSITY, - // "dim" is not supported using basic character attributes, but let's still make it do *something*. - // This matches the old behavior of TTY.Color before the bright variants were added. - .dim => windows.FOREGROUND_INTENSITY, - .reset => ctx.reset_attributes, - }; - try w.flush(); - try windows.SetConsoleTextAttribute(ctx.handle, attributes); - }, - }; - } -}; diff --git a/lib/std/Progress.zig b/lib/std/Progress.zig index 5528591620..799e3f9c9b 100644 --- a/lib/std/Progress.zig +++ b/lib/std/Progress.zig @@ -755,10 +755,9 @@ fn appendTreeSymbol(symbol: TreeSymbol, buf: []u8, start_i: usize) usize { } } -fn clearWrittenWithEscapeCodes(w: *Io.Writer) anyerror!void { +pub fn clearWrittenWithEscapeCodes(file_writer: *Io.File.Writer) anyerror!void { if (noop_impl or !global_progress.need_clear) return; - - try w.writeAll(clear ++ progress_remove); + try file_writer.interface.writeAllUnescaped(clear ++ progress_remove); global_progress.need_clear = false; } diff --git a/lib/std/debug.zig b/lib/std/debug.zig index 7e10c5af32..347dbf85ec 100644 --- a/lib/std/debug.zig +++ b/lib/std/debug.zig @@ -1,7 +1,6 @@ const std = @import("std.zig"); const Io = std.Io; const Writer = std.Io.Writer; -const tty = std.Io.tty; const math = std.math; const mem = std.mem; const posix = std.posix; @@ -262,6 +261,10 @@ pub const sys_can_stack_trace = switch (builtin.cpu.arch) { else => true, }; +/// This is used for debug information and debug printing. It is intentionally +/// separate from the application's `Io` instance. +var static_single_threaded_io: Io.Threaded = .init_single_threaded; + /// Allows the caller to freely write to stderr until `unlockStderrWriter` is called. /// /// During the lock, any `std.Progress` information is cleared from the terminal. @@ -279,18 +282,12 @@ pub const sys_can_stack_trace = switch (builtin.cpu.arch) { /// /// Alternatively, use the higher-level `Io.lockStderrWriter` to integrate with /// the application's chosen `Io` implementation. -pub fn lockStderrWriter(buffer: []u8) struct { *Writer, tty.Config } { - Io.stderr_thread_mutex.lock(); - const w = std.Progress.lockStderrWriter(buffer); - // The stderr lock also locks access to `global.conf`. - if (StderrWriter.singleton.tty_config == null) { - StderrWriter.singleton.tty_config = .detect(io, .stderr()); - } - return .{ w, global.conf.? }; +pub fn lockStderrWriter(buffer: []u8) *File.Writer { + return static_single_threaded_io.ioBasic().lockStderrWriter(buffer) catch unreachable; } pub fn unlockStderrWriter() void { - std.Progress.unlockStderrWriter(); + static_single_threaded_io.ioBasic().unlockStderrWriter(); } /// Writes to stderr, ignoring errors. @@ -305,39 +302,13 @@ pub fn unlockStderrWriter() void { /// Alternatively, use the higher-level `std.log` or `Io.lockStderrWriter` to /// integrate with the application's chosen `Io` implementation. pub fn print(comptime fmt: []const u8, args: anytype) void { - var buffer: [64]u8 = undefined; - const bw, _ = lockStderrWriter(&buffer); - defer unlockStderrWriter(); - nosuspend bw.print(fmt, args) catch return; -} - -const StderrWriter = struct { - interface: Writer, - tty_config: ?tty.Config, - - var singleton: StderrWriter = .{ - .interface = .{ - .buffer = &.{}, - .vtable = &.{ .drain = drain }, - }, - .tty_config = null, - }; - - fn drain(io_w: *Writer, data: []const []const u8, splat: usize) Writer.Error!usize { - const w: *Writer = @alignCast(@fieldParentPtr("interface", io_w)); - var n: usize = 0; - const header = w.interface.buffered(); - if (header.len != 0) n += try std.Io.Threaded.debugWrite(header); - for (data[0 .. data.len - 1]) |d| { - if (d.len != 0) n += try std.Io.Threaded.debugWrite(d); - } - const pattern = data[data.len - 1]; - if (pattern.len != 0) { - for (0..splat) |_| n += try std.Io.Threaded.debugWrite(pattern); - } - return io_w.consume(n); + nosuspend { + var buffer: [64]u8 = undefined; + const stderr = lockStderrWriter(&buffer); + defer unlockStderrWriter(); + stderr.interface.print(fmt, args) catch return; } -}; +} /// Marked `inline` to propagate a comptime-known error to callers. pub inline fn getSelfDebugInfo() !*SelfInfo { @@ -357,16 +328,16 @@ pub fn dumpHex(bytes: []const u8) void { } /// Prints a hexadecimal view of the bytes, returning any error that occurs. -pub fn dumpHexFallible(bw: *Writer, tty_config: tty.Config, bytes: []const u8) !void { +pub fn dumpHexFallible(bw: *Writer, fwm: File.Writer.Mode, bytes: []const u8) !void { var chunks = mem.window(u8, bytes, 16, 16); while (chunks.next()) |window| { // 1. Print the address. const address = (@intFromPtr(bytes.ptr) + 0x10 * (std.math.divCeil(usize, chunks.index orelse bytes.len, 16) catch unreachable)) - 0x10; - try tty_config.setColor(bw, .dim); + try fwm.setColor(bw, .dim); // We print the address in lowercase and the bytes in uppercase hexadecimal to distinguish them more. // Also, make sure all lines are aligned by padding the address. try bw.print("{x:0>[1]} ", .{ address, @sizeOf(usize) * 2 }); - try tty_config.setColor(bw, .reset); + try fwm.setColor(bw, .reset); // 2. Print the bytes. for (window, 0..) |byte, index| { @@ -386,7 +357,7 @@ pub fn dumpHexFallible(bw: *Writer, tty_config: tty.Config, bytes: []const u8) ! try bw.writeByte(byte); } else { // Related: https://github.com/ziglang/zig/issues/7600 - if (tty_config == .windows_api) { + if (fwm == .terminal_winapi) { try bw.writeByte('.'); continue; } @@ -408,11 +379,11 @@ pub fn dumpHexFallible(bw: *Writer, tty_config: tty.Config, bytes: []const u8) ! test dumpHexFallible { const bytes: []const u8 = &.{ 0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff, 0x01, 0x12, 0x13 }; - var aw: Writer.Allocating = .init(std.testing.allocator); + var aw: Writer.Allocating = .init(testing.allocator); defer aw.deinit(); try dumpHexFallible(&aw.writer, .no_color, bytes); - const expected = try std.fmt.allocPrint(std.testing.allocator, + const expected = try std.fmt.allocPrint(testing.allocator, \\{x:0>[2]} 00 11 22 33 44 55 66 77 88 99 AA BB CC DD EE FF .."3DUfw........ \\{x:0>[2]} 01 12 13 ... \\ @@ -421,8 +392,8 @@ test dumpHexFallible { @intFromPtr(bytes.ptr) + 16, @sizeOf(usize) * 2, }); - defer std.testing.allocator.free(expected); - try std.testing.expectEqualStrings(expected, aw.written()); + defer testing.allocator.free(expected); + try testing.expectEqualStrings(expected, aw.written()); } /// The pointer through which a `cpu_context.Native` is received from callers of stack tracing logic. @@ -437,7 +408,7 @@ pub const CpuContextPtr = if (cpu_context.Native == noreturn) noreturn else *con /// away, and in fact the optimizer is able to use the assertion in its /// heuristics. /// -/// Inside a test block, it is best to use the `std.testing` module rather than +/// Inside a test block, it is best to use the `testing` module rather than /// this function, because this function may not detect a test failure in /// ReleaseFast and ReleaseSmall mode. Outside of a test block, this assert /// function is the correct function to use. @@ -574,26 +545,26 @@ pub fn defaultPanic( _ = panicking.fetchAdd(1, .seq_cst); trace: { - const stderr, const tty_config = lockStderrWriter(&.{}); + const stderr = lockStderrWriter(&.{}); defer unlockStderrWriter(); if (builtin.single_threaded) { - stderr.print("panic: ", .{}) catch break :trace; + stderr.interface.print("panic: ", .{}) catch break :trace; } else { const current_thread_id = std.Thread.getCurrentId(); - stderr.print("thread {d} panic: ", .{current_thread_id}) catch break :trace; + stderr.interface.print("thread {d} panic: ", .{current_thread_id}) catch break :trace; } - stderr.print("{s}\n", .{msg}) catch break :trace; + stderr.interface.print("{s}\n", .{msg}) catch break :trace; if (@errorReturnTrace()) |t| if (t.index > 0) { - stderr.writeAll("error return context:\n") catch break :trace; - writeStackTrace(t, stderr, tty_config) catch break :trace; - stderr.writeAll("\nstack trace:\n") catch break :trace; + stderr.interface.writeAll("error return context:\n") catch break :trace; + writeStackTrace(t, &stderr.interface, stderr.mode) catch break :trace; + stderr.interface.writeAll("\nstack trace:\n") catch break :trace; }; writeCurrentStackTrace(.{ .first_address = first_trace_addr orelse @returnAddress(), .allow_unsafe_unwind = true, // we're crashing anyway, give it our all! - }, stderr, tty_config) catch break :trace; + }, &stderr.interface, stderr.mode) catch break :trace; } waitForOtherThreadToFinishPanicking(); @@ -603,8 +574,8 @@ pub fn defaultPanic( // A panic happened while trying to print a previous panic message. // We're still holding the mutex but that's fine as we're going to // call abort(). - const stderr, _ = lockStderrWriter(&.{}); - stderr.writeAll("aborting due to recursive panic\n") catch {}; + const stderr = lockStderrWriter(&.{}); + stderr.interface.writeAll("aborting due to recursive panic\n") catch {}; }, else => {}, // Panicked while printing the recursive panic message. } @@ -651,8 +622,7 @@ pub noinline fn captureCurrentStackTrace(options: StackUnwindOptions, addr_buf: defer it.deinit(); if (!it.stratOk(options.allow_unsafe_unwind)) return empty_trace; - var threaded: Io.Threaded = .init_single_threaded; - const io = threaded.ioBasic(); + const io = static_single_threaded_io.ioBasic(); var total_frames: usize = 0; var index: usize = 0; @@ -686,36 +656,34 @@ pub noinline fn captureCurrentStackTrace(options: StackUnwindOptions, addr_buf: /// Write the current stack trace to `writer`, annotated with source locations. /// /// See `captureCurrentStackTrace` to capture the trace addresses into a buffer instead of printing. -pub noinline fn writeCurrentStackTrace(options: StackUnwindOptions, writer: *Writer, tty_config: tty.Config) Writer.Error!void { - var threaded: Io.Threaded = .init_single_threaded; - const io = threaded.ioBasic(); - +pub noinline fn writeCurrentStackTrace(options: StackUnwindOptions, writer: *Writer, fwm: File.Writer.Mode) Writer.Error!void { if (!std.options.allow_stack_tracing) { - tty_config.setColor(writer, .dim) catch {}; + fwm.setColor(writer, .dim) catch {}; try writer.print("Cannot print stack trace: stack tracing is disabled\n", .{}); - tty_config.setColor(writer, .reset) catch {}; + fwm.setColor(writer, .reset) catch {}; return; } const di_gpa = getDebugInfoAllocator(); const di = getSelfDebugInfo() catch |err| switch (err) { error.UnsupportedTarget => { - tty_config.setColor(writer, .dim) catch {}; + fwm.setColor(writer, .dim) catch {}; try writer.print("Cannot print stack trace: debug info unavailable for target\n", .{}); - tty_config.setColor(writer, .reset) catch {}; + fwm.setColor(writer, .reset) catch {}; return; }, }; var it: StackIterator = .init(options.context); defer it.deinit(); if (!it.stratOk(options.allow_unsafe_unwind)) { - tty_config.setColor(writer, .dim) catch {}; + fwm.setColor(writer, .dim) catch {}; try writer.print("Cannot print stack trace: safe unwind unavailable for target\n", .{}); - tty_config.setColor(writer, .reset) catch {}; + fwm.setColor(writer, .reset) catch {}; return; } var total_frames: usize = 0; var wait_for = options.first_address; var printed_any_frame = false; + const io = static_single_threaded_io.ioBasic(); while (true) switch (it.next(io)) { .switch_to_fp => |unwind_error| { switch (StackIterator.fp_usability) { @@ -733,31 +701,31 @@ pub noinline fn writeCurrentStackTrace(options: StackUnwindOptions, writer: *Wri error.Unexpected => "unexpected error", }; if (it.stratOk(options.allow_unsafe_unwind)) { - tty_config.setColor(writer, .dim) catch {}; + fwm.setColor(writer, .dim) catch {}; try writer.print( "Unwind error at address `{s}:0x{x}` ({s}), remaining frames may be incorrect\n", .{ module_name, unwind_error.address, caption }, ); - tty_config.setColor(writer, .reset) catch {}; + fwm.setColor(writer, .reset) catch {}; } else { - tty_config.setColor(writer, .dim) catch {}; + fwm.setColor(writer, .dim) catch {}; try writer.print( "Unwind error at address `{s}:0x{x}` ({s}), stopping trace early\n", .{ module_name, unwind_error.address, caption }, ); - tty_config.setColor(writer, .reset) catch {}; + fwm.setColor(writer, .reset) catch {}; return; } }, .end => break, .frame => |ret_addr| { if (total_frames > 10_000) { - tty_config.setColor(writer, .dim) catch {}; + fwm.setColor(writer, .dim) catch {}; try writer.print( "Stopping trace after {d} frames (large frame count may indicate broken debug info)\n", .{total_frames}, ); - tty_config.setColor(writer, .reset) catch {}; + fwm.setColor(writer, .reset) catch {}; return; } total_frames += 1; @@ -767,7 +735,7 @@ pub noinline fn writeCurrentStackTrace(options: StackUnwindOptions, writer: *Wri } // `ret_addr` is the return address, which is *after* the function call. // Subtract 1 to get an address *in* the function call for a better source location. - try printSourceAtAddress(di_gpa, io, di, writer, ret_addr -| StackIterator.ra_call_offset, tty_config); + try printSourceAtAddress(di_gpa, io, di, writer, ret_addr -| StackIterator.ra_call_offset, fwm); printed_any_frame = true; }, }; @@ -775,7 +743,7 @@ pub noinline fn writeCurrentStackTrace(options: StackUnwindOptions, writer: *Wri } /// A thin wrapper around `writeCurrentStackTrace` which writes to stderr and ignores write errors. pub fn dumpCurrentStackTrace(options: StackUnwindOptions) void { - const stderr, const tty_config = lockStderrWriter(&.{}); + const stderr = lockStderrWriter(&.{}); defer unlockStderrWriter(); writeCurrentStackTrace(.{ .first_address = a: { @@ -785,33 +753,40 @@ pub fn dumpCurrentStackTrace(options: StackUnwindOptions) void { }, .context = options.context, .allow_unsafe_unwind = options.allow_unsafe_unwind, - }, stderr, tty_config) catch |err| switch (err) { + }, &stderr.interface, stderr.mode) catch |err| switch (err) { error.WriteFailed => {}, }; } pub const FormatStackTrace = struct { stack_trace: StackTrace, - tty_config: tty.Config, - pub fn format(context: @This(), writer: *Writer) Writer.Error!void { - try writer.writeAll("\n"); - try writeStackTrace(&context.stack_trace, writer, context.tty_config); + pub const Decorated = struct { + stack_trace: StackTrace, + file_writer_mode: File.Writer.Mode, + + pub fn format(decorated: Decorated, writer: *Writer) Writer.Error!void { + try writer.writeByte('\n'); + try writeStackTrace(&decorated.stack_trace, writer, decorated.file_writer_mode); + } + }; + + pub fn format(context: FormatStackTrace, writer: *Writer) Writer.Error!void { + return Decorated.format(.{ + .stack_trace = context.stack_trace, + .file_writer_mode = .streaming, + }, writer); } }; /// Write a previously captured stack trace to `writer`, annotated with source locations. -pub fn writeStackTrace(st: *const StackTrace, writer: *Writer, tty_config: tty.Config) Writer.Error!void { +pub fn writeStackTrace(st: *const StackTrace, writer: *Writer, fwm: File.Writer.Mode) Writer.Error!void { if (!std.options.allow_stack_tracing) { - tty_config.setColor(writer, .dim) catch {}; + fwm.setColor(writer, .dim) catch {}; try writer.print("Cannot print stack trace: stack tracing is disabled\n", .{}); - tty_config.setColor(writer, .reset) catch {}; + fwm.setColor(writer, .reset) catch {}; return; } - // We use an independent Io implementation here in case there was a problem - // with the application's Io implementation itself. - var threaded: Io.Threaded = .init_single_threaded; - const io = threaded.ioBasic(); // Fetch `st.index` straight away. Aside from avoiding redundant loads, this prevents issues if // `st` is `@errorReturnTrace()` and errors are encountered while writing the stack trace. @@ -820,22 +795,23 @@ pub fn writeStackTrace(st: *const StackTrace, writer: *Writer, tty_config: tty.C const di_gpa = getDebugInfoAllocator(); const di = getSelfDebugInfo() catch |err| switch (err) { error.UnsupportedTarget => { - tty_config.setColor(writer, .dim) catch {}; + fwm.setColor(writer, .dim) catch {}; try writer.print("Cannot print stack trace: debug info unavailable for target\n\n", .{}); - tty_config.setColor(writer, .reset) catch {}; + fwm.setColor(writer, .reset) catch {}; return; }, }; + const io = static_single_threaded_io.ioBasic(); const captured_frames = @min(n_frames, st.instruction_addresses.len); for (st.instruction_addresses[0..captured_frames]) |ret_addr| { // `ret_addr` is the return address, which is *after* the function call. // Subtract 1 to get an address *in* the function call for a better source location. - try printSourceAtAddress(di_gpa, io, di, writer, ret_addr -| StackIterator.ra_call_offset, tty_config); + try printSourceAtAddress(di_gpa, io, di, writer, ret_addr -| StackIterator.ra_call_offset, fwm); } if (n_frames > captured_frames) { - tty_config.setColor(writer, .bold) catch {}; + fwm.setColor(writer, .bold) catch {}; try writer.print("({d} additional stack frames skipped...)\n", .{n_frames - captured_frames}); - tty_config.setColor(writer, .reset) catch {}; + fwm.setColor(writer, .reset) catch {}; } } /// A thin wrapper around `writeStackTrace` which writes to stderr and ignores write errors. @@ -1143,7 +1119,7 @@ fn printSourceAtAddress( debug_info: *SelfInfo, writer: *Writer, address: usize, - tty_config: tty.Config, + fwm: File.Writer.Mode, ) Writer.Error!void { const symbol: Symbol = debug_info.getSymbol(gpa, io, address) catch |err| switch (err) { error.MissingDebugInfo, @@ -1151,15 +1127,15 @@ fn printSourceAtAddress( error.InvalidDebugInfo, => .unknown, error.ReadFailed, error.Unexpected, error.Canceled => s: { - tty_config.setColor(writer, .dim) catch {}; + fwm.setColor(writer, .dim) catch {}; try writer.print("Failed to read debug info from filesystem, trace may be incomplete\n\n", .{}); - tty_config.setColor(writer, .reset) catch {}; + fwm.setColor(writer, .reset) catch {}; break :s .unknown; }, error.OutOfMemory => s: { - tty_config.setColor(writer, .dim) catch {}; + fwm.setColor(writer, .dim) catch {}; try writer.print("Ran out of memory loading debug info, trace may be incomplete\n\n", .{}); - tty_config.setColor(writer, .reset) catch {}; + fwm.setColor(writer, .reset) catch {}; break :s .unknown; }, }; @@ -1171,7 +1147,7 @@ fn printSourceAtAddress( address, symbol.name orelse "???", symbol.compile_unit_name orelse debug_info.getModuleName(gpa, address) catch "???", - tty_config, + fwm, ); } fn printLineInfo( @@ -1181,10 +1157,10 @@ fn printLineInfo( address: usize, symbol_name: []const u8, compile_unit_name: []const u8, - tty_config: tty.Config, + fwm: File.Writer.Mode, ) Writer.Error!void { nosuspend { - tty_config.setColor(writer, .bold) catch {}; + fwm.setColor(writer, .bold) catch {}; if (source_location) |*sl| { try writer.print("{s}:{d}:{d}", .{ sl.file_name, sl.line, sl.column }); @@ -1192,11 +1168,11 @@ fn printLineInfo( try writer.writeAll("???:?:?"); } - tty_config.setColor(writer, .reset) catch {}; + fwm.setColor(writer, .reset) catch {}; try writer.writeAll(": "); - tty_config.setColor(writer, .dim) catch {}; + fwm.setColor(writer, .dim) catch {}; try writer.print("0x{x} in {s} ({s})", .{ address, symbol_name, compile_unit_name }); - tty_config.setColor(writer, .reset) catch {}; + fwm.setColor(writer, .reset) catch {}; try writer.writeAll("\n"); // Show the matching source code line if possible @@ -1207,9 +1183,9 @@ fn printLineInfo( const space_needed = @as(usize, @intCast(sl.column - 1)); try writer.splatByteAll(' ', space_needed); - tty_config.setColor(writer, .green) catch {}; + fwm.setColor(writer, .green) catch {}; try writer.writeAll("^"); - tty_config.setColor(writer, .reset) catch {}; + fwm.setColor(writer, .reset) catch {}; } try writer.writeAll("\n"); } else |_| { @@ -1250,18 +1226,18 @@ fn printLineFromFile(io: Io, writer: *Writer, source_location: SourceLocation) ! } test printLineFromFile { - const io = std.testing.io; - const gpa = std.testing.allocator; + const io = testing.io; + const gpa = testing.allocator; var aw: Writer.Allocating = .init(gpa); defer aw.deinit(); const output_stream = &aw.writer; const join = std.fs.path.join; - const expectError = std.testing.expectError; - const expectEqualStrings = std.testing.expectEqualStrings; + const expectError = testing.expectError; + const expectEqualStrings = testing.expectEqualStrings; - var test_dir = std.testing.tmpDir(.{}); + var test_dir = testing.tmpDir(.{}); defer test_dir.cleanup(); // Relies on testing.tmpDir internals which is not ideal, but SourceLocation requires paths. const test_dir_path = try join(gpa, &.{ ".zig-cache", "tmp", test_dir.sub_path[0..] }); @@ -1578,19 +1554,19 @@ pub fn defaultHandleSegfault(addr: ?usize, name: []const u8, opt_ctx: ?CpuContex _ = panicking.fetchAdd(1, .seq_cst); trace: { - const stderr, const tty_config = lockStderrWriter(&.{}); + const stderr = lockStderrWriter(&.{}); defer unlockStderrWriter(); if (addr) |a| { - stderr.print("{s} at address 0x{x}\n", .{ name, a }) catch break :trace; + stderr.interface.print("{s} at address 0x{x}\n", .{ name, a }) catch break :trace; } else { - stderr.print("{s} (no address available)\n", .{name}) catch break :trace; + stderr.interface.print("{s} (no address available)\n", .{name}) catch break :trace; } if (opt_ctx) |context| { writeCurrentStackTrace(.{ .context = context, .allow_unsafe_unwind = true, // we're crashing anyway, give it our all! - }, stderr, tty_config) catch break :trace; + }, &stderr.interface, stderr.mode) catch break :trace; } } }, @@ -1599,8 +1575,8 @@ pub fn defaultHandleSegfault(addr: ?usize, name: []const u8, opt_ctx: ?CpuContex // A segfault happened while trying to print a previous panic message. // We're still holding the mutex but that's fine as we're going to // call abort(). - const stderr, _ = lockStderrWriter(&.{}); - stderr.writeAll("aborting due to recursive panic\n") catch {}; + const stderr = lockStderrWriter(&.{}); + stderr.interface.writeAll("aborting due to recursive panic\n") catch {}; }, else => {}, // Panicked while printing the recursive panic message. } @@ -1632,9 +1608,9 @@ test "manage resources correctly" { return @returnAddress(); } }; - const gpa = std.testing.allocator; - var threaded: Io.Threaded = .init_single_threaded; - const io = threaded.ioBasic(); + const gpa = testing.allocator; + const io = testing.io; + var discarding: Writer.Discarding = .init(&.{}); var di: SelfInfo = .init; defer di.deinit(gpa); diff --git a/lib/std/heap/debug_allocator.zig b/lib/std/heap/debug_allocator.zig index 27b1b9179f..66ff59a711 100644 --- a/lib/std/heap/debug_allocator.zig +++ b/lib/std/heap/debug_allocator.zig @@ -179,8 +179,6 @@ pub fn DebugAllocator(comptime config: Config) type { total_requested_bytes: @TypeOf(total_requested_bytes_init) = total_requested_bytes_init, requested_memory_limit: @TypeOf(requested_memory_limit_init) = requested_memory_limit_init, mutex: @TypeOf(mutex_init) = mutex_init, - /// Set this value differently to affect how errors and leaks are logged. - tty_config: std.Io.tty.Config = .no_color, const Self = @This(); @@ -427,7 +425,6 @@ pub fn DebugAllocator(comptime config: Config) type { bucket: *BucketHeader, size_class_index: usize, used_bits_count: usize, - tty_config: std.Io.tty.Config, ) usize { const size_class = @as(usize, 1) << @as(Log2USize, @intCast(size_class_index)); const slot_count = slot_counts[size_class_index]; @@ -444,11 +441,7 @@ pub fn DebugAllocator(comptime config: Config) type { const page_addr = @intFromPtr(bucket) & ~(page_size - 1); const addr = page_addr + slot_index * size_class; log.err("memory address 0x{x} leaked: {f}", .{ - addr, - std.debug.FormatStackTrace{ - .stack_trace = stack_trace, - .tty_config = tty_config, - }, + addr, std.debug.FormatStackTrace{ .stack_trace = stack_trace }, }); leaks += 1; } @@ -460,8 +453,6 @@ pub fn DebugAllocator(comptime config: Config) type { /// Emits log messages for leaks and then returns the number of detected leaks (0 if no leaks were detected). pub fn detectLeaks(self: *Self) usize { - const tty_config = self.tty_config; - var leaks: usize = 0; for (self.buckets, 0..) |init_optional_bucket, size_class_index| { @@ -469,7 +460,7 @@ pub fn DebugAllocator(comptime config: Config) type { const slot_count = slot_counts[size_class_index]; const used_bits_count = usedBitsCount(slot_count); while (optional_bucket) |bucket| { - leaks += detectLeaksInBucket(bucket, size_class_index, used_bits_count, tty_config); + leaks += detectLeaksInBucket(bucket, size_class_index, used_bits_count); optional_bucket = bucket.prev; } } @@ -480,10 +471,7 @@ pub fn DebugAllocator(comptime config: Config) type { const stack_trace = large_alloc.getStackTrace(.alloc); log.err("memory address 0x{x} leaked: {f}", .{ @intFromPtr(large_alloc.bytes.ptr), - std.debug.FormatStackTrace{ - .stack_trace = stack_trace, - .tty_config = tty_config, - }, + std.debug.FormatStackTrace{ .stack_trace = stack_trace }, }); leaks += 1; } @@ -535,28 +523,14 @@ pub fn DebugAllocator(comptime config: Config) type { @memset(addr_buf[@min(st.index, addr_buf.len)..], 0); } - fn reportDoubleFree( - tty_config: std.Io.tty.Config, - ret_addr: usize, - alloc_stack_trace: StackTrace, - free_stack_trace: StackTrace, - ) void { + fn reportDoubleFree(ret_addr: usize, alloc_stack_trace: StackTrace, free_stack_trace: StackTrace) void { @branchHint(.cold); var addr_buf: [stack_n]usize = undefined; const second_free_stack_trace = std.debug.captureCurrentStackTrace(.{ .first_address = ret_addr }, &addr_buf); log.err("Double free detected. Allocation: {f} First free: {f} Second free: {f}", .{ - std.debug.FormatStackTrace{ - .stack_trace = alloc_stack_trace, - .tty_config = tty_config, - }, - std.debug.FormatStackTrace{ - .stack_trace = free_stack_trace, - .tty_config = tty_config, - }, - std.debug.FormatStackTrace{ - .stack_trace = second_free_stack_trace, - .tty_config = tty_config, - }, + std.debug.FormatStackTrace{ .stack_trace = alloc_stack_trace }, + std.debug.FormatStackTrace{ .stack_trace = free_stack_trace }, + std.debug.FormatStackTrace{ .stack_trace = second_free_stack_trace }, }); } @@ -587,7 +561,7 @@ pub fn DebugAllocator(comptime config: Config) type { if (config.retain_metadata and entry.value_ptr.freed) { if (config.safety) { - reportDoubleFree(self.tty_config, ret_addr, entry.value_ptr.getStackTrace(.alloc), entry.value_ptr.getStackTrace(.free)); + reportDoubleFree(ret_addr, entry.value_ptr.getStackTrace(.alloc), entry.value_ptr.getStackTrace(.free)); @panic("Unrecoverable double free"); } else { unreachable; @@ -598,18 +572,11 @@ pub fn DebugAllocator(comptime config: Config) type { @branchHint(.cold); var addr_buf: [stack_n]usize = undefined; const free_stack_trace = std.debug.captureCurrentStackTrace(.{ .first_address = ret_addr }, &addr_buf); - const tty_config = self.tty_config; log.err("Allocation size {d} bytes does not match free size {d}. Allocation: {f} Free: {f}", .{ entry.value_ptr.bytes.len, old_mem.len, - std.debug.FormatStackTrace{ - .stack_trace = entry.value_ptr.getStackTrace(.alloc), - .tty_config = tty_config, - }, - std.debug.FormatStackTrace{ - .stack_trace = free_stack_trace, - .tty_config = tty_config, - }, + std.debug.FormatStackTrace{ .stack_trace = entry.value_ptr.getStackTrace(.alloc) }, + std.debug.FormatStackTrace{ .stack_trace = free_stack_trace }, }); } @@ -701,7 +668,7 @@ pub fn DebugAllocator(comptime config: Config) type { if (config.retain_metadata and entry.value_ptr.freed) { if (config.safety) { - reportDoubleFree(self.tty_config, ret_addr, entry.value_ptr.getStackTrace(.alloc), entry.value_ptr.getStackTrace(.free)); + reportDoubleFree(ret_addr, entry.value_ptr.getStackTrace(.alloc), entry.value_ptr.getStackTrace(.free)); return; } else { unreachable; @@ -712,18 +679,11 @@ pub fn DebugAllocator(comptime config: Config) type { @branchHint(.cold); var addr_buf: [stack_n]usize = undefined; const free_stack_trace = std.debug.captureCurrentStackTrace(.{ .first_address = ret_addr }, &addr_buf); - const tty_config = self.tty_config; log.err("Allocation size {d} bytes does not match free size {d}. Allocation: {f} Free: {f}", .{ entry.value_ptr.bytes.len, old_mem.len, - std.debug.FormatStackTrace{ - .stack_trace = entry.value_ptr.getStackTrace(.alloc), - .tty_config = tty_config, - }, - std.debug.FormatStackTrace{ - .stack_trace = free_stack_trace, - .tty_config = tty_config, - }, + std.debug.FormatStackTrace{ .stack_trace = entry.value_ptr.getStackTrace(.alloc) }, + std.debug.FormatStackTrace{ .stack_trace = free_stack_trace }, }); } @@ -924,7 +884,6 @@ pub fn DebugAllocator(comptime config: Config) type { if (!is_used) { if (config.safety) { reportDoubleFree( - self.tty_config, return_address, bucketStackTrace(bucket, slot_count, slot_index, .alloc), bucketStackTrace(bucket, slot_count, slot_index, .free), @@ -946,34 +905,24 @@ pub fn DebugAllocator(comptime config: Config) type { const free_stack_trace = std.debug.captureCurrentStackTrace(.{ .first_address = return_address }, &addr_buf); if (old_memory.len != requested_size) { @branchHint(.cold); - const tty_config = self.tty_config; log.err("Allocation size {d} bytes does not match free size {d}. Allocation: {f} Free: {f}", .{ requested_size, old_memory.len, std.debug.FormatStackTrace{ .stack_trace = bucketStackTrace(bucket, slot_count, slot_index, .alloc), - .tty_config = tty_config, - }, - std.debug.FormatStackTrace{ - .stack_trace = free_stack_trace, - .tty_config = tty_config, }, + std.debug.FormatStackTrace{ .stack_trace = free_stack_trace }, }); } if (alignment != slot_alignment) { @branchHint(.cold); - const tty_config = self.tty_config; log.err("Allocation alignment {d} does not match free alignment {d}. Allocation: {f} Free: {f}", .{ slot_alignment.toByteUnits(), alignment.toByteUnits(), std.debug.FormatStackTrace{ .stack_trace = bucketStackTrace(bucket, slot_count, slot_index, .alloc), - .tty_config = tty_config, - }, - std.debug.FormatStackTrace{ - .stack_trace = free_stack_trace, - .tty_config = tty_config, }, + std.debug.FormatStackTrace{ .stack_trace = free_stack_trace }, }); } } @@ -1040,7 +989,6 @@ pub fn DebugAllocator(comptime config: Config) type { const is_used = @as(u1, @truncate(used_byte.* >> used_bit_index)) != 0; if (!is_used) { reportDoubleFree( - self.tty_config, return_address, bucketStackTrace(bucket, slot_count, slot_index, .alloc), bucketStackTrace(bucket, slot_count, slot_index, .free), @@ -1058,34 +1006,24 @@ pub fn DebugAllocator(comptime config: Config) type { const free_stack_trace = std.debug.captureCurrentStackTrace(.{ .first_address = return_address }, &addr_buf); if (memory.len != requested_size) { @branchHint(.cold); - const tty_config = self.tty_config; log.err("Allocation size {d} bytes does not match free size {d}. Allocation: {f} Free: {f}", .{ requested_size, memory.len, std.debug.FormatStackTrace{ .stack_trace = bucketStackTrace(bucket, slot_count, slot_index, .alloc), - .tty_config = tty_config, - }, - std.debug.FormatStackTrace{ - .stack_trace = free_stack_trace, - .tty_config = tty_config, }, + std.debug.FormatStackTrace{ .stack_trace = free_stack_trace }, }); } if (alignment != slot_alignment) { @branchHint(.cold); - const tty_config = self.tty_config; log.err("Allocation alignment {d} does not match free alignment {d}. Allocation: {f} Free: {f}", .{ slot_alignment.toByteUnits(), alignment.toByteUnits(), std.debug.FormatStackTrace{ .stack_trace = bucketStackTrace(bucket, slot_count, slot_index, .alloc), - .tty_config = tty_config, - }, - std.debug.FormatStackTrace{ - .stack_trace = free_stack_trace, - .tty_config = tty_config, }, + std.debug.FormatStackTrace{ .stack_trace = free_stack_trace }, }); } } diff --git a/lib/std/log.zig b/lib/std/log.zig index 461dfca36e..029f724f44 100644 --- a/lib/std/log.zig +++ b/lib/std/log.zig @@ -15,7 +15,7 @@ //! //! For an example implementation of the `logFn` function, see `defaultLog`, //! which is the default implementation. It outputs to stderr, using color if -//! the detected `std.Io.tty.Config` supports it. Its output looks like this: +//! supported. Its output looks like this: //! ``` //! error: this is an error //! error(scope): this is an error with a non-default scope @@ -80,8 +80,6 @@ pub fn logEnabled(comptime level: Level, comptime scope: @EnumLiteral()) bool { return @intFromEnum(level) <= @intFromEnum(std.options.log_level); } -var static_threaded_io: std.Io.Threaded = .init_single_threaded; - /// The default implementation for the log function. Custom log functions may /// forward log messages to this function. /// @@ -93,36 +91,64 @@ pub fn defaultLog( comptime format: []const u8, args: anytype, ) void { - return defaultLogIo(level, scope, format, args, static_threaded_io.io()); + var buffer: [64]u8 = undefined; + const stderr = std.debug.lockStderrWriter(&buffer); + defer std.debug.unlockStderrWriter(); + return defaultLogFileWriter(level, scope, format, args, stderr); } -pub fn defaultLogIo( +pub fn defaultLogFileWriter( comptime level: Level, comptime scope: @EnumLiteral(), comptime format: []const u8, args: anytype, - io: std.Io, + fw: *std.Io.File.Writer, ) void { - var buffer: [64]u8 = undefined; - const stderr, const ttyconf = io.lockStderrWriter(&buffer); - defer io.unlockStderrWriter(); - ttyconf.setColor(stderr, switch (level) { + fw.setColor(switch (level) { .err => .red, .warn => .yellow, .info => .green, .debug => .magenta, }) catch {}; - ttyconf.setColor(stderr, .bold) catch {}; - stderr.writeAll(level.asText()) catch return; - ttyconf.setColor(stderr, .reset) catch {}; - ttyconf.setColor(stderr, .dim) catch {}; - ttyconf.setColor(stderr, .bold) catch {}; + fw.setColor(.bold) catch {}; + fw.interface.writeAll(level.asText()) catch return; + fw.setColor(.reset) catch {}; + fw.setColor(.dim) catch {}; + fw.setColor(.bold) catch {}; if (scope != .default) { - stderr.print("({s})", .{@tagName(scope)}) catch return; + fw.interface.print("({s})", .{@tagName(scope)}) catch return; + } + fw.interface.writeAll(": ") catch return; + fw.setColor(.reset) catch {}; + fw.interface.print(format ++ "\n", decorateArgs(args, fw.mode)) catch return; +} + +fn DecorateArgs(comptime Args: type) type { + const fields = @typeInfo(Args).@"struct".fields; + var new_fields: [fields.len]type = undefined; + for (fields, &new_fields) |old, *new| { + if (old.type == std.debug.FormatStackTrace) { + new.* = std.debug.FormatStackTrace.Decorated; + } else { + new.* = old.type; + } + } + return @Tuple(&new_fields); +} + +fn decorateArgs(args: anytype, file_writer_mode: std.Io.File.Writer.Mode) DecorateArgs(@TypeOf(args)) { + var new_args: DecorateArgs(@TypeOf(args)) = undefined; + inline for (args, &new_args) |old, *new| { + if (@TypeOf(old) == std.debug.FormatStackTrace) { + new.* = .{ + .stack_trace = old.stack_trace, + .file_writer_mode = file_writer_mode, + }; + } else { + new.* = old; + } } - stderr.writeAll(": ") catch return; - ttyconf.setColor(stderr, .reset) catch {}; - stderr.print(format ++ "\n", args) catch return; + return new_args; } /// Returns a scoped logging namespace that logs all messages using the scope diff --git a/lib/std/process.zig b/lib/std/process.zig index f7ecf5fdc2..5c6e6f89eb 100644 --- a/lib/std/process.zig +++ b/lib/std/process.zig @@ -439,25 +439,25 @@ pub fn getEnvVarOwned(allocator: Allocator, key: []const u8) GetEnvVarOwnedError } /// On Windows, `key` must be valid WTF-8. -pub fn hasEnvVarConstant(comptime key: []const u8) bool { +pub inline fn hasEnvVarConstant(comptime key: []const u8) bool { if (native_os == .windows) { const key_w = comptime unicode.wtf8ToWtf16LeStringLiteral(key); return getenvW(key_w) != null; } else if (native_os == .wasi and !builtin.link_libc) { - @compileError("hasEnvVarConstant is not supported for WASI without libc"); + return false; } else { return posix.getenv(key) != null; } } /// On Windows, `key` must be valid WTF-8. -pub fn hasNonEmptyEnvVarConstant(comptime key: []const u8) bool { +pub inline fn hasNonEmptyEnvVarConstant(comptime key: []const u8) bool { if (native_os == .windows) { const key_w = comptime unicode.wtf8ToWtf16LeStringLiteral(key); const value = getenvW(key_w) orelse return false; return value.len != 0; } else if (native_os == .wasi and !builtin.link_libc) { - @compileError("hasNonEmptyEnvVarConstant is not supported for WASI without libc"); + return false; } else { const value = posix.getenv(key) orelse return false; return value.len != 0; diff --git a/src/main.zig b/src/main.zig index bb940a1fe7..2eebb6d060 100644 --- a/src/main.zig +++ b/src/main.zig @@ -247,8 +247,6 @@ fn mainArgs(gpa: Allocator, arena: Allocator, args: []const []const u8) !void { threaded.stack_size = thread_stack_size; const io = threaded.io(); - debug_allocator.tty_config = .detect(io, .stderr()); - const cmd = args[1]; const cmd_args = args[2..]; if (mem.eql(u8, cmd, "build-exe")) { -- cgit v1.2.3 From e68ae8d7a1e78a25392e271d7ca894e0f09aa218 Mon Sep 17 00:00:00 2001 From: Andrew Kelley Date: Fri, 12 Dec 2025 17:18:29 -0800 Subject: update uses of std.debug.lockStdErr --- lib/compiler/test_runner.zig | 17 ++++++++++++----- lib/std/debug/simple_panic.zig | 6 +++--- lib/std/process.zig | 24 +++++++++++------------- src/main.zig | 10 +++++----- 4 files changed, 31 insertions(+), 26 deletions(-) (limited to 'lib/std/process.zig') diff --git a/lib/compiler/test_runner.zig b/lib/compiler/test_runner.zig index b0ef17a7e9..f7f59a5bf3 100644 --- a/lib/compiler/test_runner.zig +++ b/lib/compiler/test_runner.zig @@ -405,15 +405,22 @@ pub fn fuzz( testOne(ctx, input.toSlice()) catch |err| switch (err) { error.SkipZigTest => return, else => { - std.debug.lockStdErr(); - if (@errorReturnTrace()) |trace| std.debug.dumpStackTrace(trace); - std.debug.print("failed with error.{t}\n", .{err}); + const stderr = std.debug.lockStderrWriter(&.{}); + p: { + if (@errorReturnTrace()) |trace| { + std.debug.writeStackTrace(trace, &stderr.interface, stderr.mode) catch break :p; + } + stderr.interface.print("failed with error.{t}\n", .{err}) catch break :p; + stderr.interface.flush() catch break :p; + } + stderr.interface.flush() catch {}; std.process.exit(1); }, }; if (log_err_count != 0) { - std.debug.lockStdErr(); - std.debug.print("error logs detected\n", .{}); + const stderr = std.debug.lockStderrWriter(&.{}); + stderr.interface.print("error logs detected\n", .{}) catch {}; + stderr.interface.flush() catch {}; std.process.exit(1); } } diff --git a/lib/std/debug/simple_panic.zig b/lib/std/debug/simple_panic.zig index f6ff77e04f..2d0e5abf4e 100644 --- a/lib/std/debug/simple_panic.zig +++ b/lib/std/debug/simple_panic.zig @@ -14,9 +14,9 @@ const std = @import("../std.zig"); pub fn call(msg: []const u8, ra: ?usize) noreturn { @branchHint(.cold); _ = ra; - std.debug.lockStdErr(); - const stderr: std.Io.File = .stderr(); - stderr.writeAll(msg) catch {}; + const stderr = std.debug.lockStderrWriter(&.{}); + stderr.interface.writeAll(msg) catch {}; + stderr.interface.flush(msg) catch {}; @trap(); } diff --git a/lib/std/process.zig b/lib/std/process.zig index 5c6e6f89eb..216e793cbe 100644 --- a/lib/std/process.zig +++ b/lib/std/process.zig @@ -1841,21 +1841,19 @@ pub fn totalSystemMemory() TotalSystemMemoryError!u64 { } } -/// Indicate that we are now terminating with a successful exit code. -/// In debug builds, this is a no-op, so that the calling code's -/// cleanup mechanisms are tested and so that external tools that -/// check for resource leaks can be accurate. In release builds, this -/// calls exit(0), and does not return. -pub fn cleanExit() void { - if (builtin.mode == .Debug) { - return; - } else { - std.debug.lockStdErr(); - exit(0); - } +/// Indicate intent to terminate with a successful exit code. +/// +/// In debug builds, this is a no-op, so that the calling code's cleanup +/// mechanisms are tested and so that external tools checking for resource +/// leaks can be accurate. In release builds, this calls `exit` with code zero, +/// and does not return. +pub fn cleanExit(io: Io) void { + if (builtin.mode == .Debug) return; + _ = io.lockStderrWriter(&.{}); + exit(0); } -/// Raise the open file descriptor limit. +/// Request ability to have more open file descriptors simultaneously. /// /// On some systems, this raises the limit before seeing ProcessFdQuotaExceeded /// errors. On other systems, this does nothing. diff --git a/src/main.zig b/src/main.zig index c1f5e3c026..160b9919eb 100644 --- a/src/main.zig +++ b/src/main.zig @@ -4429,12 +4429,12 @@ fn runOrTest( // the error message and invocation below. if (process.can_execv and arg_mode == .run) { // execv releases the locks; no need to destroy the Compilation here. - std.debug.lockStdErr(); + _ = std.debug.lockStderrWriter(&.{}); const err = process.execve(gpa, argv.items, &env_map); - std.debug.unlockStdErr(); + std.debug.unlockStderrWriter(); try warnAboutForeignBinaries(io, arena, arg_mode, target, link_libc); const cmd = try std.mem.join(arena, " ", argv.items); - fatal("the following command failed to execve with '{s}':\n{s}", .{ @errorName(err), cmd }); + fatal("the following command failed to execve with '{t}':\n{s}", .{ err, cmd }); } else if (process.can_spawn) { var child = std.process.Child.init(argv.items, gpa); child.env_map = &env_map; @@ -4448,8 +4448,8 @@ fn runOrTest( comp_destroyed.* = true; const term_result = t: { - std.debug.lockStdErr(); - defer std.debug.unlockStdErr(); + _ = std.debug.lockStderrWriter(); + defer std.debug.unlockStderrWriter(); break :t child.spawnAndWait(io); }; const term = term_result catch |err| { -- cgit v1.2.3 From 1925e0319f1337b4856bd5a181bf4f6d3ac7d428 Mon Sep 17 00:00:00 2001 From: Andrew Kelley Date: Fri, 12 Dec 2025 19:04:35 -0800 Subject: update lockStderrWriter sites use the application's Io implementation where possible. This correctly makes writing to stderr cancelable, fallible, and participate in the application's event loop. It also removes one more hard-coded dependency on a secondary Io implementation. --- lib/compiler/build_runner.zig | 18 +++---- lib/compiler/resinator/cli.zig | 8 ++-- lib/compiler/resinator/errors.zig | 8 ++-- lib/compiler/resinator/main.zig | 98 ++++++++++++++++++++------------------- lib/compiler/std-docs.zig | 2 +- lib/compiler/test_runner.zig | 3 -- lib/std/Build.zig | 31 +++++++++---- lib/std/Build/Fuzz.zig | 21 +++++---- lib/std/Build/Step/Compile.zig | 17 ++++--- lib/std/Build/Step/Run.zig | 5 +- lib/std/Build/WebServer.zig | 2 +- lib/std/Progress.zig | 2 +- lib/std/json/dynamic.zig | 5 +- lib/std/process.zig | 2 +- lib/std/testing.zig | 16 +++++-- lib/std/zig.zig | 4 +- lib/std/zig/ErrorBundle.zig | 10 ++-- lib/std/zig/parser_test.zig | 30 +++++++----- src/Air/print.zig | 28 +++++++---- src/Compilation.zig | 53 ++++++++++++--------- src/InternPool.zig | 63 ++++++++++++------------- src/Sema.zig | 8 ++-- src/Zcu/PerThread.zig | 5 +- src/codegen/aarch64/Select.zig | 11 +++-- src/crash_report.zig | 9 ++-- src/fmt.zig | 8 ++-- src/libs/mingw.zig | 29 ++++++++---- src/libs/mingw/def.zig | 32 +++++++++---- src/link.zig | 2 +- src/link/Coff.zig | 15 ++++-- src/link/Elf2.zig | 14 ++++-- src/main.zig | 33 ++++++------- 32 files changed, 345 insertions(+), 247 deletions(-) (limited to 'lib/std/process.zig') diff --git a/lib/compiler/build_runner.zig b/lib/compiler/build_runner.zig index acba1a4621..d284d85747 100644 --- a/lib/compiler/build_runner.zig +++ b/lib/compiler/build_runner.zig @@ -522,7 +522,7 @@ pub fn main() !void { // Perhaps in the future there could be an Advanced Options flag // such as --debug-build-runner-leaks which would make this code // return instead of calling exit. - _ = std.debug.lockStderrWriter(&.{}); + _ = io.lockStderrWriter(&.{}) catch {}; process.exit(1); }, else => |e| return e, @@ -554,8 +554,8 @@ pub fn main() !void { } rebuild: while (true) : (if (run.error_style.clearOnUpdate()) { - const stderr = std.debug.lockStderrWriter(&stdio_buffer_allocation); - defer std.debug.unlockStderrWriter(); + const stderr = try io.lockStderrWriter(&stdio_buffer_allocation); + defer io.unlockStderrWriter(); try stderr.writeAllUnescaped("\x1B[2J\x1B[3J\x1B[H"); }) { if (run.web_server) |*ws| ws.startBuild(); @@ -856,8 +856,8 @@ fn runStepNames( .none => break :summary, } - const stderr = std.debug.lockStderrWriter(&stdio_buffer_allocation); - defer std.debug.unlockStderrWriter(); + const stderr = try io.lockStderrWriter(&stdio_buffer_allocation); + defer io.unlockStderrWriter(); const w = &stderr.interface; const fwm = stderr.mode; @@ -954,7 +954,7 @@ fn runStepNames( if (run.error_style.verboseContext()) break :code 1; // failure; print build command break :code 2; // failure; do not print build command }; - _ = std.debug.lockStderrWriter(&.{}); + _ = io.lockStderrWriter(&.{}) catch {}; process.exit(code); } @@ -1369,8 +1369,10 @@ fn workerMakeOneStep( const show_error_msgs = s.result_error_msgs.items.len > 0; const show_stderr = s.result_stderr.len > 0; if (show_error_msgs or show_compile_errors or show_stderr) { - const stderr = std.debug.lockStderrWriter(&stdio_buffer_allocation); - defer std.debug.unlockStderrWriter(); + const stderr = io.lockStderrWriter(&stdio_buffer_allocation) catch |err| switch (err) { + error.Canceled => return, + }; + defer io.unlockStderrWriter(); printErrorMessages(gpa, s, .{}, &stderr.interface, stderr.mode, run.error_style, run.multiline_errors) catch {}; } diff --git a/lib/compiler/resinator/cli.zig b/lib/compiler/resinator/cli.zig index d2dd71b1f6..08befbe1fa 100644 --- a/lib/compiler/resinator/cli.zig +++ b/lib/compiler/resinator/cli.zig @@ -125,10 +125,10 @@ pub const Diagnostics = struct { try self.errors.append(self.allocator, error_details); } - pub fn renderToStdErr(self: *Diagnostics, args: []const []const u8) void { - const stderr, const ttyconf = std.debug.lockStderrWriter(&.{}); - defer std.debug.unlockStderrWriter(); - self.renderToWriter(args, stderr, ttyconf) catch return; + pub fn renderToStderr(self: *Diagnostics, io: Io, args: []const []const u8) void { + const stderr = io.lockStderrWriter(&.{}); + defer io.unlockStderrWriter(); + self.renderToWriter(args, &stderr.interface, stderr.mode) catch return; } pub fn renderToWriter(self: *Diagnostics, args: []const []const u8, writer: *std.Io.Writer, config: std.Io.tty.Config) !void { diff --git a/lib/compiler/resinator/errors.zig b/lib/compiler/resinator/errors.zig index 14ff28a697..031b5d3574 100644 --- a/lib/compiler/resinator/errors.zig +++ b/lib/compiler/resinator/errors.zig @@ -67,12 +67,12 @@ pub const Diagnostics = struct { return @intCast(index); } - pub fn renderToStdErr(self: *Diagnostics, cwd: Io.Dir, source: []const u8, source_mappings: ?SourceMappings) void { + pub fn renderToStderr(self: *Diagnostics, cwd: Io.Dir, source: []const u8, source_mappings: ?SourceMappings) void { const io = self.io; - const stderr, const ttyconf = std.debug.lockStderrWriter(&.{}); - defer std.debug.unlockStderrWriter(); + const stderr = io.lockStderrWriter(&.{}); + defer io.unlockStderrWriter(); for (self.errors.items) |err_details| { - renderErrorMessage(io, stderr, ttyconf, cwd, err_details, source, self.strings.items, source_mappings) catch return; + renderErrorMessage(io, &stderr.interface, stderr.mode, cwd, err_details, source, self.strings.items, source_mappings) catch return; } } diff --git a/lib/compiler/resinator/main.zig b/lib/compiler/resinator/main.zig index 6c12903f06..bd2fcc912e 100644 --- a/lib/compiler/resinator/main.zig +++ b/lib/compiler/resinator/main.zig @@ -24,6 +24,10 @@ pub fn main() !void { defer std.debug.assert(debug_allocator.deinit() == .ok); const gpa = debug_allocator.allocator(); + var threaded: std.Io.Threaded = .init(gpa); + defer threaded.deinit(); + const io = threaded.io(); + var arena_state = std.heap.ArenaAllocator.init(gpa); defer arena_state.deinit(); const arena = arena_state.allocator(); @@ -31,8 +35,8 @@ pub fn main() !void { const args = try std.process.argsAlloc(arena); if (args.len < 2) { - const w, const ttyconf = std.debug.lockStderrWriter(&.{}); - try renderErrorMessage(w, ttyconf, .err, "expected zig lib dir as first argument", .{}); + const stderr = io.lockStderrWriter(&.{}); + try renderErrorMessage(&stderr.interface, stderr.mode, .err, "expected zig lib dir as first argument", .{}); std.process.exit(1); } const zig_lib_dir = args[1]; @@ -45,7 +49,7 @@ pub fn main() !void { } var stdout_buffer: [1024]u8 = undefined; - var stdout_writer = Io.File.stdout().writer(&stdout_buffer); + var stdout_writer = Io.File.stdout().writer(io, &stdout_buffer); const stdout = &stdout_writer.interface; var error_handler: ErrorHandler = switch (zig_integration) { true => .{ @@ -71,24 +75,20 @@ pub fn main() !void { if (!zig_integration) { // print any warnings/notes - cli_diagnostics.renderToStdErr(cli_args); + cli_diagnostics.renderToStderr(io, cli_args); // If there was something printed, then add an extra newline separator // so that there is a clear separation between the cli diagnostics and whatever // gets printed after if (cli_diagnostics.errors.items.len > 0) { - const stderr, _ = std.debug.lockStderrWriter(&.{}); - defer std.debug.unlockStderrWriter(); - try stderr.writeByte('\n'); + const stderr = io.lockStderrWriter(&.{}); + defer io.unlockStderrWriter(); + try stderr.interface.writeByte('\n'); } } break :options options; }; defer options.deinit(); - var threaded: std.Io.Threaded = .init(gpa); - defer threaded.deinit(); - const io = threaded.io(); - if (options.print_help_and_exit) { try cli.writeUsage(stdout, "zig rc"); try stdout.flush(); @@ -130,10 +130,10 @@ pub fn main() !void { var stderr_buf: [512]u8 = undefined; var diagnostics: aro.Diagnostics = .{ .output = output: { if (zig_integration) break :output .{ .to_list = .{ .arena = .init(gpa) } }; - const w, const ttyconf = std.debug.lockStderrWriter(&stderr_buf); + const stderr = io.lockStderrWriter(&stderr_buf); break :output .{ .to_writer = .{ - .writer = w, - .color = ttyconf, + .writer = &stderr.interface, + .color = stderr.mode, } }; } }; defer { @@ -175,11 +175,11 @@ pub fn main() !void { std.process.exit(1); }, error.FileTooBig => { - try error_handler.emitMessage(gpa, .err, "failed during preprocessing: maximum file size exceeded", .{}); + try error_handler.emitMessage(gpa, io, .err, "failed during preprocessing: maximum file size exceeded", .{}); std.process.exit(1); }, error.WriteFailed => { - try error_handler.emitMessage(gpa, .err, "failed during preprocessing: error writing the preprocessed output", .{}); + try error_handler.emitMessage(gpa, io, .err, "failed during preprocessing: error writing the preprocessed output", .{}); std.process.exit(1); }, error.OutOfMemory => |e| return e, @@ -191,13 +191,13 @@ pub fn main() !void { .stdio => |file| { var file_reader = file.reader(io, &.{}); break :full_input file_reader.interface.allocRemaining(gpa, .unlimited) catch |err| { - try error_handler.emitMessage(gpa, .err, "unable to read input from stdin: {s}", .{@errorName(err)}); + try error_handler.emitMessage(gpa, io, .err, "unable to read input from stdin: {s}", .{@errorName(err)}); std.process.exit(1); }; }, .filename => |input_filename| { break :full_input Io.Dir.cwd().readFileAlloc(input_filename, gpa, .unlimited) catch |err| { - try error_handler.emitMessage(gpa, .err, "unable to read input file path '{s}': {s}", .{ input_filename, @errorName(err) }); + try error_handler.emitMessage(gpa, io, .err, "unable to read input file path '{s}': {s}", .{ input_filename, @errorName(err) }); std.process.exit(1); }; }, @@ -228,12 +228,12 @@ pub fn main() !void { } else if (options.input_format == .res) IoStream.fromIoSource(options.input_source, .input) catch |err| { - try error_handler.emitMessage(gpa, .err, "unable to read res file path '{s}': {s}", .{ options.input_source.filename, @errorName(err) }); + try error_handler.emitMessage(gpa, io, .err, "unable to read res file path '{s}': {s}", .{ options.input_source.filename, @errorName(err) }); std.process.exit(1); } else IoStream.fromIoSource(options.output_source, .output) catch |err| { - try error_handler.emitMessage(gpa, .err, "unable to create output file '{s}': {s}", .{ options.output_source.filename, @errorName(err) }); + try error_handler.emitMessage(gpa, io, .err, "unable to create output file '{s}': {s}", .{ options.output_source.filename, @errorName(err) }); std.process.exit(1); }; defer res_stream.deinit(gpa); @@ -246,17 +246,17 @@ pub fn main() !void { var mapping_results = parseAndRemoveLineCommands(gpa, full_input, full_input, .{ .initial_filename = options.input_source.filename }) catch |err| switch (err) { error.InvalidLineCommand => { // TODO: Maybe output the invalid line command - try error_handler.emitMessage(gpa, .err, "invalid line command in the preprocessed source", .{}); + try error_handler.emitMessage(gpa, io, .err, "invalid line command in the preprocessed source", .{}); if (options.preprocess == .no) { - try error_handler.emitMessage(gpa, .note, "line commands must be of the format: #line \"\"", .{}); + try error_handler.emitMessage(gpa, io, .note, "line commands must be of the format: #line \"\"", .{}); } else { - try error_handler.emitMessage(gpa, .note, "this is likely to be a bug, please report it", .{}); + try error_handler.emitMessage(gpa, io, .note, "this is likely to be a bug, please report it", .{}); } std.process.exit(1); }, error.LineNumberOverflow => { // TODO: Better error message - try error_handler.emitMessage(gpa, .err, "line number count exceeded maximum of {}", .{std.math.maxInt(usize)}); + try error_handler.emitMessage(gpa, io, .err, "line number count exceeded maximum of {}", .{std.math.maxInt(usize)}); std.process.exit(1); }, error.OutOfMemory => |e| return e, @@ -306,13 +306,13 @@ pub fn main() !void { // print any warnings/notes if (!zig_integration) { - diagnostics.renderToStdErr(Io.Dir.cwd(), final_input, mapping_results.mappings); + diagnostics.renderToStderr(io, Io.Dir.cwd(), final_input, mapping_results.mappings); } // write the depfile if (options.depfile_path) |depfile_path| { var depfile = Io.Dir.cwd().createFile(io, depfile_path, .{}) catch |err| { - try error_handler.emitMessage(gpa, .err, "unable to create depfile '{s}': {s}", .{ depfile_path, @errorName(err) }); + try error_handler.emitMessage(gpa, io, .err, "unable to create depfile '{s}': {s}", .{ depfile_path, @errorName(err) }); std.process.exit(1); }; defer depfile.close(io); @@ -340,7 +340,7 @@ pub fn main() !void { if (options.output_format != .coff) return; break :res_data res_stream.source.readAll(gpa, io) catch |err| { - try error_handler.emitMessage(gpa, .err, "unable to read res from '{s}': {s}", .{ res_stream.name, @errorName(err) }); + try error_handler.emitMessage(gpa, io, .err, "unable to read res from '{s}': {s}", .{ res_stream.name, @errorName(err) }); std.process.exit(1); }; }; @@ -353,14 +353,14 @@ pub fn main() !void { var res_reader: std.Io.Reader = .fixed(res_data.bytes); break :resources cvtres.parseRes(gpa, &res_reader, .{ .max_size = res_data.bytes.len }) catch |err| { // TODO: Better errors - try error_handler.emitMessage(gpa, .err, "unable to parse res from '{s}': {s}", .{ res_stream.name, @errorName(err) }); + try error_handler.emitMessage(gpa, io, .err, "unable to parse res from '{s}': {s}", .{ res_stream.name, @errorName(err) }); std.process.exit(1); }; }; defer resources.deinit(); var coff_stream = IoStream.fromIoSource(options.output_source, .output) catch |err| { - try error_handler.emitMessage(gpa, .err, "unable to create output file '{s}': {s}", .{ options.output_source.filename, @errorName(err) }); + try error_handler.emitMessage(gpa, io, .err, "unable to create output file '{s}': {s}", .{ options.output_source.filename, @errorName(err) }); std.process.exit(1); }; defer coff_stream.deinit(gpa); @@ -373,7 +373,7 @@ pub fn main() !void { switch (err) { error.DuplicateResource => { const duplicate_resource = resources.list.items[cvtres_diagnostics.duplicate_resource]; - try error_handler.emitMessage(gpa, .err, "duplicate resource [id: {f}, type: {f}, language: {f}]", .{ + try error_handler.emitMessage(gpa, io, .err, "duplicate resource [id: {f}, type: {f}, language: {f}]", .{ duplicate_resource.name_value, fmtResourceType(duplicate_resource.type_value), duplicate_resource.language, @@ -381,8 +381,8 @@ pub fn main() !void { }, error.ResourceDataTooLong => { const overflow_resource = resources.list.items[cvtres_diagnostics.duplicate_resource]; - try error_handler.emitMessage(gpa, .err, "resource has a data length that is too large to be written into a coff section", .{}); - try error_handler.emitMessage(gpa, .note, "the resource with the invalid size is [id: {f}, type: {f}, language: {f}]", .{ + try error_handler.emitMessage(gpa, io, .err, "resource has a data length that is too large to be written into a coff section", .{}); + try error_handler.emitMessage(gpa, io, .note, "the resource with the invalid size is [id: {f}, type: {f}, language: {f}]", .{ overflow_resource.name_value, fmtResourceType(overflow_resource.type_value), overflow_resource.language, @@ -390,15 +390,15 @@ pub fn main() !void { }, error.TotalResourceDataTooLong => { const overflow_resource = resources.list.items[cvtres_diagnostics.duplicate_resource]; - try error_handler.emitMessage(gpa, .err, "total resource data exceeds the maximum of the coff 'size of raw data' field", .{}); - try error_handler.emitMessage(gpa, .note, "size overflow occurred when attempting to write this resource: [id: {f}, type: {f}, language: {f}]", .{ + try error_handler.emitMessage(gpa, io, .err, "total resource data exceeds the maximum of the coff 'size of raw data' field", .{}); + try error_handler.emitMessage(gpa, io, .note, "size overflow occurred when attempting to write this resource: [id: {f}, type: {f}, language: {f}]", .{ overflow_resource.name_value, fmtResourceType(overflow_resource.type_value), overflow_resource.language, }); }, else => { - try error_handler.emitMessage(gpa, .err, "unable to write coff output file '{s}': {s}", .{ coff_stream.name, @errorName(err) }); + try error_handler.emitMessage(gpa, io, .err, "unable to write coff output file '{s}': {s}", .{ coff_stream.name, @errorName(err) }); }, } // Delete the output file on error @@ -550,16 +550,16 @@ const LazyIncludePaths = struct { else => |e| { switch (e) { error.UnsupportedAutoIncludesMachineType => { - try error_handler.emitMessage(self.arena, .err, "automatic include path detection is not supported for target '{s}'", .{@tagName(self.target_machine_type)}); + try error_handler.emitMessage(self.arena, io, .err, "automatic include path detection is not supported for target '{s}'", .{@tagName(self.target_machine_type)}); }, error.MsvcIncludesNotFound => { - try error_handler.emitMessage(self.arena, .err, "MSVC include paths could not be automatically detected", .{}); + try error_handler.emitMessage(self.arena, io, .err, "MSVC include paths could not be automatically detected", .{}); }, error.MingwIncludesNotFound => { - try error_handler.emitMessage(self.arena, .err, "MinGW include paths could not be automatically detected", .{}); + try error_handler.emitMessage(self.arena, io, .err, "MinGW include paths could not be automatically detected", .{}); }, } - try error_handler.emitMessage(self.arena, .note, "to disable auto includes, use the option /:auto-includes none", .{}); + try error_handler.emitMessage(self.arena, io, .note, "to disable auto includes, use the option /:auto-includes none", .{}); std.process.exit(1); }, }; @@ -664,6 +664,7 @@ const ErrorHandler = union(enum) { pub fn emitCliDiagnostics( self: *ErrorHandler, allocator: Allocator, + io: Io, args: []const []const u8, diagnostics: *cli.Diagnostics, ) !void { @@ -674,7 +675,7 @@ const ErrorHandler = union(enum) { try server.serveErrorBundle(error_bundle); }, - .stderr => diagnostics.renderToStdErr(args), + .stderr => diagnostics.renderToStderr(io, args), } } @@ -684,6 +685,7 @@ const ErrorHandler = union(enum) { fail_msg: []const u8, comp: *aro.Compilation, ) !void { + const io = comp.io; switch (self.*) { .server => |*server| { var error_bundle = try compiler_util.aroDiagnosticsToErrorBundle( @@ -697,9 +699,9 @@ const ErrorHandler = union(enum) { }, .stderr => { // aro errors have already been emitted - const stderr, const ttyconf = std.debug.lockStderrWriter(&.{}); - defer std.debug.unlockStderrWriter(); - try renderErrorMessage(stderr, ttyconf, .err, "{s}", .{fail_msg}); + const stderr = io.lockStderrWriter(&.{}); + defer io.unlockStderrWriter(); + try renderErrorMessage(&stderr.interface, stderr.mode, .err, "{s}", .{fail_msg}); }, } } @@ -707,6 +709,7 @@ const ErrorHandler = union(enum) { pub fn emitDiagnostics( self: *ErrorHandler, allocator: Allocator, + io: Io, cwd: Io.Dir, source: []const u8, diagnostics: *Diagnostics, @@ -719,13 +722,14 @@ const ErrorHandler = union(enum) { try server.serveErrorBundle(error_bundle); }, - .stderr => diagnostics.renderToStdErr(cwd, source, mappings), + .stderr => diagnostics.renderToStderr(io, cwd, source, mappings), } } pub fn emitMessage( self: *ErrorHandler, allocator: Allocator, + io: Io, msg_type: @import("utils.zig").ErrorMessageType, comptime format: []const u8, args: anytype, @@ -741,9 +745,9 @@ const ErrorHandler = union(enum) { try server.serveErrorBundle(error_bundle); }, .stderr => { - const stderr, const ttyconf = std.debug.lockStderrWriter(&.{}); - defer std.debug.unlockStderrWriter(); - try renderErrorMessage(stderr, ttyconf, msg_type, format, args); + const stderr = io.lockStderrWriter(&.{}); + defer io.unlockStderrWriter(); + try renderErrorMessage(&stderr.interface, stderr.mode, msg_type, format, args); }, } } diff --git a/lib/compiler/std-docs.zig b/lib/compiler/std-docs.zig index 538558fe48..9ff199d07b 100644 --- a/lib/compiler/std-docs.zig +++ b/lib/compiler/std-docs.zig @@ -407,7 +407,7 @@ fn buildWasmBinary( } if (result_error_bundle.errorMessageCount() > 0) { - result_error_bundle.renderToStdErr(.{}, true); + result_error_bundle.renderToStderr(io, .{}, true); std.log.err("the following command failed with {d} compilation errors:\n{s}", .{ result_error_bundle.errorMessageCount(), try std.Build.Step.allocPrintCmd(arena, null, argv.items), diff --git a/lib/compiler/test_runner.zig b/lib/compiler/test_runner.zig index f7f59a5bf3..3a135ab8fd 100644 --- a/lib/compiler/test_runner.zig +++ b/lib/compiler/test_runner.zig @@ -411,16 +411,13 @@ pub fn fuzz( std.debug.writeStackTrace(trace, &stderr.interface, stderr.mode) catch break :p; } stderr.interface.print("failed with error.{t}\n", .{err}) catch break :p; - stderr.interface.flush() catch break :p; } - stderr.interface.flush() catch {}; std.process.exit(1); }, }; if (log_err_count != 0) { const stderr = std.debug.lockStderrWriter(&.{}); stderr.interface.print("error logs detected\n", .{}) catch {}; - stderr.interface.flush() catch {}; std.process.exit(1); } } diff --git a/lib/std/Build.zig b/lib/std/Build.zig index b7300cb5df..de2985d2b5 100644 --- a/lib/std/Build.zig +++ b/lib/std/Build.zig @@ -2238,7 +2238,7 @@ pub const GeneratedFile = struct { /// This value must be set in the `fn make()` of the `step` and must not be `null` afterwards. path: ?[]const u8 = null, - /// Deprecated, see `getPath2`. + /// Deprecated, see `getPath3`. pub fn getPath(gen: GeneratedFile) []const u8 { return gen.step.owner.pathFromCwd(gen.path orelse std.debug.panic( "getPath() was called on a GeneratedFile that wasn't built yet. Is there a missing Step dependency on step '{s}'?", @@ -2246,11 +2246,18 @@ pub const GeneratedFile = struct { )); } + /// Deprecated, see `getPath3`. pub fn getPath2(gen: GeneratedFile, src_builder: *Build, asking_step: ?*Step) []const u8 { + return getPath3(gen, src_builder, asking_step) catch |err| switch (err) { + error.Canceled => std.process.exit(1), + }; + } + + pub fn getPath3(gen: GeneratedFile, src_builder: *Build, asking_step: ?*Step) Io.Cancelable![]const u8 { return gen.path orelse { - const stderr = std.debug.lockStderrWriter(&.{}); + const io = gen.step.owner.graph.io; + const stderr = try io.lockStderrWriter(&.{}); dumpBadGetPathHelp(gen.step, &stderr.interface, stderr.mode, src_builder, asking_step) catch {}; - std.debug.unlockStderrWriter(); @panic("misconfigured build script"); }; } @@ -2425,22 +2432,29 @@ pub const LazyPath = union(enum) { } } - /// Deprecated, see `getPath3`. + /// Deprecated, see `getPath4`. pub fn getPath(lazy_path: LazyPath, src_builder: *Build) []const u8 { return getPath2(lazy_path, src_builder, null); } - /// Deprecated, see `getPath3`. + /// Deprecated, see `getPath4`. pub fn getPath2(lazy_path: LazyPath, src_builder: *Build, asking_step: ?*Step) []const u8 { const p = getPath3(lazy_path, src_builder, asking_step); return src_builder.pathResolve(&.{ p.root_dir.path orelse ".", p.sub_path }); } + /// Deprecated, see `getPath4`. + pub fn getPath3(lazy_path: LazyPath, src_builder: *Build, asking_step: ?*Step) Cache.Path { + return getPath4(lazy_path, src_builder, asking_step) catch |err| switch (err) { + error.Canceled => std.process.exit(1), + }; + } + /// Intended to be used during the make phase only. /// /// `asking_step` is only used for debugging purposes; it's the step being /// run that is asking for the path. - pub fn getPath3(lazy_path: LazyPath, src_builder: *Build, asking_step: ?*Step) Cache.Path { + pub fn getPath4(lazy_path: LazyPath, src_builder: *Build, asking_step: ?*Step) Io.Cancelable!Cache.Path { switch (lazy_path) { .src_path => |sp| return .{ .root_dir = sp.owner.build_root, @@ -2457,9 +2471,10 @@ pub const LazyPath = union(enum) { var file_path: Cache.Path = .{ .root_dir = Cache.Directory.cwd(), .sub_path = gen.file.path orelse { - const stderr = std.debug.lockStderrWriter(&.{}); + const io = src_builder.graph.io; + const stderr = try io.lockStderrWriter(&.{}); dumpBadGetPathHelp(gen.file.step, &stderr.interface, stderr.mode, src_builder, asking_step) catch {}; - std.debug.unlockStderrWriter(); + io.unlockStderrWriter(); @panic("misconfigured build script"); }, }; diff --git a/lib/std/Build/Fuzz.zig b/lib/std/Build/Fuzz.zig index c2b7d1e9e9..4770e03a0c 100644 --- a/lib/std/Build/Fuzz.zig +++ b/lib/std/Build/Fuzz.zig @@ -158,6 +158,7 @@ fn rebuildTestsWorkerRun(run: *Step.Run, gpa: Allocator, parent_prog_node: std.P } fn rebuildTestsWorkerRunFallible(run: *Step.Run, gpa: Allocator, parent_prog_node: std.Progress.Node) !void { + const io = run.step.owner.graph.io; const compile = run.producer.?; const prog_node = parent_prog_node.start(compile.step.name, 0); defer prog_node.end(); @@ -170,8 +171,8 @@ fn rebuildTestsWorkerRunFallible(run: *Step.Run, gpa: Allocator, parent_prog_nod if (show_error_msgs or show_compile_errors or show_stderr) { var buf: [256]u8 = undefined; - const stderr = std.debug.lockStderrWriter(&buf); - defer std.debug.unlockStderrWriter(); + const stderr = try io.lockStderrWriter(&buf); + defer io.unlockStderrWriter(); build_runner.printErrorMessages(gpa, &compile.step, .{}, &stderr.interface, stderr.mode, .verbose, .indent) catch {}; } @@ -182,12 +183,10 @@ fn rebuildTestsWorkerRunFallible(run: *Step.Run, gpa: Allocator, parent_prog_nod run.rebuilt_executable = try rebuilt_bin_path.join(gpa, compile.out_filename); } -fn fuzzWorkerRun( - fuzz: *Fuzz, - run: *Step.Run, - unit_test_index: u32, -) void { - const gpa = run.step.owner.allocator; +fn fuzzWorkerRun(fuzz: *Fuzz, run: *Step.Run, unit_test_index: u32) void { + const owner = run.step.owner; + const gpa = owner.allocator; + const io = owner.graph.io; const test_name = run.cached_test_metadata.?.testName(unit_test_index); const prog_node = fuzz.prog_node.start(test_name, 0); @@ -196,8 +195,10 @@ fn fuzzWorkerRun( run.rerunInFuzzMode(fuzz, unit_test_index, prog_node) catch |err| switch (err) { error.MakeFailed => { var buf: [256]u8 = undefined; - const stderr = std.debug.lockStderrWriter(&buf); - defer std.debug.unlockStderrWriter(); + const stderr = io.lockStderrWriter(&buf) catch |e| switch (e) { + error.Canceled => return, + }; + defer io.unlockStderrWriter(); build_runner.printErrorMessages(gpa, &run.step, .{}, &stderr.interface, stderr.mode, .verbose, .indent) catch {}; return; }, diff --git a/lib/std/Build/Step/Compile.zig b/lib/std/Build/Step/Compile.zig index d703e55b87..888775884d 100644 --- a/lib/std/Build/Step/Compile.zig +++ b/lib/std/Build/Step/Compile.zig @@ -922,20 +922,23 @@ const CliNamedModules = struct { } }; -fn getGeneratedFilePath(compile: *Compile, comptime tag_name: []const u8, asking_step: ?*Step) []const u8 { +fn getGeneratedFilePath(compile: *Compile, comptime tag_name: []const u8, asking_step: ?*Step) ![]const u8 { + const step = &compile.step; + const b = step.owner; + const io = b.graph.io; const maybe_path: ?*GeneratedFile = @field(compile, tag_name); const generated_file = maybe_path orelse { - const stderr = std.debug.lockStderrWriter(&.{}); + const stderr = try io.lockStderrWriter(&.{}); std.Build.dumpBadGetPathHelp(&compile.step, &stderr.interface, stderr.mode, compile.step.owner, asking_step) catch {}; - std.debug.unlockStderrWriter(); + io.unlockStderrWriter(); @panic("missing emit option for " ++ tag_name); }; const path = generated_file.path orelse { - const stderr = std.debug.lockStderrWriter(&.{}); + const stderr = try io.lockStderrWriter(&.{}); std.Build.dumpBadGetPathHelp(&compile.step, &stderr.interface, stderr.mode, compile.step.owner, asking_step) catch {}; - std.debug.unlockStderrWriter(); + io.unlockStderrWriter(); @panic(tag_name ++ " is null. Is there a missing step dependency?"); }; @@ -1149,9 +1152,9 @@ fn getZigArgs(compile: *Compile, fuzz: bool) ![][]const u8 { // For everything else, we directly link // against the library file. const full_path_lib = if (other_produces_implib) - other.getGeneratedFilePath("generated_implib", &compile.step) + try other.getGeneratedFilePath("generated_implib", &compile.step) else - other.getGeneratedFilePath("generated_bin", &compile.step); + try other.getGeneratedFilePath("generated_bin", &compile.step); try zig_args.append(full_path_lib); total_linker_objects += 1; diff --git a/lib/std/Build/Step/Run.zig b/lib/std/Build/Step/Run.zig index f1e5313905..67cf201374 100644 --- a/lib/std/Build/Step/Run.zig +++ b/lib/std/Build/Step/Run.zig @@ -1559,6 +1559,7 @@ fn spawnChildAndCollect( ) !?EvalGenericResult { const b = run.step.owner; const arena = b.allocator; + const io = b.graph.io; if (fuzz_context != null) { assert(!has_side_effects); @@ -1625,10 +1626,10 @@ fn spawnChildAndCollect( child.progress_node = options.progress_node; } if (inherit) { - const stderr = std.debug.lockStderrWriter(&.{}); + const stderr = try io.lockStderrWriter(&.{}); try setColorEnvironmentVariables(run, env_map, stderr.mode); } - defer if (inherit) std.debug.unlockStderrWriter(); + defer if (inherit) io.unlockStderrWriter(); var timer = try std.time.Timer.start(); const res = try evalGeneric(run, &child); run.step.result_duration_ns = timer.read(); diff --git a/lib/std/Build/WebServer.zig b/lib/std/Build/WebServer.zig index ccb7159192..a2b35e3522 100644 --- a/lib/std/Build/WebServer.zig +++ b/lib/std/Build/WebServer.zig @@ -655,7 +655,7 @@ fn buildClientWasm(ws: *WebServer, arena: Allocator, optimize: std.builtin.Optim } if (result_error_bundle.errorMessageCount() > 0) { - result_error_bundle.renderToStdErr(.{}, .auto); + try result_error_bundle.renderToStderr(io, .{}, .auto); log.err("the following command failed with {d} compilation errors:\n{s}", .{ result_error_bundle.errorMessageCount(), try Build.Step.allocPrintCmd(arena, null, argv.items), diff --git a/lib/std/Progress.zig b/lib/std/Progress.zig index 603f53091e..6edbf9e471 100644 --- a/lib/std/Progress.zig +++ b/lib/std/Progress.zig @@ -764,7 +764,7 @@ fn appendTreeSymbol(symbol: TreeSymbol, buf: []u8, start_i: usize) usize { } } -pub fn clearWrittenWithEscapeCodes(file_writer: *Io.File.Writer) anyerror!void { +pub fn clearWrittenWithEscapeCodes(file_writer: *Io.File.Writer) Io.Writer.Error!void { if (noop_impl or !global_progress.need_clear) return; try file_writer.writeAllUnescaped(clear ++ progress_remove); global_progress.need_clear = false; diff --git a/lib/std/json/dynamic.zig b/lib/std/json/dynamic.zig index c3cccd1a91..f85916670b 100644 --- a/lib/std/json/dynamic.zig +++ b/lib/std/json/dynamic.zig @@ -47,10 +47,9 @@ pub const Value = union(enum) { } pub fn dump(v: Value) void { - const w, _ = std.debug.lockStderrWriter(&.{}); + const stderr = std.debug.lockStderrWriter(&.{}); defer std.debug.unlockStderrWriter(); - - json.Stringify.value(v, .{}, w) catch return; + json.Stringify.value(v, .{}, &stderr.interface) catch return; } pub fn jsonStringify(value: @This(), jws: anytype) !void { diff --git a/lib/std/process.zig b/lib/std/process.zig index 216e793cbe..4b4c27fe20 100644 --- a/lib/std/process.zig +++ b/lib/std/process.zig @@ -1849,7 +1849,7 @@ pub fn totalSystemMemory() TotalSystemMemoryError!u64 { /// and does not return. pub fn cleanExit(io: Io) void { if (builtin.mode == .Debug) return; - _ = io.lockStderrWriter(&.{}); + _ = io.lockStderrWriter(&.{}) catch {}; exit(0); } diff --git a/lib/std/testing.zig b/lib/std/testing.zig index 092b65d71a..64ea3d1a4d 100644 --- a/lib/std/testing.zig +++ b/lib/std/testing.zig @@ -368,13 +368,21 @@ pub fn expectEqualSlices(comptime T: type, expected: []const T, actual: []const break :diff_index if (expected.len == actual.len) return else shortest; }; if (!backend_can_print) return error.TestExpectedEqual; - const stderr = std.debug.lockStderrWriter(&.{}); - defer std.debug.unlockStderrWriter(); - failEqualSlices(T, expected, actual, diff_index, &stderr.interface, stderr.mode) catch {}; + if (io.lockStderrWriter(&.{})) |stderr| { + defer io.unlockStderrWriter(); + failEqualSlices(T, expected, actual, diff_index, &stderr.interface, stderr.mode) catch {}; + } else |_| {} return error.TestExpectedEqual; } -fn failEqualSlices(comptime T: type, expected: []const T, actual: []const T, diff_index: usize, w: *Io.Writer, fwm: Io.File.Writer.Mode) !void { +fn failEqualSlices( + comptime T: type, + expected: []const T, + actual: []const T, + diff_index: usize, + w: *Io.Writer, + fwm: Io.File.Writer.Mode, +) !void { try w.print("slices differ. first difference occurs at index {d} (0x{X})\n", .{ diff_index, diff_index }); // TODO: Should this be configurable by the caller? diff --git a/lib/std/zig.zig b/lib/std/zig.zig index b6203326f2..a094dee83f 100644 --- a/lib/std/zig.zig +++ b/lib/std/zig.zig @@ -639,7 +639,7 @@ pub fn readSourceFileToEndAlloc(gpa: Allocator, file_reader: *Io.File.Reader) ![ return buffer.toOwnedSliceSentinel(gpa, 0); } -pub fn printAstErrorsToStderr(gpa: Allocator, tree: Ast, path: []const u8, color: Color) !void { +pub fn printAstErrorsToStderr(gpa: Allocator, io: Io, tree: Ast, path: []const u8, color: Color) !void { var wip_errors: std.zig.ErrorBundle.Wip = undefined; try wip_errors.init(gpa); defer wip_errors.deinit(); @@ -648,7 +648,7 @@ pub fn printAstErrorsToStderr(gpa: Allocator, tree: Ast, path: []const u8, color var error_bundle = try wip_errors.toOwnedBundle(""); defer error_bundle.deinit(gpa); - error_bundle.renderToStdErr(.{}, color); + error_bundle.renderToStderr(io, .{}, color); } pub fn putAstErrorsIntoBundle( diff --git a/lib/std/zig/ErrorBundle.zig b/lib/std/zig/ErrorBundle.zig index cc6182c8aa..0342874cf9 100644 --- a/lib/std/zig/ErrorBundle.zig +++ b/lib/std/zig/ErrorBundle.zig @@ -162,11 +162,13 @@ pub const RenderOptions = struct { include_log_text: bool = true, }; -pub fn renderToStdErr(eb: ErrorBundle, options: RenderOptions, color: std.zig.Color) void { +pub const RenderToStderrError = Io.Cancelable || Io.File.Writer.Mode.SetColorError; + +pub fn renderToStderr(eb: ErrorBundle, io: Io, options: RenderOptions, color: std.zig.Color) RenderToStderrError!void { var buffer: [256]u8 = undefined; - const stderr = std.debug.lockStderrWriter(&buffer); - defer std.debug.unlockStderrWriter(); - renderToWriter(eb, options, &stderr.interface, color.getTtyConf(stderr.mode)) catch return; + const stderr = try io.lockStderrWriter(&buffer); + defer io.unlockStderrWriter(); + try renderToWriter(eb, options, &stderr.interface, color.getTtyConf(stderr.mode)); } pub fn renderToWriter( diff --git a/lib/std/zig/parser_test.zig b/lib/std/zig/parser_test.zig index 9d9a6a48ce..5318eab2df 100644 --- a/lib/std/zig/parser_test.zig +++ b/lib/std/zig/parser_test.zig @@ -1,7 +1,6 @@ const std = @import("std"); -const mem = std.mem; -const print = std.debug.print; -const maxInt = std.math.maxInt; +const Io = std.Io; +const Allocator = std.mem.Allocator; test "zig fmt: remove extra whitespace at start and end of file with comment between" { try testTransform( @@ -6332,10 +6331,10 @@ test "ampersand" { var fixed_buffer_mem: [100 * 1024]u8 = undefined; -fn testParse(source: [:0]const u8, allocator: mem.Allocator, anything_changed: *bool) ![]u8 { +fn testParse(io: Io, source: [:0]const u8, allocator: Allocator, anything_changed: *bool) ![]u8 { var buffer: [64]u8 = undefined; - const stderr = std.debug.lockStderrWriter(&buffer); - defer std.debug.unlockStderrWriter(); + const stderr = try io.lockStderrWriter(&buffer); + defer io.unlockStderrWriter(); var tree = try std.zig.Ast.parse(allocator, source, .zig); defer tree.deinit(allocator); @@ -6359,27 +6358,36 @@ fn testParse(source: [:0]const u8, allocator: mem.Allocator, anything_changed: * } const formatted = try tree.renderAlloc(allocator); - anything_changed.* = !mem.eql(u8, formatted, source); + anything_changed.* = !std.mem.eql(u8, formatted, source); return formatted; } -fn testTransformImpl(allocator: mem.Allocator, fba: *std.heap.FixedBufferAllocator, source: [:0]const u8, expected_source: []const u8) !void { +fn testTransformImpl( + io: Io, + allocator: Allocator, + fba: *std.heap.FixedBufferAllocator, + source: [:0]const u8, + expected_source: []const u8, +) !void { // reset the fixed buffer allocator each run so that it can be re-used for each // iteration of the failing index fba.reset(); var anything_changed: bool = undefined; - const result_source = try testParse(source, allocator, &anything_changed); + const result_source = try testParse(io, source, allocator, &anything_changed); try std.testing.expectEqualStrings(expected_source, result_source); const changes_expected = source.ptr != expected_source.ptr; if (anything_changed != changes_expected) { - print("std.zig.render returned {} instead of {}\n", .{ anything_changed, changes_expected }); + std.debug.print("std.zig.render returned {} instead of {}\n", .{ anything_changed, changes_expected }); return error.TestFailed; } try std.testing.expect(anything_changed == changes_expected); allocator.free(result_source); } fn testTransform(source: [:0]const u8, expected_source: []const u8) !void { + const io = std.testing.io; var fixed_allocator = std.heap.FixedBufferAllocator.init(fixed_buffer_mem[0..]); - return std.testing.checkAllAllocationFailures(fixed_allocator.allocator(), testTransformImpl, .{ &fixed_allocator, source, expected_source }); + return std.testing.checkAllAllocationFailures(fixed_allocator.allocator(), testTransformImpl, .{ + io, &fixed_allocator, source, expected_source, + }); } fn testCanonical(source: [:0]const u8) !void { return testTransform(source, source); diff --git a/src/Air/print.zig b/src/Air/print.zig index 95c8a1fcda..05614a0ed3 100644 --- a/src/Air/print.zig +++ b/src/Air/print.zig @@ -9,7 +9,7 @@ const Type = @import("../Type.zig"); const Air = @import("../Air.zig"); const InternPool = @import("../InternPool.zig"); -pub fn write(air: Air, stream: *std.Io.Writer, pt: Zcu.PerThread, liveness: ?Air.Liveness) void { +pub fn write(air: Air, stream: *std.Io.Writer, pt: Zcu.PerThread, liveness: ?Air.Liveness) !void { comptime assert(build_options.enable_debug_extensions); const instruction_bytes = air.instructions.len * // Here we don't use @sizeOf(Air.Inst.Data) because it would include @@ -24,7 +24,7 @@ pub fn write(air: Air, stream: *std.Io.Writer, pt: Zcu.PerThread, liveness: ?Air liveness_special_bytes + tomb_bytes; // zig fmt: off - stream.print( + try stream.print( \\# Total AIR+Liveness bytes: {Bi} \\# AIR Instructions: {d} ({Bi}) \\# AIR Extra Data: {d} ({Bi}) @@ -39,7 +39,7 @@ pub fn write(air: Air, stream: *std.Io.Writer, pt: Zcu.PerThread, liveness: ?Air tomb_bytes, if (liveness) |l| l.extra.len else 0, liveness_extra_bytes, if (liveness) |l| l.special.count() else 0, liveness_special_bytes, - }) catch return; + }); // zig fmt: on var writer: Writer = .{ @@ -50,7 +50,7 @@ pub fn write(air: Air, stream: *std.Io.Writer, pt: Zcu.PerThread, liveness: ?Air .indent = 2, .skip_body = false, }; - writer.writeBody(stream, air.getMainBody()) catch return; + try writer.writeBody(stream, air.getMainBody()); } pub fn writeInst( @@ -73,15 +73,23 @@ pub fn writeInst( } pub fn dump(air: Air, pt: Zcu.PerThread, liveness: ?Air.Liveness) void { - const stderr_bw, _ = std.debug.lockStderrWriter(&.{}); - defer std.debug.unlockStderrWriter(); - air.write(stderr_bw, pt, liveness); + const comp = pt.zcu.comp; + const io = comp.io; + var buffer: [512]u8 = undefined; + const stderr = try io.lockStderrWriter(&buffer); + defer io.unlockStderrWriter(); + const w = &stderr.interface; + air.write(w, pt, liveness); } pub fn dumpInst(air: Air, inst: Air.Inst.Index, pt: Zcu.PerThread, liveness: ?Air.Liveness) void { - const stderr_bw, _ = std.debug.lockStderrWriter(&.{}); - defer std.debug.unlockStderrWriter(); - air.writeInst(stderr_bw, inst, pt, liveness); + const comp = pt.zcu.comp; + const io = comp.io; + var buffer: [512]u8 = undefined; + const stderr = try io.lockStderrWriter(&buffer); + defer io.unlockStderrWriter(); + const w = &stderr.interface; + air.writeInst(w, inst, pt, liveness); } const Writer = struct { diff --git a/src/Compilation.zig b/src/Compilation.zig index 9e76b8feca..47098317b5 100644 --- a/src/Compilation.zig +++ b/src/Compilation.zig @@ -2088,12 +2088,13 @@ pub fn create(gpa: Allocator, arena: Allocator, io: Io, diag: *CreateDiagnostic, if (options.verbose_llvm_cpu_features) { if (options.root_mod.resolved_target.llvm_cpu_features) |cf| print: { - const stderr_w, _ = std.debug.lockStderrWriter(&.{}); - defer std.debug.unlockStderrWriter(); - stderr_w.print("compilation: {s}\n", .{options.root_name}) catch break :print; - stderr_w.print(" target: {s}\n", .{try target.zigTriple(arena)}) catch break :print; - stderr_w.print(" cpu: {s}\n", .{target.cpu.model.name}) catch break :print; - stderr_w.print(" features: {s}\n", .{cf}) catch {}; + const stderr = try io.lockStderrWriter(&.{}); + defer io.unlockStderrWriter(); + const w = &stderr.interface; + w.print("compilation: {s}\n", .{options.root_name}) catch break :print; + w.print(" target: {s}\n", .{try target.zigTriple(arena)}) catch break :print; + w.print(" cpu: {s}\n", .{target.cpu.model.name}) catch break :print; + w.print(" features: {s}\n", .{cf}) catch {}; } } @@ -4257,12 +4258,13 @@ pub fn getAllErrorsAlloc(comp: *Compilation) error{OutOfMemory}!ErrorBundle { // However, we haven't reported any such error. // This is a compiler bug. print_ctx: { - var stderr_w, _ = std.debug.lockStderrWriter(&.{}); - defer std.debug.unlockStderrWriter(); - stderr_w.writeAll("referenced transitive analysis errors, but none actually emitted\n") catch break :print_ctx; - stderr_w.print("{f} [transitive failure]\n", .{zcu.fmtAnalUnit(failed_unit)}) catch break :print_ctx; + const stderr = try io.lockStderrWriter(&.{}); + defer io.unlockStderrWriter(); + const w = &stderr.interface; + w.writeAll("referenced transitive analysis errors, but none actually emitted\n") catch break :print_ctx; + w.print("{f} [transitive failure]\n", .{zcu.fmtAnalUnit(failed_unit)}) catch break :print_ctx; while (ref) |r| { - stderr_w.print("referenced by: {f}{s}\n", .{ + w.print("referenced by: {f}{s}\n", .{ zcu.fmtAnalUnit(r.referencer), if (zcu.transitive_failed_analysis.contains(r.referencer)) " [transitive failure]" else "", }) catch break :print_ctx; @@ -5756,7 +5758,7 @@ pub fn translateC( try argv.appendSlice(comp.global_cc_argv); try argv.appendSlice(owner_mod.cc_argv); try argv.appendSlice(&.{ source_path, "-o", translated_path }); - if (comp.verbose_cimport) dump_argv(argv.items); + if (comp.verbose_cimport) dumpArgv(io, argv.items); } var stdout: []u8 = undefined; @@ -6264,7 +6266,7 @@ fn updateCObject(comp: *Compilation, c_object: *CObject, c_obj_prog_node: std.Pr } if (comp.verbose_cc) { - dump_argv(argv.items); + dumpArgv(io, argv.items); } const err = std.process.execv(arena, argv.items); @@ -6310,7 +6312,7 @@ fn updateCObject(comp: *Compilation, c_object: *CObject, c_obj_prog_node: std.Pr } if (comp.verbose_cc) { - dump_argv(argv.items); + dumpArgv(io, argv.items); } // Just to save disk space, we delete the files that are never needed again. @@ -7773,17 +7775,22 @@ pub fn lockAndSetMiscFailure( return setMiscFailure(comp, tag, format, args); } -pub fn dump_argv(argv: []const []const u8) void { +pub fn dumpArgv(io: Io, argv: []const []const u8) Io.Cancelable!void { var buffer: [64]u8 = undefined; - const stderr, _ = std.debug.lockStderrWriter(&buffer); - defer std.debug.unlockStderrWriter(); - nosuspend { - for (argv, 0..) |arg, i| { - if (i != 0) stderr.writeByte(' ') catch return; - stderr.writeAll(arg) catch return; - } - stderr.writeByte('\n') catch return; + const stderr = try io.lockStderrWriter(&buffer); + defer io.unlockStderrWriter(); + const w = &stderr.interface; + return dumpArgvWriter(w, argv) catch |err| switch (err) { + error.WriteFailed => return stderr.err.?, + }; +} + +fn dumpArgvWriter(w: *Io.Writer, argv: []const []const u8) Io.Writer.Error!void { + for (argv, 0..) |arg, i| { + if (i != 0) try w.writeByte(' '); + try w.writeAll(arg); } + try w.writeByte('\n'); } pub fn getZigBackend(comp: Compilation) std.builtin.CompilerBackend { diff --git a/src/InternPool.zig b/src/InternPool.zig index 5568c493d9..539cb441c5 100644 --- a/src/InternPool.zig +++ b/src/InternPool.zig @@ -1,8 +1,11 @@ //! All interned objects have both a value and a type. //! This data structure is self-contained. +const InternPool = @This(); const builtin = @import("builtin"); + const std = @import("std"); +const Io = std.Io; const Allocator = std.mem.Allocator; const assert = std.debug.assert; const BigIntConst = std.math.big.int.Const; @@ -11,10 +14,9 @@ const Cache = std.Build.Cache; const Io = std.Io; const Limb = std.math.big.Limb; const Hash = std.hash.Wyhash; +const Zir = std.zig.Zir; -const InternPool = @This(); const Zcu = @import("Zcu.zig"); -const Zir = std.zig.Zir; /// One item per thread, indexed by `tid`, which is dense and unique per thread. locals: []Local, @@ -11165,12 +11167,16 @@ pub fn mutateVarInit(ip: *InternPool, io: Io, index: Index, init_index: Index) v @atomicStore(u32, &extra_items[item.data + std.meta.fieldIndex(Tag.Variable, "init").?], @intFromEnum(init_index), .release); } -pub fn dump(ip: *const InternPool) void { - dumpStatsFallible(ip, std.heap.page_allocator) catch return; - dumpAllFallible(ip) catch return; +pub fn dump(ip: *const InternPool, io: Io) Io.Cancelable!void { + var buffer: [4096]u8 = undefined; + const stderr_writer = try io.lockStderrWriter(&buffer); + defer io.unlockStderrWriter(); + const w = &stderr_writer.interface; + try dumpStatsFallible(ip, w, std.heap.page_allocator); + try dumpAllFallible(ip, w); } -fn dumpStatsFallible(ip: *const InternPool, arena: Allocator) anyerror!void { +fn dumpStatsFallible(ip: *const InternPool, w: *Io.Writer, arena: Allocator) anyerror!void { var items_len: usize = 0; var extra_len: usize = 0; var limbs_len: usize = 0; @@ -11423,18 +11429,13 @@ fn dumpStatsFallible(ip: *const InternPool, arena: Allocator) anyerror!void { }; counts.sort(SortContext{ .map = &counts }); const len = @min(50, counts.count()); - std.debug.print(" top 50 tags:\n", .{}); + w.print(" top 50 tags:\n", .{}); for (counts.keys()[0..len], counts.values()[0..len]) |tag, stats| { - std.debug.print(" {s}: {d} occurrences, {d} total bytes\n", .{ - @tagName(tag), stats.count, stats.bytes, - }); + w.print(" {t}: {d} occurrences, {d} total bytes\n", .{ tag, stats.count, stats.bytes }); } } -fn dumpAllFallible(ip: *const InternPool) anyerror!void { - var buffer: [4096]u8 = undefined; - const stderr_bw, _ = std.debug.lockStderrWriter(&buffer); - defer std.debug.unlockStderrWriter(); +fn dumpAllFallible(ip: *const InternPool, w: *Io.Writer) anyerror!void { for (ip.locals, 0..) |*local, tid| { const items = local.shared.items.view(); for ( @@ -11443,12 +11444,12 @@ fn dumpAllFallible(ip: *const InternPool) anyerror!void { 0.., ) |tag, data, index| { const i = Index.Unwrapped.wrap(.{ .tid = @enumFromInt(tid), .index = @intCast(index) }, ip); - try stderr_bw.print("${d} = {s}(", .{ i, @tagName(tag) }); + try w.print("${d} = {s}(", .{ i, @tagName(tag) }); switch (tag) { .removed => {}, - .simple_type => try stderr_bw.print("{s}", .{@tagName(@as(SimpleType, @enumFromInt(@intFromEnum(i))))}), - .simple_value => try stderr_bw.print("{s}", .{@tagName(@as(SimpleValue, @enumFromInt(@intFromEnum(i))))}), + .simple_type => try w.print("{s}", .{@tagName(@as(SimpleType, @enumFromInt(@intFromEnum(i))))}), + .simple_value => try w.print("{s}", .{@tagName(@as(SimpleValue, @enumFromInt(@intFromEnum(i))))}), .type_int_signed, .type_int_unsigned, @@ -11521,23 +11522,27 @@ fn dumpAllFallible(ip: *const InternPool) anyerror!void { .func_coerced, .union_value, .memoized_call, - => try stderr_bw.print("{d}", .{data}), + => try w.print("{d}", .{data}), .opt_null, .type_slice, .only_possible_value, - => try stderr_bw.print("${d}", .{data}), + => try w.print("${d}", .{data}), } - try stderr_bw.writeAll(")\n"); + try w.writeAll(")\n"); } } } -pub fn dumpGenericInstances(ip: *const InternPool, allocator: Allocator) void { - ip.dumpGenericInstancesFallible(allocator) catch return; +pub fn dumpGenericInstances(ip: *const InternPool, io: Io, allocator: Allocator) Io.Cancelable!void { + var buffer: [4096]u8 = undefined; + const stderr_writer = try io.lockStderrWriter(&buffer); + defer io.unlockStderrWriter(); + const w = &stderr_writer.interface; + try ip.dumpGenericInstancesFallible(allocator, w); } -pub fn dumpGenericInstancesFallible(ip: *const InternPool, allocator: Allocator) anyerror!void { +pub fn dumpGenericInstancesFallible(ip: *const InternPool, allocator: Allocator, w: *Io.Writer) !void { var arena_allocator = std.heap.ArenaAllocator.init(allocator); defer arena_allocator.deinit(); const arena = arena_allocator.allocator(); @@ -11564,10 +11569,6 @@ pub fn dumpGenericInstancesFallible(ip: *const InternPool, allocator: Allocator) } } - var buffer: [4096]u8 = undefined; - const stderr_bw, _ = std.debug.lockStderrWriter(&buffer); - defer std.debug.unlockStderrWriter(); - const SortContext = struct { values: []std.ArrayList(Index), pub fn lessThan(ctx: @This(), a_index: usize, b_index: usize) bool { @@ -11579,19 +11580,19 @@ pub fn dumpGenericInstancesFallible(ip: *const InternPool, allocator: Allocator) var it = instances.iterator(); while (it.next()) |entry| { const generic_fn_owner_nav = ip.getNav(ip.funcDeclInfo(entry.key_ptr.*).owner_nav); - try stderr_bw.print("{f} ({d}): \n", .{ generic_fn_owner_nav.name.fmt(ip), entry.value_ptr.items.len }); + try w.print("{f} ({d}): \n", .{ generic_fn_owner_nav.name.fmt(ip), entry.value_ptr.items.len }); for (entry.value_ptr.items) |index| { const unwrapped_index = index.unwrap(ip); const func = ip.extraFuncInstance(unwrapped_index.tid, unwrapped_index.getExtra(ip), unwrapped_index.getData(ip)); const owner_nav = ip.getNav(func.owner_nav); - try stderr_bw.print(" {f}: (", .{owner_nav.name.fmt(ip)}); + try w.print(" {f}: (", .{owner_nav.name.fmt(ip)}); for (func.comptime_args.get(ip)) |arg| { if (arg != .none) { const key = ip.indexToKey(arg); - try stderr_bw.print(" {} ", .{key}); + try w.print(" {} ", .{key}); } } - try stderr_bw.writeAll(")\n"); + try w.writeAll(")\n"); } } } diff --git a/src/Sema.zig b/src/Sema.zig index fec6850c4c..1f6a577f60 100644 --- a/src/Sema.zig +++ b/src/Sema.zig @@ -2668,16 +2668,18 @@ fn failWithTypeMismatch(sema: *Sema, block: *Block, src: LazySrcLoc, expected: T pub fn failWithOwnedErrorMsg(sema: *Sema, block: ?*Block, err_msg: *Zcu.ErrorMsg) error{ AnalysisFail, OutOfMemory } { @branchHint(.cold); - const gpa = sema.gpa; const zcu = sema.pt.zcu; + const comp = zcu.comp; + const gpa = comp.gpa; + const io = comp.io; - if (build_options.enable_debug_extensions and zcu.comp.debug_compile_errors) { + if (build_options.enable_debug_extensions and comp.debug_compile_errors) { var wip_errors: std.zig.ErrorBundle.Wip = undefined; wip_errors.init(gpa) catch @panic("out of memory"); Compilation.addModuleErrorMsg(zcu, &wip_errors, err_msg.*, false) catch @panic("out of memory"); std.debug.print("compile error during Sema:\n", .{}); var error_bundle = wip_errors.toOwnedBundle("") catch @panic("out of memory"); - error_bundle.renderToStdErr(.{}, .auto); + error_bundle.renderToStderr(io, .{}, .auto); std.debug.panicExtra(@returnAddress(), "unexpected compile error occurred", .{}); } diff --git a/src/Zcu/PerThread.zig b/src/Zcu/PerThread.zig index 2333d7f286..408f16bd74 100644 --- a/src/Zcu/PerThread.zig +++ b/src/Zcu/PerThread.zig @@ -4556,8 +4556,9 @@ fn runCodegenInner(pt: Zcu.PerThread, func_index: InternPool.Index, air: *Air) e defer if (liveness) |*l| l.deinit(gpa); if (build_options.enable_debug_extensions and comp.verbose_air) { - const stderr, _ = std.debug.lockStderrWriter(&.{}); - defer std.debug.unlockStderrWriter(); + const io = comp.io; + const stderr = try io.lockStderrWriter(&.{}); + defer io.unlockStderrWriter(); stderr.print("# Begin Function AIR: {f}:\n", .{fqn.fmt(ip)}) catch {}; air.write(stderr, pt, liveness); stderr.print("# End Function AIR: {f}\n\n", .{fqn.fmt(ip)}) catch {}; diff --git a/src/codegen/aarch64/Select.zig b/src/codegen/aarch64/Select.zig index f390d83f03..138c70fecf 100644 --- a/src/codegen/aarch64/Select.zig +++ b/src/codegen/aarch64/Select.zig @@ -11273,15 +11273,18 @@ fn initValueAdvanced( return @enumFromInt(isel.values.items.len); } pub fn dumpValues(isel: *Select, which: enum { only_referenced, all }) void { - errdefer |err| @panic(@errorName(err)); - const stderr, _ = std.debug.lockStderrWriter(&.{}); - defer std.debug.unlockStderrWriter(); - const zcu = isel.pt.zcu; + const io = zcu.comp.io; const gpa = zcu.gpa; const ip = &zcu.intern_pool; const nav = ip.getNav(isel.nav_index); + errdefer |err| @panic(@errorName(err)); + + const stderr_writer = io.lockStderrWriter(&.{}) catch return; + defer io.unlockStderrWriter(); + const stderr = &stderr_writer.interface; + var reverse_live_values: std.AutoArrayHashMapUnmanaged(Value.Index, std.ArrayList(Air.Inst.Index)) = .empty; defer { for (reverse_live_values.values()) |*list| list.deinit(gpa); diff --git a/src/crash_report.zig b/src/crash_report.zig index 76503fdc53..f23806b758 100644 --- a/src/crash_report.zig +++ b/src/crash_report.zig @@ -97,17 +97,18 @@ fn dumpCrashContext() Io.Writer.Error!void { // and the actual panic printing, which would be quite confusing. const stderr = std.debug.lockStderrWriter(&.{}); defer std.debug.unlockStderrWriter(); + const w = &stderr.interface; - try stderr.interface.writeAll("Compiler crash context:\n"); + try w.writeAll("Compiler crash context:\n"); if (CodegenFunc.current) |*cg| { const func_nav = cg.zcu.funcInfo(cg.func_index).owner_nav; const func_fqn = cg.zcu.intern_pool.getNav(func_nav).fqn; - try stderr.interface.print("Generating function '{f}'\n\n", .{func_fqn.fmt(&cg.zcu.intern_pool)}); + try w.print("Generating function '{f}'\n\n", .{func_fqn.fmt(&cg.zcu.intern_pool)}); } else if (AnalyzeBody.current) |anal| { - try dumpCrashContextSema(anal, &stderr.interface, &S.crash_heap); + try dumpCrashContextSema(anal, w, &S.crash_heap); } else { - try stderr.interface.writeAll("(no context)\n\n"); + try w.writeAll("(no context)\n\n"); } } fn dumpCrashContextSema(anal: *AnalyzeBody, stderr: *Io.Writer, crash_heap: []u8) Io.Writer.Error!void { diff --git a/src/fmt.zig b/src/fmt.zig index 55a68d1dc8..b9b1cc1363 100644 --- a/src/fmt.zig +++ b/src/fmt.zig @@ -124,7 +124,7 @@ pub fn run(gpa: Allocator, arena: Allocator, io: Io, args: []const []const u8) ! try wip_errors.addZirErrorMessages(zir, tree, source_code, ""); var error_bundle = try wip_errors.toOwnedBundle(""); defer error_bundle.deinit(gpa); - error_bundle.renderToStdErr(.{}, color); + error_bundle.renderToStderr(io, .{}, color); process.exit(2); } } else { @@ -138,7 +138,7 @@ pub fn run(gpa: Allocator, arena: Allocator, io: Io, args: []const []const u8) ! try wip_errors.addZoirErrorMessages(zoir, tree, source_code, ""); var error_bundle = try wip_errors.toOwnedBundle(""); defer error_bundle.deinit(gpa); - error_bundle.renderToStdErr(.{}, color); + error_bundle.renderToStderr(io, .{}, color); process.exit(2); } } @@ -319,7 +319,7 @@ fn fmtPathFile( try wip_errors.addZirErrorMessages(zir, tree, source_code, file_path); var error_bundle = try wip_errors.toOwnedBundle(""); defer error_bundle.deinit(gpa); - error_bundle.renderToStdErr(.{}, fmt.color); + error_bundle.renderToStderr(io, .{}, fmt.color); fmt.any_error = true; } }, @@ -334,7 +334,7 @@ fn fmtPathFile( try wip_errors.addZoirErrorMessages(zoir, tree, source_code, file_path); var error_bundle = try wip_errors.toOwnedBundle(""); defer error_bundle.deinit(gpa); - error_bundle.renderToStdErr(.{}, fmt.color); + error_bundle.renderToStderr(io, .{}, fmt.color); fmt.any_error = true; } }, diff --git a/src/libs/mingw.zig b/src/libs/mingw.zig index 05a9de71e7..a76fec9237 100644 --- a/src/libs/mingw.zig +++ b/src/libs/mingw.zig @@ -312,11 +312,17 @@ pub fn buildImportLib(comp: *Compilation, lib_name: []const u8) !void { const include_dir = try comp.dirs.zig_lib.join(arena, &.{ "libc", "mingw", "def-include" }); - if (comp.verbose_cc) print: { - var stderr, _ = std.debug.lockStderrWriter(&.{}); - defer std.debug.unlockStderrWriter(); - nosuspend stderr.print("def file: {s}\n", .{def_file_path}) catch break :print; - nosuspend stderr.print("include dir: {s}\n", .{include_dir}) catch break :print; + if (comp.verbose_cc) { + var buffer: [256]u8 = undefined; + const stderr = try io.lockStderrWriter(&buffer); + defer io.unlockStderrWriter(); + const w = &stderr.interface; + w.print("def file: {s}\n", .{def_file_path}) catch |err| switch (err) { + error.WriteFailed => return stderr.err.?, + }; + w.print("include dir: {s}\n", .{include_dir}) catch |err| switch (err) { + error.WriteFailed => return stderr.err.?, + }; } try aro_comp.search_path.append(gpa, .{ .path = include_dir, .kind = .normal }); @@ -333,11 +339,13 @@ pub fn buildImportLib(comp: *Compilation, lib_name: []const u8) !void { if (aro_comp.diagnostics.output.to_list.messages.items.len != 0) { var buffer: [64]u8 = undefined; - const w, const ttyconf = std.debug.lockStderrWriter(&buffer); - defer std.debug.unlockStderrWriter(); + const stderr = try io.lockStderrWriter(&buffer); + defer io.unlockStderrWriter(); for (aro_comp.diagnostics.output.to_list.messages.items) |msg| { if (msg.kind == .@"fatal error" or msg.kind == .@"error") { - msg.write(w, ttyconf, true) catch {}; + msg.write(&stderr.interface, stderr.mode, true) catch |err| switch (err) { + error.WriteFailed => return stderr.err.?, + }; return error.AroPreprocessorFailed; } } @@ -357,8 +365,9 @@ pub fn buildImportLib(comp: *Compilation, lib_name: []const u8) !void { error.OutOfMemory => |e| return e, error.ParseError => { var buffer: [64]u8 = undefined; - const w, _ = std.debug.lockStderrWriter(&buffer); - defer std.debug.unlockStderrWriter(); + const stderr = try io.lockStderrWriter(&buffer); + defer io.unlockStderrWriter(); + const w = &stderr.interface; try w.writeAll("error: "); try def_diagnostics.writeMsg(w, input); try w.writeByte('\n'); diff --git a/src/libs/mingw/def.zig b/src/libs/mingw/def.zig index 24dc95c13c..9a105b6182 100644 --- a/src/libs/mingw/def.zig +++ b/src/libs/mingw/def.zig @@ -1,4 +1,5 @@ const std = @import("std"); +const Io = std.Io; pub const ModuleDefinitionType = enum { mingw, @@ -663,7 +664,9 @@ test parse { \\ ; - try testParse(.AMD64, source, "foo.dll", &[_]ModuleDefinition.Export{ + const io = std.testing.io; + + try testParse(io, .AMD64, source, "foo.dll", &[_]ModuleDefinition.Export{ .{ .name = "foo", .mangled_symbol_name = null, @@ -743,7 +746,7 @@ test parse { }, }); - try testParse(.I386, source, "foo.dll", &[_]ModuleDefinition.Export{ + try testParse(io, .I386, source, "foo.dll", &[_]ModuleDefinition.Export{ .{ .name = "_foo", .mangled_symbol_name = null, @@ -823,7 +826,7 @@ test parse { }, }); - try testParse(.ARMNT, source, "foo.dll", &[_]ModuleDefinition.Export{ + try testParse(io, .ARMNT, source, "foo.dll", &[_]ModuleDefinition.Export{ .{ .name = "foo", .mangled_symbol_name = null, @@ -903,7 +906,7 @@ test parse { }, }); - try testParse(.ARM64, source, "foo.dll", &[_]ModuleDefinition.Export{ + try testParse(io, .ARM64, source, "foo.dll", &[_]ModuleDefinition.Export{ .{ .name = "foo", .mangled_symbol_name = null, @@ -997,7 +1000,9 @@ test "ntdll" { \\RtlActivateActivationContextUnsafeFast@0 ; - try testParse(.AMD64, source, "ntdll.dll", &[_]ModuleDefinition.Export{ + const io = std.testing.io; + + try testParse(io, .AMD64, source, "ntdll.dll", &[_]ModuleDefinition.Export{ .{ .name = "RtlDispatchAPC@12", .mangled_symbol_name = null, @@ -1023,15 +1028,22 @@ test "ntdll" { }); } -fn testParse(machine_type: std.coff.IMAGE.FILE.MACHINE, source: [:0]const u8, expected_module_name: []const u8, expected_exports: []const ModuleDefinition.Export) !void { +fn testParse( + io: Io, + machine_type: std.coff.IMAGE.FILE.MACHINE, + source: [:0]const u8, + expected_module_name: []const u8, + expected_exports: []const ModuleDefinition.Export, +) !void { var diagnostics: Diagnostics = undefined; const module = parse(std.testing.allocator, source, machine_type, .mingw, &diagnostics) catch |err| switch (err) { error.OutOfMemory => |e| return e, error.ParseError => { - const stderr, _ = std.debug.lockStderrWriter(&.{}); - defer std.debug.unlockStderrWriter(); - try diagnostics.writeMsg(stderr, source); - try stderr.writeByte('\n'); + const stderr = try io.lockStderrWriter(&.{}); + defer io.unlockStderrWriter(); + const w = &stderr.interface; + try diagnostics.writeMsg(w, source); + try w.writeByte('\n'); return err; }, }; diff --git a/src/link.zig b/src/link.zig index e37ba98cd9..a4729f296c 100644 --- a/src/link.zig +++ b/src/link.zig @@ -2246,7 +2246,7 @@ fn resolvePathInputLib( var error_bundle = try wip_errors.toOwnedBundle(""); defer error_bundle.deinit(gpa); - error_bundle.renderToStdErr(.{}, color); + error_bundle.renderToStderr(io, .{}, color); std.process.exit(1); } diff --git a/src/link/Coff.zig b/src/link/Coff.zig index 8954266e3f..d379669613 100644 --- a/src/link/Coff.zig +++ b/src/link/Coff.zig @@ -4,6 +4,7 @@ const builtin = @import("builtin"); const native_endian = builtin.cpu.arch.endian(); const std = @import("std"); +const Io = std.Io; const assert = std.debug.assert; const log = std.log.scoped(.link); @@ -2377,10 +2378,16 @@ pub fn deleteExport(coff: *Coff, exported: Zcu.Exported, name: InternPool.NullTe _ = name; } -pub fn dump(coff: *Coff, tid: Zcu.PerThread.Id) void { - const w, _ = std.debug.lockStderrWriter(&.{}); - defer std.debug.unlockStderrWriter(); - coff.printNode(tid, w, .root, 0) catch {}; +pub fn dump(coff: *Coff, tid: Zcu.PerThread.Id) Io.Cancelable!void { + const comp = coff.base.comp; + const io = comp.io; + var buffer: [512]u8 = undefined; + const stderr = try io.lockStderrWriter(&buffer); + defer io.unlockStderrWriter(); + const w = &stderr.interface; + coff.printNode(tid, w, .root, 0) catch |err| switch (err) { + error.WriteFailed => return stderr.err.?, + }; } pub fn printNode( diff --git a/src/link/Elf2.zig b/src/link/Elf2.zig index fc4794a7b0..3c511f1ee9 100644 --- a/src/link/Elf2.zig +++ b/src/link/Elf2.zig @@ -3729,10 +3729,16 @@ pub fn deleteExport(elf: *Elf, exported: Zcu.Exported, name: InternPool.NullTerm _ = name; } -pub fn dump(elf: *Elf, tid: Zcu.PerThread.Id) void { - const w, _ = std.debug.lockStderrWriter(&.{}); - defer std.debug.unlockStderrWriter(); - elf.printNode(tid, w, .root, 0) catch {}; +pub fn dump(elf: *Elf, tid: Zcu.PerThread.Id) Io.Cancelable!void { + const comp = elf.base.comp; + const io = comp.io; + var buffer: [512]u8 = undefined; + const stderr = try io.lockStderrWriter(&buffer); + defer io.unlockStderrWriter(); + const w = &stderr.interface; + elf.printNode(tid, w, .root, 0) catch |err| switch (err) { + error.WriteFailed => return stderr.err.?, + }; } pub fn printNode( diff --git a/src/main.zig b/src/main.zig index 115c1b5212..f735f671c6 100644 --- a/src/main.zig +++ b/src/main.zig @@ -4429,9 +4429,9 @@ fn runOrTest( // the error message and invocation below. if (process.can_execv and arg_mode == .run) { // execv releases the locks; no need to destroy the Compilation here. - _ = std.debug.lockStderrWriter(&.{}); + _ = try io.lockStderrWriter(&.{}); const err = process.execve(gpa, argv.items, &env_map); - std.debug.unlockStderrWriter(); + io.unlockStderrWriter(); try warnAboutForeignBinaries(io, arena, arg_mode, target, link_libc); const cmd = try std.mem.join(arena, " ", argv.items); fatal("the following command failed to execve with '{t}':\n{s}", .{ err, cmd }); @@ -4448,8 +4448,8 @@ fn runOrTest( comp_destroyed.* = true; const term_result = t: { - _ = std.debug.lockStderrWriter(); - defer std.debug.unlockStderrWriter(); + _ = try io.lockStderrWriter(&.{}); + defer io.unlockStderrWriter(); break :t child.spawnAndWait(io); }; const term = term_result catch |err| { @@ -4606,7 +4606,8 @@ fn updateModule(comp: *Compilation, color: Color, prog_node: std.Progress.Node) defer errors.deinit(comp.gpa); if (errors.errorMessageCount() > 0) { - errors.renderToStdErr(.{}, color); + const io = comp.io; + errors.renderToStderr(io, .{}, color); return error.CompileErrorsReported; } } @@ -4659,7 +4660,7 @@ fn cmdTranslateC( return; } else { const color: Color = .auto; - result.errors.renderToStdErr(.{}, color); + result.errors.renderToStderr(io, .{}, color); process.exit(1); } } @@ -5280,7 +5281,7 @@ fn cmdBuild(gpa: Allocator, arena: Allocator, io: Io, args: []const []const u8) if (fetch.error_bundle.root_list.items.len > 0) { var errors = try fetch.error_bundle.toOwnedBundle(""); - errors.renderToStdErr(.{}, color); + errors.renderToStderr(io, .{}, color); process.exit(1); } @@ -5412,8 +5413,8 @@ fn cmdBuild(gpa: Allocator, arena: Allocator, io: Io, args: []const []const u8) child.stderr_behavior = .Inherit; const term = t: { - _ = std.debug.lockStderrWriter(&.{}); - defer std.debug.unlockStderrWriter(); + _ = try io.lockStderrWriter(&.{}); + defer io.unlockStderrWriter(); break :t child.spawnAndWait(io) catch |err| fatal("failed to spawn build runner {s}: {t}", .{ child_argv.items[0], err }); }; @@ -6212,7 +6213,7 @@ fn cmdAstCheck(arena: Allocator, io: Io, args: []const []const u8) !void { try wip_errors.init(arena); try wip_errors.addZirErrorMessages(zir, tree, source, display_path); var error_bundle = try wip_errors.toOwnedBundle(""); - error_bundle.renderToStdErr(.{}, color); + error_bundle.renderToStderr(io, .{}, color); if (zir.loweringFailed()) { process.exit(1); } @@ -6283,7 +6284,7 @@ fn cmdAstCheck(arena: Allocator, io: Io, args: []const []const u8) !void { try wip_errors.init(arena); try wip_errors.addZoirErrorMessages(zoir, tree, source, display_path); var error_bundle = try wip_errors.toOwnedBundle(""); - error_bundle.renderToStdErr(.{}, color); + error_bundle.renderToStderr(io, .{}, color); process.exit(1); } @@ -6557,7 +6558,7 @@ fn cmdChangelist(arena: Allocator, io: Io, args: []const []const u8) !void { try wip_errors.init(arena); try wip_errors.addZirErrorMessages(old_zir, old_tree, old_source, old_source_path); var error_bundle = try wip_errors.toOwnedBundle(""); - error_bundle.renderToStdErr(.{}, color); + error_bundle.renderToStderr(io, .{}, color); process.exit(1); } @@ -6569,7 +6570,7 @@ fn cmdChangelist(arena: Allocator, io: Io, args: []const []const u8) !void { try wip_errors.init(arena); try wip_errors.addZirErrorMessages(new_zir, new_tree, new_source, new_source_path); var error_bundle = try wip_errors.toOwnedBundle(""); - error_bundle.renderToStdErr(.{}, color); + error_bundle.renderToStderr(io, .{}, color); process.exit(1); } @@ -7005,7 +7006,7 @@ fn cmdFetch( if (fetch.error_bundle.root_list.items.len > 0) { var errors = try fetch.error_bundle.toOwnedBundle(""); - errors.renderToStdErr(.{}, color); + errors.renderToStderr(io, .{}, color); process.exit(1); } @@ -7345,7 +7346,7 @@ fn loadManifest( errdefer ast.deinit(gpa); if (ast.errors.len > 0) { - try std.zig.printAstErrorsToStderr(gpa, ast, Package.Manifest.basename, options.color); + try std.zig.printAstErrorsToStderr(gpa, io, ast, Package.Manifest.basename, options.color); process.exit(2); } @@ -7362,7 +7363,7 @@ fn loadManifest( var error_bundle = try wip_errors.toOwnedBundle(""); defer error_bundle.deinit(gpa); - error_bundle.renderToStdErr(.{}, options.color); + error_bundle.renderToStderr(io, .{}, options.color); process.exit(2); } -- cgit v1.2.3 From aa57793b680b3da05f1d888b4df15807905e57c8 Mon Sep 17 00:00:00 2001 From: Andrew Kelley Date: Wed, 17 Dec 2025 15:47:33 -0800 Subject: std: rework locking stderr --- lib/std/Io.zig | 46 +++++---- lib/std/Io/File/Reader.zig | 34 +++--- lib/std/Io/File/Writer.zig | 250 +-------------------------------------------- lib/std/Io/Terminal.zig | 154 ++++++++++++++++++++++++++++ lib/std/Io/Threaded.zig | 83 +++++++++------ lib/std/Io/Writer.zig | 2 +- lib/std/Progress.zig | 36 +++---- lib/std/debug.zig | 232 +++++++++++++++++++++-------------------- lib/std/log.zig | 34 +++--- lib/std/process.zig | 7 ++ 10 files changed, 409 insertions(+), 469 deletions(-) create mode 100644 lib/std/Io/Terminal.zig (limited to 'lib/std/process.zig') diff --git a/lib/std/Io.zig b/lib/std/Io.zig index 83a2e940bf..6fa05b16d2 100644 --- a/lib/std/Io.zig +++ b/lib/std/Io.zig @@ -557,13 +557,6 @@ pub const net = @import("Io/net.zig"); userdata: ?*anyopaque, vtable: *const VTable, -/// This is the global, process-wide protection to coordinate stderr writes. -/// -/// The primary motivation for recursive mutex here is so that a panic while -/// stderr mutex is held still dumps the stack trace and other debug -/// information. -pub var stderr_thread_mutex: std.Thread.Mutex.Recursive = .init; - pub const VTable = struct { /// If it returns `null` it means `result` has been already populated and /// `await` will be a no-op. @@ -719,9 +712,9 @@ pub const VTable = struct { processExecutableOpen: *const fn (?*anyopaque, File.OpenFlags) std.process.OpenExecutableError!File, processExecutablePath: *const fn (?*anyopaque, buffer: []u8) std.process.ExecutablePathError!usize, - lockStderrWriter: *const fn (?*anyopaque, buffer: []u8) Cancelable!*File.Writer, - tryLockStderrWriter: *const fn (?*anyopaque, buffer: []u8) ?*File.Writer, - unlockStderrWriter: *const fn (?*anyopaque) void, + lockStderr: *const fn (?*anyopaque, buffer: []u8, ?Terminal.Mode) Cancelable!LockedStderr, + tryLockStderr: *const fn (?*anyopaque, buffer: []u8) Cancelable!?LockedStderr, + unlockStderr: *const fn (?*anyopaque) void, now: *const fn (?*anyopaque, Clock) Clock.Error!Timestamp, sleep: *const fn (?*anyopaque, Timeout) SleepError!void, @@ -763,6 +756,7 @@ pub const UnexpectedError = error{ pub const Dir = @import("Io/Dir.zig"); pub const File = @import("Io/File.zig"); +pub const Terminal = @import("Io/Terminal.zig"); pub const Clock = enum { /// A settable system-wide clock that measures real (i.e. wall-clock) @@ -2177,22 +2171,34 @@ pub fn select(io: Io, s: anytype) Cancelable!SelectUnion(@TypeOf(s)) { } } +pub const LockedStderr = struct { + file_writer: *File.Writer, + terminal_mode: Terminal.Mode, + + pub fn terminal(ls: LockedStderr) Terminal { + return .{ + .writer = &ls.file_writer.interface, + .mode = ls.terminal_mode, + }; + } +}; + /// For doing application-level writes to the standard error stream. /// Coordinates also with debug-level writes that are ignorant of Io interface -/// and implementations. When this returns, `stderr_thread_mutex` will be -/// locked. +/// and implementations. When this returns, `std.process.stderr_thread_mutex` +/// will be locked. /// /// See also: -/// * `tryLockStderrWriter` -pub fn lockStderrWriter(io: Io, buffer: []u8) Cancelable!*File.Writer { - return io.vtable.lockStderrWriter(io.userdata, buffer); +/// * `tryLockStderr` +pub fn lockStderr(io: Io, buffer: []u8, terminal_mode: ?Terminal.Mode) Cancelable!LockedStderr { + return io.vtable.lockStderr(io.userdata, buffer, terminal_mode); } -/// Same as `lockStderrWriter` but uncancelable and non-blocking. -pub fn tryLockStderrWriter(io: Io, buffer: []u8) ?*File.Writer { - return io.vtable.tryLockStderrWriter(io.userdata, buffer); +/// Same as `lockStderr` but non-blocking. +pub fn tryLockStderr(io: Io, buffer: []u8, terminal_mode: ?Terminal.Mode) Cancelable!?LockedStderr { + return io.vtable.tryLockStderr(io.userdata, buffer, terminal_mode); } -pub fn unlockStderrWriter(io: Io) void { - return io.vtable.unlockStderrWriter(io.userdata); +pub fn unlockStderr(io: Io) void { + return io.vtable.unlockStderr(io.userdata); } diff --git a/lib/std/Io/File/Reader.zig b/lib/std/Io/File/Reader.zig index 8b2074a0aa..792b20a716 100644 --- a/lib/std/Io/File/Reader.zig +++ b/lib/std/Io/File/Reader.zig @@ -64,24 +64,24 @@ pub const Mode = enum { streaming, positional, /// Avoid syscalls other than `read` and `readv`. - streaming_reading, + streaming_simple, /// Avoid syscalls other than `pread` and `preadv`. - positional_reading, + positional_simple, /// Indicates reading cannot continue because of a seek failure. failure, pub fn toStreaming(m: @This()) @This() { return switch (m) { .positional, .streaming => .streaming, - .positional_reading, .streaming_reading => .streaming_reading, + .positional_simple, .streaming_simple => .streaming_simple, .failure => .failure, }; } - pub fn toReading(m: @This()) @This() { + pub fn toSimple(m: @This()) @This() { return switch (m) { - .positional, .positional_reading => .positional_reading, - .streaming, .streaming_reading => .streaming_reading, + .positional, .positional_simple => .positional_simple, + .streaming, .streaming_simple => .streaming_simple, .failure => .failure, }; } @@ -153,10 +153,10 @@ pub fn getSize(r: *Reader) SizeError!u64 { pub fn seekBy(r: *Reader, offset: i64) SeekError!void { const io = r.io; switch (r.mode) { - .positional, .positional_reading => { + .positional, .positional_simple => { setLogicalPos(r, @intCast(@as(i64, @intCast(logicalPos(r))) + offset)); }, - .streaming, .streaming_reading => { + .streaming, .streaming_simple => { const seek_err = r.seek_err orelse e: { if (io.vtable.fileSeekBy(io.userdata, r.file, offset)) |_| { setLogicalPos(r, @intCast(@as(i64, @intCast(logicalPos(r))) + offset)); @@ -183,10 +183,10 @@ pub fn seekBy(r: *Reader, offset: i64) SeekError!void { pub fn seekTo(r: *Reader, offset: u64) SeekError!void { const io = r.io; switch (r.mode) { - .positional, .positional_reading => { + .positional, .positional_simple => { setLogicalPos(r, offset); }, - .streaming, .streaming_reading => { + .streaming, .streaming_simple => { const logical_pos = logicalPos(r); if (offset >= logical_pos) return seekBy(r, @intCast(offset - logical_pos)); if (r.seek_err) |err| return err; @@ -225,19 +225,19 @@ pub fn streamMode(r: *Reader, w: *Io.Writer, limit: Io.Limit, mode: Mode) Io.Rea switch (mode) { .positional, .streaming => return w.sendFile(r, limit) catch |write_err| switch (write_err) { error.Unimplemented => { - r.mode = r.mode.toReading(); + r.mode = r.mode.toSimple(); return 0; }, else => |e| return e, }, - .positional_reading => { + .positional_simple => { const dest = limit.slice(try w.writableSliceGreedy(1)); var data: [1][]u8 = .{dest}; const n = try readVecPositional(r, &data); w.advance(n); return n; }, - .streaming_reading => { + .streaming_simple => { const dest = limit.slice(try w.writableSliceGreedy(1)); var data: [1][]u8 = .{dest}; const n = try readVecStreaming(r, &data); @@ -251,8 +251,8 @@ pub fn streamMode(r: *Reader, w: *Io.Writer, limit: Io.Limit, mode: Mode) Io.Rea fn readVec(io_reader: *Io.Reader, data: [][]u8) Io.Reader.Error!usize { const r: *Reader = @alignCast(@fieldParentPtr("interface", io_reader)); switch (r.mode) { - .positional, .positional_reading => return readVecPositional(r, data), - .streaming, .streaming_reading => return readVecStreaming(r, data), + .positional, .positional_simple => return readVecPositional(r, data), + .streaming, .streaming_simple => return readVecStreaming(r, data), .failure => return error.ReadFailed, } } @@ -320,7 +320,7 @@ fn discard(io_reader: *Io.Reader, limit: Io.Limit) Io.Reader.Error!usize { const io = r.io; const file = r.file; switch (r.mode) { - .positional, .positional_reading => { + .positional, .positional_simple => { const size = r.getSize() catch { r.mode = r.mode.toStreaming(); return 0; @@ -330,7 +330,7 @@ fn discard(io_reader: *Io.Reader, limit: Io.Limit) Io.Reader.Error!usize { setLogicalPos(r, logical_pos + delta); return delta; }, - .streaming, .streaming_reading => { + .streaming, .streaming_simple => { // Unfortunately we can't seek forward without knowing the // size because the seek syscalls provided to us will not // return the true end position if a seek would exceed the diff --git a/lib/std/Io/File/Writer.zig b/lib/std/Io/File/Writer.zig index 03e4ad8a15..1d0494c674 100644 --- a/lib/std/Io/File/Writer.zig +++ b/lib/std/Io/File/Writer.zig @@ -18,172 +18,7 @@ write_file_err: ?WriteFileError = null, seek_err: ?SeekError = null, interface: Io.Writer, -pub const Mode = union(enum) { - /// Uses `Io.VTable.fileWriteFileStreaming` if possible. Not a terminal. - /// `setColor` does nothing. - streaming, - /// Uses `Io.VTable.fileWriteFilePositional` if possible. Not a terminal. - /// `setColor` does nothing. - positional, - /// Avoids `Io.VTable.fileWriteFileStreaming`. Not a terminal. `setColor` - /// does nothing. - streaming_simple, - /// Avoids `Io.VTable.fileWriteFilePositional`. Not a terminal. `setColor` - /// does nothing. - positional_simple, - /// It's a terminal. Writes are escaped so as to strip escape sequences. - /// Color is enabled. - terminal_escaped, - /// It's a terminal. Colors are enabled via calling - /// SetConsoleTextAttribute. Writes are not escaped. - terminal_winapi: TerminalWinapi, - /// Indicates writing cannot continue because of a seek failure. - failure, - - pub fn toStreaming(m: @This()) @This() { - return switch (m) { - .positional, .streaming => .streaming, - .positional_simple, .streaming_simple => .streaming_simple, - inline else => |_, x| x, - }; - } - - pub fn toSimple(m: @This()) @This() { - return switch (m) { - .positional, .positional_simple => .positional_simple, - .streaming, .streaming_simple => .streaming_simple, - inline else => |_, x| x, - }; - } - - pub fn toUnescaped(m: @This()) @This() { - return switch (m) { - .terminal_escaped => .streaming_simple, - inline else => |_, x| x, - }; - } - - pub const TerminalWinapi = if (!is_windows) noreturn else struct { - handle: File.Handle, - reset_attributes: u16, - }; - - /// Detect suitable TTY configuration options for the given file (commonly - /// stdout/stderr). - /// - /// Will attempt to enable ANSI escape code support if necessary/possible. - pub fn detect(io: Io, file: File, want_color: bool, fallback: Mode) Io.Cancelable!Mode { - if (!want_color) return if (try file.isTty(io)) .terminal_escaped else fallback; - - if (file.enableAnsiEscapeCodes(io)) |_| { - return .terminal_escaped; - } else |err| switch (err) { - error.Canceled => return error.Canceled, - error.NotTerminalDevice, error.Unexpected => {}, - } - - if (is_windows and file.isTty(io)) { - const windows = std.os.windows; - var info: windows.CONSOLE_SCREEN_BUFFER_INFO = undefined; - if (windows.kernel32.GetConsoleScreenBufferInfo(file.handle, &info) != windows.FALSE) { - return .{ .terminal_winapi = .{ - .handle = file.handle, - .reset_attributes = info.wAttributes, - } }; - } - return .terminal_escaped; - } - - return fallback; - } - - pub const SetColorError = std.os.windows.SetConsoleTextAttributeError || Io.Writer.Error; - - pub fn setColor(mode: Mode, io_w: *Io.Writer, color: Color) SetColorError!void { - switch (mode) { - .streaming, .positional, .streaming_simple, .positional_simple, .failure => return, - .terminal_escaped => { - const color_string = switch (color) { - .black => "\x1b[30m", - .red => "\x1b[31m", - .green => "\x1b[32m", - .yellow => "\x1b[33m", - .blue => "\x1b[34m", - .magenta => "\x1b[35m", - .cyan => "\x1b[36m", - .white => "\x1b[37m", - .bright_black => "\x1b[90m", - .bright_red => "\x1b[91m", - .bright_green => "\x1b[92m", - .bright_yellow => "\x1b[93m", - .bright_blue => "\x1b[94m", - .bright_magenta => "\x1b[95m", - .bright_cyan => "\x1b[96m", - .bright_white => "\x1b[97m", - .bold => "\x1b[1m", - .dim => "\x1b[2m", - .reset => "\x1b[0m", - }; - try io_w.writeAll(color_string); - }, - .terminal_winapi => |ctx| { - const windows = std.os.windows; - const attributes: windows.WORD = switch (color) { - .black => 0, - .red => windows.FOREGROUND_RED, - .green => windows.FOREGROUND_GREEN, - .yellow => windows.FOREGROUND_RED | windows.FOREGROUND_GREEN, - .blue => windows.FOREGROUND_BLUE, - .magenta => windows.FOREGROUND_RED | windows.FOREGROUND_BLUE, - .cyan => windows.FOREGROUND_GREEN | windows.FOREGROUND_BLUE, - .white => windows.FOREGROUND_RED | windows.FOREGROUND_GREEN | windows.FOREGROUND_BLUE, - .bright_black => windows.FOREGROUND_INTENSITY, - .bright_red => windows.FOREGROUND_RED | windows.FOREGROUND_INTENSITY, - .bright_green => windows.FOREGROUND_GREEN | windows.FOREGROUND_INTENSITY, - .bright_yellow => windows.FOREGROUND_RED | windows.FOREGROUND_GREEN | windows.FOREGROUND_INTENSITY, - .bright_blue => windows.FOREGROUND_BLUE | windows.FOREGROUND_INTENSITY, - .bright_magenta => windows.FOREGROUND_RED | windows.FOREGROUND_BLUE | windows.FOREGROUND_INTENSITY, - .bright_cyan => windows.FOREGROUND_GREEN | windows.FOREGROUND_BLUE | windows.FOREGROUND_INTENSITY, - .bright_white, .bold => windows.FOREGROUND_RED | windows.FOREGROUND_GREEN | windows.FOREGROUND_BLUE | windows.FOREGROUND_INTENSITY, - // "dim" is not supported using basic character attributes, but let's still make it do *something*. - // This matches the old behavior of TTY.Color before the bright variants were added. - .dim => windows.FOREGROUND_INTENSITY, - .reset => ctx.reset_attributes, - }; - try io_w.flush(); - try windows.SetConsoleTextAttribute(ctx.handle, attributes); - }, - } - } - - fn DecorateArgs(comptime Args: type) type { - const fields = @typeInfo(Args).@"struct".fields; - var new_fields: [fields.len]type = undefined; - for (fields, &new_fields) |old, *new| { - if (old.type == std.debug.FormatStackTrace) { - new.* = std.debug.FormatStackTrace.Decorated; - } else { - new.* = old.type; - } - } - return @Tuple(&new_fields); - } - - pub fn decorateArgs(file_writer_mode: std.Io.File.Writer.Mode, args: anytype) DecorateArgs(@TypeOf(args)) { - var new_args: DecorateArgs(@TypeOf(args)) = undefined; - inline for (args, &new_args) |old, *new| { - if (@TypeOf(old) == std.debug.FormatStackTrace) { - new.* = .{ - .stack_trace = old.stack_trace, - .file_writer_mode = file_writer_mode, - }; - } else { - new.* = old; - } - } - return new_args; - } -}; +pub const Mode = File.Reader.Mode; pub const Error = error{ DiskQuota, @@ -277,8 +112,7 @@ pub fn drain(io_w: *Io.Writer, data: []const []const u8, splat: usize) Io.Writer const w: *Writer = @alignCast(@fieldParentPtr("interface", io_w)); switch (w.mode) { .positional, .positional_simple => return drainPositional(w, data, splat), - .streaming, .streaming_simple, .terminal_winapi => return drainStreaming(w, data, splat), - .terminal_escaped => return drainEscaping(w, data, splat), + .streaming, .streaming_simple => return drainStreaming(w, data, splat), .failure => return error.WriteFailed, } } @@ -319,38 +153,13 @@ fn drainStreaming(w: *Writer, data: []const []const u8, splat: usize) Io.Writer. return w.interface.consume(n); } -fn findTerminalEscape(buffer: []const u8) ?usize { - return std.mem.findScalar(u8, buffer, 0x1b); -} - -fn drainEscaping(w: *Writer, data: []const []const u8, splat: usize) Io.Writer.Error!usize { - const io = w.io; - const header = w.interface.buffered(); - if (findTerminalEscape(header)) |i| { - _ = i; - // TODO strip terminal escape sequences here - } - for (data) |d| { - if (findTerminalEscape(d)) |i| { - _ = i; - // TODO strip terminal escape sequences here - } - } - const n = io.vtable.fileWriteStreaming(io.userdata, w.file, header, data, splat) catch |err| { - w.err = err; - return error.WriteFailed; - }; - w.pos += n; - return w.interface.consume(n); -} - pub fn sendFile(io_w: *Io.Writer, file_reader: *Io.File.Reader, limit: Io.Limit) Io.Writer.FileError!usize { const w: *Writer = @alignCast(@fieldParentPtr("interface", io_w)); switch (w.mode) { .positional => return sendFilePositional(w, file_reader, limit), .positional_simple => return error.Unimplemented, .streaming => return sendFileStreaming(w, file_reader, limit), - .streaming_simple, .terminal_escaped, .terminal_winapi => return error.Unimplemented, + .streaming_simple => return error.Unimplemented, .failure => return error.WriteFailed, } } @@ -454,60 +263,7 @@ pub fn end(w: *Writer) EndError!void { .streaming, .streaming_simple, - .terminal_escaped, - .terminal_winapi, .failure, => {}, } } - -pub const Color = enum { - black, - red, - green, - yellow, - blue, - magenta, - cyan, - white, - bright_black, - bright_red, - bright_green, - bright_yellow, - bright_blue, - bright_magenta, - bright_cyan, - bright_white, - dim, - bold, - reset, -}; - -pub fn setColor(w: *Writer, color: Color) Io.Writer.Error!void { - return w.mode.setColor(&w.interface, color) catch |err| switch (err) { - error.WriteFailed => |e| return e, - else => |e| w.err = e, - }; -} - -pub fn disableEscape(w: *Writer) Mode { - const prev = w.mode; - w.mode = w.mode.toUnescaped(); - return prev; -} - -pub fn restoreEscape(w: *Writer, mode: Mode) void { - w.mode = mode; -} - -pub fn writeAllUnescaped(w: *Writer, bytes: []const u8) Io.Writer.Error!void { - const prev_mode = w.disableEscape(); - defer w.restoreEscape(prev_mode); - return w.interface.writeAll(bytes); -} - -pub fn printUnescaped(w: *Writer, comptime fmt: []const u8, args: anytype) Io.Writer.Error!void { - const prev_mode = w.disableEscape(); - defer w.restoreEscape(prev_mode); - return w.interface.print(fmt, args); -} diff --git a/lib/std/Io/Terminal.zig b/lib/std/Io/Terminal.zig new file mode 100644 index 0000000000..cd0ec56caa --- /dev/null +++ b/lib/std/Io/Terminal.zig @@ -0,0 +1,154 @@ +/// Abstraction for writing to a stream that might support terminal escape +/// codes. +const Terminal = @This(); + +const builtin = @import("builtin"); +const is_windows = builtin.os.tag == .windows; + +const std = @import("std"); +const Io = std.Io; +const File = std.Io.File; + +writer: *Io.Writer, +mode: Mode, + +pub const Color = enum { + black, + red, + green, + yellow, + blue, + magenta, + cyan, + white, + bright_black, + bright_red, + bright_green, + bright_yellow, + bright_blue, + bright_magenta, + bright_cyan, + bright_white, + dim, + bold, + reset, +}; + +pub const Mode = union(enum) { + no_color, + escape_codes, + windows_api: WindowsApi, + + pub const WindowsApi = if (!is_windows) noreturn else struct { + handle: File.Handle, + reset_attributes: u16, + }; + + /// Detect suitable TTY configuration options for the given file (commonly + /// stdout/stderr). + /// + /// Will attempt to enable ANSI escape code support if necessary/possible. + pub fn detect(io: Io, file: File) Io.Cancelable!Mode { + if (file.enableAnsiEscapeCodes(io)) |_| { + return .escape_codes; + } else |err| switch (err) { + error.Canceled => return error.Canceled, + error.NotTerminalDevice, error.Unexpected => {}, + } + + if (is_windows and file.isTty(io)) { + const windows = std.os.windows; + var info: windows.CONSOLE_SCREEN_BUFFER_INFO = undefined; + if (windows.kernel32.GetConsoleScreenBufferInfo(file.handle, &info) != 0) { + return .{ .terminal_winapi = .{ + .handle = file.handle, + .reset_attributes = info.wAttributes, + } }; + } + return .escape_codes; + } + + return .no_color; + } +}; + +pub const SetColorError = std.os.windows.SetConsoleTextAttributeError || Io.Writer.Error; + +pub fn setColor(t: Terminal, color: Color) Io.Writer.Error!void { + switch (t.mode) { + .no_color => return, + .escape_codes => { + const color_string = switch (color) { + .black => "\x1b[30m", + .red => "\x1b[31m", + .green => "\x1b[32m", + .yellow => "\x1b[33m", + .blue => "\x1b[34m", + .magenta => "\x1b[35m", + .cyan => "\x1b[36m", + .white => "\x1b[37m", + .bright_black => "\x1b[90m", + .bright_red => "\x1b[91m", + .bright_green => "\x1b[92m", + .bright_yellow => "\x1b[93m", + .bright_blue => "\x1b[94m", + .bright_magenta => "\x1b[95m", + .bright_cyan => "\x1b[96m", + .bright_white => "\x1b[97m", + .bold => "\x1b[1m", + .dim => "\x1b[2m", + .reset => "\x1b[0m", + }; + try t.writer.writeAll(color_string); + }, + .windows_api => |wa| { + const windows = std.os.windows; + const attributes: windows.WORD = switch (color) { + .black => 0, + .red => windows.FOREGROUND_RED, + .green => windows.FOREGROUND_GREEN, + .yellow => windows.FOREGROUND_RED | windows.FOREGROUND_GREEN, + .blue => windows.FOREGROUND_BLUE, + .magenta => windows.FOREGROUND_RED | windows.FOREGROUND_BLUE, + .cyan => windows.FOREGROUND_GREEN | windows.FOREGROUND_BLUE, + .white => windows.FOREGROUND_RED | windows.FOREGROUND_GREEN | windows.FOREGROUND_BLUE, + .bright_black => windows.FOREGROUND_INTENSITY, + .bright_red => windows.FOREGROUND_RED | windows.FOREGROUND_INTENSITY, + .bright_green => windows.FOREGROUND_GREEN | windows.FOREGROUND_INTENSITY, + .bright_yellow => windows.FOREGROUND_RED | windows.FOREGROUND_GREEN | windows.FOREGROUND_INTENSITY, + .bright_blue => windows.FOREGROUND_BLUE | windows.FOREGROUND_INTENSITY, + .bright_magenta => windows.FOREGROUND_RED | windows.FOREGROUND_BLUE | windows.FOREGROUND_INTENSITY, + .bright_cyan => windows.FOREGROUND_GREEN | windows.FOREGROUND_BLUE | windows.FOREGROUND_INTENSITY, + .bright_white, .bold => windows.FOREGROUND_RED | windows.FOREGROUND_GREEN | windows.FOREGROUND_BLUE | windows.FOREGROUND_INTENSITY, + // "dim" is not supported using basic character attributes, but let's still make it do *something*. + // This matches the old behavior of TTY.Color before the bright variants were added. + .dim => windows.FOREGROUND_INTENSITY, + .reset => wa.reset_attributes, + }; + try t.writer.flush(); + try windows.SetConsoleTextAttribute(wa.handle, attributes); + }, + } +} + +pub fn disableEscape(t: *Terminal) Mode { + const prev = t.mode; + t.mode = t.mode.toUnescaped(); + return prev; +} + +pub fn restoreEscape(t: *Terminal, mode: Mode) void { + t.mode = mode; +} + +pub fn writeAllUnescaped(t: *Terminal, bytes: []const u8) Io.Writer.Error!void { + const prev_mode = t.disableEscape(); + defer t.restoreEscape(prev_mode); + return t.interface.writeAll(bytes); +} + +pub fn printUnescaped(t: *Terminal, comptime fmt: []const u8, args: anytype) Io.Writer.Error!void { + const prev_mode = t.disableEscape(); + defer t.restoreEscape(prev_mode); + return t.interface.print(fmt, args); +} diff --git a/lib/std/Io/Threaded.zig b/lib/std/Io/Threaded.zig index f375bbcb7c..3e7b6950d4 100644 --- a/lib/std/Io/Threaded.zig +++ b/lib/std/Io/Threaded.zig @@ -82,8 +82,8 @@ stderr_writer: File.Writer = .{ .io = undefined, .interface = Io.File.Writer.initInterface(&.{}), .file = if (is_windows) undefined else .stderr(), - .mode = undefined, }, +stderr_mode: Io.Terminal.Mode = .no_color, stderr_writer_initialized: bool = false, pub const RobustCancel = if (std.Thread.use_pthreads or native_os == .linux) enum { @@ -755,9 +755,9 @@ pub fn io(t: *Threaded) Io { .processExecutableOpen = processExecutableOpen, .processExecutablePath = processExecutablePath, - .lockStderrWriter = lockStderrWriter, - .tryLockStderrWriter = tryLockStderrWriter, - .unlockStderrWriter = unlockStderrWriter, + .lockStderr = lockStderr, + .tryLockStderr = tryLockStderr, + .unlockStderr = unlockStderr, .now = now, .sleep = sleep, @@ -887,9 +887,9 @@ pub fn ioBasic(t: *Threaded) Io { .processExecutableOpen = processExecutableOpen, .processExecutablePath = processExecutablePath, - .lockStderrWriter = lockStderrWriter, - .tryLockStderrWriter = tryLockStderrWriter, - .unlockStderrWriter = unlockStderrWriter, + .lockStderr = lockStderr, + .tryLockStderr = tryLockStderr, + .unlockStderr = unlockStderr, .now = now, .sleep = sleep, @@ -10090,47 +10090,70 @@ fn netLookupFallible( return error.OptionUnsupported; } -fn lockStderrWriter(userdata: ?*anyopaque, buffer: []u8) Io.Cancelable!*File.Writer { +fn lockStderr( + userdata: ?*anyopaque, + buffer: []u8, + terminal_mode: ?Io.Terminal.Mode, +) Io.Cancelable!Io.LockedStderr { const t: *Threaded = @ptrCast(@alignCast(userdata)); // Only global mutex since this is Threaded. - Io.stderr_thread_mutex.lock(); - if (!t.stderr_writer_initialized) { - const io_t = ioBasic(t); - if (is_windows) t.stderr_writer.file = .stderr(); - t.stderr_writer.io = io_t; - t.stderr_writer.mode = try .detect(io_t, t.stderr_writer.file, true, .streaming_simple); - t.stderr_writer_initialized = true; - } - std.Progress.clearWrittenWithEscapeCodes(&t.stderr_writer) catch {}; - t.stderr_writer.interface.flush() catch {}; - t.stderr_writer.interface.buffer = buffer; - return &t.stderr_writer; + std.process.stderr_thread_mutex.lock(); + return initLockedStderr(t, buffer, terminal_mode); } -fn tryLockStderrWriter(userdata: ?*anyopaque, buffer: []u8) ?*File.Writer { +fn tryLockStderr( + userdata: ?*anyopaque, + buffer: []u8, + terminal_mode: ?Io.Terminal.Mode, +) Io.Cancelable!?Io.LockedStderr { const t: *Threaded = @ptrCast(@alignCast(userdata)); // Only global mutex since this is Threaded. - if (!Io.stderr_thread_mutex.tryLock()) return null; + if (!std.process.stderr_thread_mutex.tryLock()) return null; + return try initLockedStderr(t, buffer, terminal_mode); +} + +fn initLockedStderr( + t: *Threaded, + buffer: []u8, + terminal_mode: ?Io.Terminal.Mode, +) Io.Cancelable!Io.LockedStderr { if (!t.stderr_writer_initialized) { const io_t = ioBasic(t); if (is_windows) t.stderr_writer.file = .stderr(); t.stderr_writer.io = io_t; - t.stderr_writer.mode = File.Writer.Mode.detect(io_t, t.stderr_writer.file, true, .streaming_simple) catch - return null; t.stderr_writer_initialized = true; + t.stderr_mode = terminal_mode orelse try .detect(io_t, t.stderr_writer.file); } - std.Progress.clearWrittenWithEscapeCodes(&t.stderr_writer) catch {}; - t.stderr_writer.interface.flush() catch {}; + std.Progress.clearWrittenWithEscapeCodes(&t.stderr_writer) catch |err| switch (err) { + error.WriteFailed => switch (t.stderr_writer.err.?) { + error.Canceled => |e| return e, + else => {}, + }, + }; + t.stderr_writer.interface.flush() catch |err| switch (err) { + error.WriteFailed => switch (t.stderr_writer.err.?) { + error.Canceled => |e| return e, + else => {}, + }, + }; t.stderr_writer.interface.buffer = buffer; - return &t.stderr_writer; + return .{ + .file_writer = &t.stderr_writer, + .terminal_mode = t.stderr_mode, + }; } -fn unlockStderrWriter(userdata: ?*anyopaque) void { +fn unlockStderr(userdata: ?*anyopaque) void { const t: *Threaded = @ptrCast(@alignCast(userdata)); - t.stderr_writer.interface.flush() catch {}; + t.stderr_writer.interface.flush() catch |err| switch (err) { + error.WriteFailed => switch (t.stderr_writer.err.?) { + error.Canceled => @panic("TODO make this uncancelable"), + else => {}, + }, + }; t.stderr_writer.interface.end = 0; t.stderr_writer.interface.buffer = &.{}; - Io.stderr_thread_mutex.unlock(); + std.process.stderr_thread_mutex.unlock(); } pub const PosixAddress = extern union { diff --git a/lib/std/Io/Writer.zig b/lib/std/Io/Writer.zig index 63ae6d93a0..5349eef104 100644 --- a/lib/std/Io/Writer.zig +++ b/lib/std/Io/Writer.zig @@ -961,7 +961,7 @@ pub fn sendFileAll(w: *Writer, file_reader: *File.Reader, limit: Limit) FileAllE const n = sendFile(w, file_reader, .limited(remaining)) catch |err| switch (err) { error.EndOfStream => break, error.Unimplemented => { - file_reader.mode = file_reader.mode.toReading(); + file_reader.mode = file_reader.mode.toSimple(); remaining -= try w.sendFileReadingAll(file_reader, .limited(remaining)); break; }, diff --git a/lib/std/Progress.zig b/lib/std/Progress.zig index 6edbf9e471..54b52d48af 100644 --- a/lib/std/Progress.zig +++ b/lib/std/Progress.zig @@ -565,10 +565,10 @@ fn updateThreadRun(io: Io) void { maybeUpdateSize(resize_flag); const buffer, _ = computeRedraw(&serialized_buffer); - if (io.tryLockStderrWriter(&.{})) |fw| { - defer io.unlockStderrWriter(); + if (io.tryLockStderr(&.{}, null) catch return) |locked_stderr| { + defer io.unlockStderr(); global_progress.need_clear = true; - fw.writeAllUnescaped(buffer) catch return; + locked_stderr.file_writer.interface.writeAll(buffer) catch return; } } @@ -576,18 +576,18 @@ fn updateThreadRun(io: Io) void { const resize_flag = wait(io, global_progress.refresh_rate_ns); if (@atomicLoad(bool, &global_progress.done, .monotonic)) { - const fw = io.lockStderrWriter(&.{}) catch return; - defer io.unlockStderrWriter(); - return clearWrittenWithEscapeCodes(fw) catch {}; + const stderr = io.lockStderr(&.{}, null) catch return; + defer io.unlockStderr(); + return clearWrittenWithEscapeCodes(stderr.file_writer) catch {}; } maybeUpdateSize(resize_flag); const buffer, _ = computeRedraw(&serialized_buffer); - if (io.tryLockStderrWriter(&.{})) |fw| { - defer io.unlockStderrWriter(); + if (io.tryLockStderr(&.{}, null) catch return) |locked_stderr| { + defer io.unlockStderr(); global_progress.need_clear = true; - fw.writeAllUnescaped(buffer) catch return; + locked_stderr.file_writer.interface.writeAll(buffer) catch return; } } } @@ -609,11 +609,11 @@ fn windowsApiUpdateThreadRun(io: Io) void { maybeUpdateSize(resize_flag); const buffer, const nl_n = computeRedraw(&serialized_buffer); - if (io.tryLockStderrWriter()) |fw| { - defer io.unlockStderrWriter(); + if (io.tryLockStderr(&.{}, null) catch return) |locked_stderr| { + defer io.unlockStderr(); windowsApiWriteMarker(); global_progress.need_clear = true; - fw.writeAllUnescaped(buffer) catch return; + locked_stderr.file_writer.interface.writeAll(buffer) catch return; windowsApiMoveToMarker(nl_n) catch return; } } @@ -622,20 +622,20 @@ fn windowsApiUpdateThreadRun(io: Io) void { const resize_flag = wait(io, global_progress.refresh_rate_ns); if (@atomicLoad(bool, &global_progress.done, .monotonic)) { - _ = io.lockStderrWriter() catch return; - defer io.unlockStderrWriter(); + _ = io.lockStderr(&.{}, null) catch return; + defer io.unlockStderr(); return clearWrittenWindowsApi() catch {}; } maybeUpdateSize(resize_flag); const buffer, const nl_n = computeRedraw(&serialized_buffer); - if (io.tryLockStderrWriter()) |fw| { - defer io.unlockStderrWriter(); + if (io.tryLockStderr(&.{}, null) catch return) |locked_stderr| { + defer io.unlockStderr(); clearWrittenWindowsApi() catch return; windowsApiWriteMarker(); global_progress.need_clear = true; - fw.writeAllUnescaped(buffer) catch return; + locked_stderr.file_writer.interface.writeAll(buffer) catch return; windowsApiMoveToMarker(nl_n) catch return; } } @@ -766,7 +766,7 @@ fn appendTreeSymbol(symbol: TreeSymbol, buf: []u8, start_i: usize) usize { pub fn clearWrittenWithEscapeCodes(file_writer: *Io.File.Writer) Io.Writer.Error!void { if (noop_impl or !global_progress.need_clear) return; - try file_writer.writeAllUnescaped(clear ++ progress_remove); + try file_writer.interface.writeAll(clear ++ progress_remove); global_progress.need_clear = false; } diff --git a/lib/std/debug.zig b/lib/std/debug.zig index 67f7d3a9fe..fd3a456ff7 100644 --- a/lib/std/debug.zig +++ b/lib/std/debug.zig @@ -265,29 +265,34 @@ pub const sys_can_stack_trace = switch (builtin.cpu.arch) { /// separate from the application's `Io` instance. var static_single_threaded_io: Io.Threaded = .init_single_threaded; -/// Allows the caller to freely write to stderr until `unlockStderrWriter` is called. +/// Allows the caller to freely write to stderr until `unlockStderr` is called. /// /// During the lock, any `std.Progress` information is cleared from the terminal. /// -/// The lock is recursive, so it is valid for the same thread to call `lockStderrWriter` multiple -/// times. The primary motivation is that this allows the panic handler to safely dump the stack -/// trace and panic message even if the mutex was held at the panic site. +/// The lock is recursive, so it is valid for the same thread to call +/// `lockStderr` multiple times, allowing the panic handler to safely +/// dump the stack trace and panic message even if the mutex was held at the +/// panic site. /// /// The returned `Writer` does not need to be manually flushed: flushing is -/// performed automatically when the matching `unlockStderrWriter` call occurs. +/// performed automatically when the matching `unlockStderr` call occurs. /// /// This is a low-level debugging primitive that bypasses the `Io` interface, /// writing directly to stderr using the most basic syscalls available. This /// function does not switch threads, switch stacks, or suspend. /// -/// Alternatively, use the higher-level `Io.lockStderrWriter` to integrate with -/// the application's chosen `Io` implementation. -pub fn lockStderrWriter(buffer: []u8) *File.Writer { - return static_single_threaded_io.ioBasic().lockStderrWriter(buffer) catch unreachable; +/// Alternatively, use the higher-level `Io.lockStderr` to integrate with the +/// application's chosen `Io` implementation. +pub fn lockStderr(buffer: []u8) Io.Terminal { + return (static_single_threaded_io.ioBasic().lockStderr(buffer, null) catch |err| switch (err) { + // Impossible to cancel because no calls to cancel using + // `static_single_threaded_io` exist. + error.Canceled => unreachable, + }).terminal(); } -pub fn unlockStderrWriter() void { - static_single_threaded_io.ioBasic().unlockStderrWriter(); +pub fn unlockStderr() void { + static_single_threaded_io.ioBasic().unlockStderr(); } /// Writes to stderr, ignoring errors. @@ -299,14 +304,14 @@ pub fn unlockStderrWriter() void { /// Uses a 64-byte buffer for formatted printing which is flushed before this /// function returns. /// -/// Alternatively, use the higher-level `std.log` or `Io.lockStderrWriter` to +/// Alternatively, use the higher-level `std.log` or `Io.lockStderr` to /// integrate with the application's chosen `Io` implementation. pub fn print(comptime fmt: []const u8, args: anytype) void { nosuspend { var buffer: [64]u8 = undefined; - const stderr = lockStderrWriter(&buffer); - defer unlockStderrWriter(); - stderr.interface.print(fmt, stderr.mode.decorateArgs(args)) catch return; + const stderr = lockStderr(&buffer); + defer unlockStderr(); + stderr.writer.print(fmt, args) catch return; } } @@ -322,43 +327,44 @@ pub inline fn getSelfDebugInfo() !*SelfInfo { /// Tries to print a hexadecimal view of the bytes, unbuffered, and ignores any error returned. /// Obtains the stderr mutex while dumping. pub fn dumpHex(bytes: []const u8) void { - const bw, const ttyconf = lockStderrWriter(&.{}); - defer unlockStderrWriter(); - dumpHexFallible(bw, ttyconf, bytes) catch {}; + const stderr = lockStderr(&.{}); + defer unlockStderr(); + dumpHexFallible(stderr, bytes) catch {}; } /// Prints a hexadecimal view of the bytes, returning any error that occurs. -pub fn dumpHexFallible(bw: *Writer, fwm: File.Writer.Mode, bytes: []const u8) !void { +pub fn dumpHexFallible(t: Io.Terminal, bytes: []const u8) !void { + const w = t.writer; var chunks = mem.window(u8, bytes, 16, 16); while (chunks.next()) |window| { // 1. Print the address. const address = (@intFromPtr(bytes.ptr) + 0x10 * (std.math.divCeil(usize, chunks.index orelse bytes.len, 16) catch unreachable)) - 0x10; - try fwm.setColor(bw, .dim); + try t.setColor(.dim); // We print the address in lowercase and the bytes in uppercase hexadecimal to distinguish them more. // Also, make sure all lines are aligned by padding the address. - try bw.print("{x:0>[1]} ", .{ address, @sizeOf(usize) * 2 }); - try fwm.setColor(bw, .reset); + try w.print("{x:0>[1]} ", .{ address, @sizeOf(usize) * 2 }); + try t.setColor(.reset); // 2. Print the bytes. for (window, 0..) |byte, index| { - try bw.print("{X:0>2} ", .{byte}); - if (index == 7) try bw.writeByte(' '); + try w.print("{X:0>2} ", .{byte}); + if (index == 7) try w.writeByte(' '); } - try bw.writeByte(' '); + try w.writeByte(' '); if (window.len < 16) { var missing_columns = (16 - window.len) * 3; if (window.len < 8) missing_columns += 1; - try bw.splatByteAll(' ', missing_columns); + try w.splatByteAll(' ', missing_columns); } // 3. Print the characters. for (window) |byte| { if (std.ascii.isPrint(byte)) { - try bw.writeByte(byte); + try w.writeByte(byte); } else { // Related: https://github.com/ziglang/zig/issues/7600 - if (fwm == .terminal_winapi) { - try bw.writeByte('.'); + if (t.mode == .windows_api) { + try w.writeByte('.'); continue; } @@ -366,14 +372,14 @@ pub fn dumpHexFallible(bw: *Writer, fwm: File.Writer.Mode, bytes: []const u8) !v // We don't want to do this for all control codes because most control codes apart from // the ones that Zig has escape sequences for are likely not very useful to print as symbols. switch (byte) { - '\n' => try bw.writeAll("␊"), - '\r' => try bw.writeAll("␍"), - '\t' => try bw.writeAll("␉"), - else => try bw.writeByte('.'), + '\n' => try w.writeAll("␊"), + '\r' => try w.writeAll("␍"), + '\t' => try w.writeAll("␉"), + else => try w.writeByte('.'), } } } - try bw.writeByte('\n'); + try w.writeByte('\n'); } } @@ -545,26 +551,27 @@ pub fn defaultPanic( _ = panicking.fetchAdd(1, .seq_cst); trace: { - const stderr = lockStderrWriter(&.{}); - defer unlockStderrWriter(); + const stderr = lockStderr(&.{}); + defer unlockStderr(); + const writer = stderr.writer; if (builtin.single_threaded) { - stderr.interface.print("panic: ", .{}) catch break :trace; + writer.print("panic: ", .{}) catch break :trace; } else { const current_thread_id = std.Thread.getCurrentId(); - stderr.interface.print("thread {d} panic: ", .{current_thread_id}) catch break :trace; + writer.print("thread {d} panic: ", .{current_thread_id}) catch break :trace; } - stderr.interface.print("{s}\n", .{msg}) catch break :trace; + writer.print("{s}\n", .{msg}) catch break :trace; if (@errorReturnTrace()) |t| if (t.index > 0) { - stderr.interface.writeAll("error return context:\n") catch break :trace; - writeStackTrace(t, &stderr.interface, stderr.mode) catch break :trace; - stderr.interface.writeAll("\nstack trace:\n") catch break :trace; + writer.writeAll("error return context:\n") catch break :trace; + writeStackTrace(t, stderr) catch break :trace; + writer.writeAll("\nstack trace:\n") catch break :trace; }; writeCurrentStackTrace(.{ .first_address = first_trace_addr orelse @returnAddress(), .allow_unsafe_unwind = true, // we're crashing anyway, give it our all! - }, &stderr.interface, stderr.mode) catch break :trace; + }, stderr) catch break :trace; } waitForOtherThreadToFinishPanicking(); @@ -574,8 +581,8 @@ pub fn defaultPanic( // A panic happened while trying to print a previous panic message. // We're still holding the mutex but that's fine as we're going to // call abort(). - const stderr = lockStderrWriter(&.{}); - stderr.interface.writeAll("aborting due to recursive panic\n") catch {}; + const stderr = lockStderr(&.{}); + stderr.writer.writeAll("aborting due to recursive panic\n") catch {}; }, else => {}, // Panicked while printing the recursive panic message. } @@ -656,28 +663,29 @@ pub noinline fn captureCurrentStackTrace(options: StackUnwindOptions, addr_buf: /// Write the current stack trace to `writer`, annotated with source locations. /// /// See `captureCurrentStackTrace` to capture the trace addresses into a buffer instead of printing. -pub noinline fn writeCurrentStackTrace(options: StackUnwindOptions, writer: *Writer, fwm: File.Writer.Mode) Writer.Error!void { +pub noinline fn writeCurrentStackTrace(options: StackUnwindOptions, t: Io.Terminal) Writer.Error!void { + const writer = t.writer; if (!std.options.allow_stack_tracing) { - fwm.setColor(writer, .dim) catch {}; + t.setColor(.dim) catch {}; try writer.print("Cannot print stack trace: stack tracing is disabled\n", .{}); - fwm.setColor(writer, .reset) catch {}; + t.setColor(.reset) catch {}; return; } const di_gpa = getDebugInfoAllocator(); const di = getSelfDebugInfo() catch |err| switch (err) { error.UnsupportedTarget => { - fwm.setColor(writer, .dim) catch {}; + t.setColor(.dim) catch {}; try writer.print("Cannot print stack trace: debug info unavailable for target\n", .{}); - fwm.setColor(writer, .reset) catch {}; + t.setColor(.reset) catch {}; return; }, }; var it: StackIterator = .init(options.context); defer it.deinit(); if (!it.stratOk(options.allow_unsafe_unwind)) { - fwm.setColor(writer, .dim) catch {}; + t.setColor(.dim) catch {}; try writer.print("Cannot print stack trace: safe unwind unavailable for target\n", .{}); - fwm.setColor(writer, .reset) catch {}; + t.setColor(.reset) catch {}; return; } var total_frames: usize = 0; @@ -701,31 +709,31 @@ pub noinline fn writeCurrentStackTrace(options: StackUnwindOptions, writer: *Wri error.Unexpected => "unexpected error", }; if (it.stratOk(options.allow_unsafe_unwind)) { - fwm.setColor(writer, .dim) catch {}; + t.setColor(.dim) catch {}; try writer.print( "Unwind error at address `{s}:0x{x}` ({s}), remaining frames may be incorrect\n", .{ module_name, unwind_error.address, caption }, ); - fwm.setColor(writer, .reset) catch {}; + t.setColor(.reset) catch {}; } else { - fwm.setColor(writer, .dim) catch {}; + t.setColor(.dim) catch {}; try writer.print( "Unwind error at address `{s}:0x{x}` ({s}), stopping trace early\n", .{ module_name, unwind_error.address, caption }, ); - fwm.setColor(writer, .reset) catch {}; + t.setColor(.reset) catch {}; return; } }, .end => break, .frame => |ret_addr| { if (total_frames > 10_000) { - fwm.setColor(writer, .dim) catch {}; + t.setColor(.dim) catch {}; try writer.print( "Stopping trace after {d} frames (large frame count may indicate broken debug info)\n", .{total_frames}, ); - fwm.setColor(writer, .reset) catch {}; + t.setColor(.reset) catch {}; return; } total_frames += 1; @@ -735,7 +743,7 @@ pub noinline fn writeCurrentStackTrace(options: StackUnwindOptions, writer: *Wri } // `ret_addr` is the return address, which is *after* the function call. // Subtract 1 to get an address *in* the function call for a better source location. - try printSourceAtAddress(di_gpa, io, di, writer, ret_addr -| StackIterator.ra_call_offset, fwm); + try printSourceAtAddress(di_gpa, io, di, t, ret_addr -| StackIterator.ra_call_offset); printed_any_frame = true; }, }; @@ -743,8 +751,8 @@ pub noinline fn writeCurrentStackTrace(options: StackUnwindOptions, writer: *Wri } /// A thin wrapper around `writeCurrentStackTrace` which writes to stderr and ignores write errors. pub fn dumpCurrentStackTrace(options: StackUnwindOptions) void { - const stderr = lockStderrWriter(&.{}); - defer unlockStderrWriter(); + const stderr = lockStderr(&.{}); + defer unlockStderr(); writeCurrentStackTrace(.{ .first_address = a: { if (options.first_address) |a| break :a a; @@ -753,38 +761,28 @@ pub fn dumpCurrentStackTrace(options: StackUnwindOptions) void { }, .context = options.context, .allow_unsafe_unwind = options.allow_unsafe_unwind, - }, &stderr.interface, stderr.mode) catch |err| switch (err) { + }, stderr) catch |err| switch (err) { error.WriteFailed => {}, }; } pub const FormatStackTrace = struct { stack_trace: StackTrace, + terminal_mode: Io.Terminal.Mode = .no_color, - pub const Decorated = struct { - stack_trace: StackTrace, - file_writer_mode: File.Writer.Mode, - - pub fn format(decorated: Decorated, writer: *Writer) Writer.Error!void { - try writer.writeByte('\n'); - try writeStackTrace(&decorated.stack_trace, writer, decorated.file_writer_mode); - } - }; - - pub fn format(context: FormatStackTrace, writer: *Writer) Writer.Error!void { - return Decorated.format(.{ - .stack_trace = context.stack_trace, - .file_writer_mode = .streaming, - }, writer); + pub fn format(fst: FormatStackTrace, writer: *Writer) Writer.Error!void { + try writer.writeByte('\n'); + try writeStackTrace(&fst.stack_trace, .{ .writer = writer, .mode = fst.terminal_mode }); } }; /// Write a previously captured stack trace to `writer`, annotated with source locations. -pub fn writeStackTrace(st: *const StackTrace, writer: *Writer, fwm: File.Writer.Mode) Writer.Error!void { +pub fn writeStackTrace(st: *const StackTrace, t: Io.Terminal) Writer.Error!void { + const writer = t.writer; if (!std.options.allow_stack_tracing) { - fwm.setColor(writer, .dim) catch {}; + t.setColor(.dim) catch {}; try writer.print("Cannot print stack trace: stack tracing is disabled\n", .{}); - fwm.setColor(writer, .reset) catch {}; + t.setColor(.reset) catch {}; return; } @@ -795,9 +793,9 @@ pub fn writeStackTrace(st: *const StackTrace, writer: *Writer, fwm: File.Writer. const di_gpa = getDebugInfoAllocator(); const di = getSelfDebugInfo() catch |err| switch (err) { error.UnsupportedTarget => { - fwm.setColor(writer, .dim) catch {}; + t.setColor(.dim) catch {}; try writer.print("Cannot print stack trace: debug info unavailable for target\n\n", .{}); - fwm.setColor(writer, .reset) catch {}; + t.setColor(.reset) catch {}; return; }, }; @@ -806,19 +804,19 @@ pub fn writeStackTrace(st: *const StackTrace, writer: *Writer, fwm: File.Writer. for (st.instruction_addresses[0..captured_frames]) |ret_addr| { // `ret_addr` is the return address, which is *after* the function call. // Subtract 1 to get an address *in* the function call for a better source location. - try printSourceAtAddress(di_gpa, io, di, writer, ret_addr -| StackIterator.ra_call_offset, fwm); + try printSourceAtAddress(di_gpa, io, di, t, ret_addr -| StackIterator.ra_call_offset); } if (n_frames > captured_frames) { - fwm.setColor(writer, .bold) catch {}; + t.setColor(.bold) catch {}; try writer.print("({d} additional stack frames skipped...)\n", .{n_frames - captured_frames}); - fwm.setColor(writer, .reset) catch {}; + t.setColor(.reset) catch {}; } } /// A thin wrapper around `writeStackTrace` which writes to stderr and ignores write errors. pub fn dumpStackTrace(st: *const StackTrace) void { - const stderr = lockStderrWriter(&.{}); - defer unlockStderrWriter(); - writeStackTrace(st, &stderr.interface, stderr.mode) catch |err| switch (err) { + const stderr = lockStderr(&.{}); + defer unlockStderr(); + writeStackTrace(st, stderr) catch |err| switch (err) { error.WriteFailed => {}, }; } @@ -1117,9 +1115,8 @@ fn printSourceAtAddress( gpa: Allocator, io: Io, debug_info: *SelfInfo, - writer: *Writer, + t: Io.Terminal, address: usize, - fwm: File.Writer.Mode, ) Writer.Error!void { const symbol: Symbol = debug_info.getSymbol(gpa, io, address) catch |err| switch (err) { error.MissingDebugInfo, @@ -1127,40 +1124,39 @@ fn printSourceAtAddress( error.InvalidDebugInfo, => .unknown, error.ReadFailed, error.Unexpected, error.Canceled => s: { - fwm.setColor(writer, .dim) catch {}; - try writer.print("Failed to read debug info from filesystem, trace may be incomplete\n\n", .{}); - fwm.setColor(writer, .reset) catch {}; + t.setColor(.dim) catch {}; + try t.writer.print("Failed to read debug info from filesystem, trace may be incomplete\n\n", .{}); + t.setColor(.reset) catch {}; break :s .unknown; }, error.OutOfMemory => s: { - fwm.setColor(writer, .dim) catch {}; - try writer.print("Ran out of memory loading debug info, trace may be incomplete\n\n", .{}); - fwm.setColor(writer, .reset) catch {}; + t.setColor(.dim) catch {}; + try t.writer.print("Ran out of memory loading debug info, trace may be incomplete\n\n", .{}); + t.setColor(.reset) catch {}; break :s .unknown; }, }; defer if (symbol.source_location) |sl| gpa.free(sl.file_name); return printLineInfo( io, - writer, + t, symbol.source_location, address, symbol.name orelse "???", symbol.compile_unit_name orelse debug_info.getModuleName(gpa, address) catch "???", - fwm, ); } fn printLineInfo( io: Io, - writer: *Writer, + t: Io.Terminal, source_location: ?SourceLocation, address: usize, symbol_name: []const u8, compile_unit_name: []const u8, - fwm: File.Writer.Mode, ) Writer.Error!void { nosuspend { - fwm.setColor(writer, .bold) catch {}; + const writer = t.writer; + t.setColor(.bold) catch {}; if (source_location) |*sl| { try writer.print("{s}:{d}:{d}", .{ sl.file_name, sl.line, sl.column }); @@ -1168,11 +1164,11 @@ fn printLineInfo( try writer.writeAll("???:?:?"); } - fwm.setColor(writer, .reset) catch {}; + t.setColor(.reset) catch {}; try writer.writeAll(": "); - fwm.setColor(writer, .dim) catch {}; + t.setColor(.dim) catch {}; try writer.print("0x{x} in {s} ({s})", .{ address, symbol_name, compile_unit_name }); - fwm.setColor(writer, .reset) catch {}; + t.setColor(.reset) catch {}; try writer.writeAll("\n"); // Show the matching source code line if possible @@ -1183,9 +1179,9 @@ fn printLineInfo( const space_needed = @as(usize, @intCast(sl.column - 1)); try writer.splatByteAll(' ', space_needed); - fwm.setColor(writer, .green) catch {}; + t.setColor(.green) catch {}; try writer.writeAll("^"); - fwm.setColor(writer, .reset) catch {}; + t.setColor(.reset) catch {}; } try writer.writeAll("\n"); } else |_| { @@ -1554,19 +1550,19 @@ pub fn defaultHandleSegfault(addr: ?usize, name: []const u8, opt_ctx: ?CpuContex _ = panicking.fetchAdd(1, .seq_cst); trace: { - const stderr = lockStderrWriter(&.{}); - defer unlockStderrWriter(); + const stderr = lockStderr(&.{}); + defer unlockStderr(); if (addr) |a| { - stderr.interface.print("{s} at address 0x{x}\n", .{ name, a }) catch break :trace; + stderr.writer.print("{s} at address 0x{x}\n", .{ name, a }) catch break :trace; } else { - stderr.interface.print("{s} (no address available)\n", .{name}) catch break :trace; + stderr.writer.print("{s} (no address available)\n", .{name}) catch break :trace; } if (opt_ctx) |context| { writeCurrentStackTrace(.{ .context = context, .allow_unsafe_unwind = true, // we're crashing anyway, give it our all! - }, &stderr.interface, stderr.mode) catch break :trace; + }, stderr) catch break :trace; } } }, @@ -1575,8 +1571,8 @@ pub fn defaultHandleSegfault(addr: ?usize, name: []const u8, opt_ctx: ?CpuContex // A segfault happened while trying to print a previous panic message. // We're still holding the mutex but that's fine as we're going to // call abort(). - const stderr = lockStderrWriter(&.{}); - stderr.interface.writeAll("aborting due to recursive panic\n") catch {}; + const stderr = lockStderr(&.{}); + stderr.writer.writeAll("aborting due to recursive panic\n") catch {}; }, else => {}, // Panicked while printing the recursive panic message. } @@ -1682,21 +1678,21 @@ pub fn ConfigurableTrace(comptime size: usize, comptime stack_frame_count: usize pub fn dump(t: @This()) void { if (!enabled) return; - const stderr = lockStderrWriter(&.{}); - defer unlockStderrWriter(); + const stderr = lockStderr(&.{}); + defer unlockStderr(); const end = @min(t.index, size); for (t.addrs[0..end], 0..) |frames_array, i| { - stderr.interface.print("{s}:\n", .{t.notes[i]}) catch return; + stderr.writer.print("{s}:\n", .{t.notes[i]}) catch return; var frames_array_mutable = frames_array; const frames = mem.sliceTo(frames_array_mutable[0..], 0); const stack_trace: StackTrace = .{ .index = frames.len, .instruction_addresses = frames, }; - writeStackTrace(&stack_trace, &stderr.interface, stderr.mode) catch return; + writeStackTrace(&stack_trace, stderr) catch return; } if (t.index > end) { - stderr.interface.print("{d} more traces not shown; consider increasing trace size\n", .{ + stderr.writer.print("{d} more traces not shown; consider increasing trace size\n", .{ t.index - end, }) catch return; } diff --git a/lib/std/log.zig b/lib/std/log.zig index b75bada580..8579bb04e0 100644 --- a/lib/std/log.zig +++ b/lib/std/log.zig @@ -92,35 +92,33 @@ pub fn defaultLog( args: anytype, ) void { var buffer: [64]u8 = undefined; - const stderr = std.debug.lockStderrWriter(&buffer); - defer std.debug.unlockStderrWriter(); - return defaultLogFileWriter(level, scope, format, args, stderr); + const stderr = std.debug.lockStderr(&buffer); + defer std.debug.unlockStderr(); + return defaultLogFileTerminal(level, scope, format, args, stderr) catch {}; } -pub fn defaultLogFileWriter( +pub fn defaultLogFileTerminal( comptime level: Level, comptime scope: @EnumLiteral(), comptime format: []const u8, args: anytype, - fw: *std.Io.File.Writer, -) void { - fw.setColor(switch (level) { + t: std.Io.Terminal, +) std.Io.Writer.Error!void { + t.setColor(switch (level) { .err => .red, .warn => .yellow, .info => .green, .debug => .magenta, }) catch {}; - fw.setColor(.bold) catch {}; - fw.interface.writeAll(level.asText()) catch return; - fw.setColor(.reset) catch {}; - fw.setColor(.dim) catch {}; - fw.setColor(.bold) catch {}; - if (scope != .default) { - fw.interface.print("({s})", .{@tagName(scope)}) catch return; - } - fw.interface.writeAll(": ") catch return; - fw.setColor(.reset) catch {}; - fw.interface.print(format ++ "\n", fw.mode.decorateArgs(args)) catch return; + t.setColor(.bold) catch {}; + try t.writer.writeAll(level.asText()); + t.setColor(.reset) catch {}; + t.setColor(.dim) catch {}; + t.setColor(.bold) catch {}; + if (scope != .default) try t.writer.print("({t})", .{scope}); + try t.writer.writeAll(": "); + t.setColor(.reset) catch {}; + try t.writer.print(format ++ "\n", args); } /// Returns a scoped logging namespace that logs all messages using the scope diff --git a/lib/std/process.zig b/lib/std/process.zig index 4b4c27fe20..45cd575133 100644 --- a/lib/std/process.zig +++ b/lib/std/process.zig @@ -21,6 +21,13 @@ pub const changeCurDirZ = posix.chdirZ; pub const GetCwdError = posix.GetCwdError; +/// This is the global, process-wide protection to coordinate stderr writes. +/// +/// The primary motivation for recursive mutex here is so that a panic while +/// stderr mutex is held still dumps the stack trace and other debug +/// information. +pub var stderr_thread_mutex: std.Thread.Mutex.Recursive = .init; + /// The result is a slice of `out_buffer`, from index `0`. /// On Windows, the result is encoded as [WTF-8](https://wtf-8.codeberg.page/). /// On other platforms, the result is an opaque sequence of bytes with no particular encoding. -- cgit v1.2.3 From 608145c2f07d90c46cdaa8bc2013f31b965a5b8b Mon Sep 17 00:00:00 2001 From: Andrew Kelley Date: Wed, 17 Dec 2025 17:00:41 -0800 Subject: fix more fallout from locking stderr --- lib/compiler/aro/aro/Diagnostics.zig | 28 ++- lib/compiler/build_runner.zig | 338 +++++++++++++++++------------------ lib/compiler/resinator/cli.zig | 10 +- lib/compiler/resinator/errors.zig | 6 +- lib/compiler/resinator/main.zig | 31 ++-- lib/compiler/test_runner.zig | 8 +- lib/std/Build.zig | 73 ++++---- lib/std/Build/Fuzz.zig | 18 +- lib/std/Build/Step.zig | 9 +- lib/std/Build/Step/Compile.zig | 17 +- lib/std/Build/Step/Run.zig | 22 +-- lib/std/Io.zig | 2 +- lib/std/Io/File/Writer.zig | 2 +- lib/std/Io/Terminal.zig | 22 --- lib/std/Io/Threaded.zig | 6 +- lib/std/debug.zig | 24 +-- lib/std/debug/simple_panic.zig | 6 +- lib/std/json/dynamic.zig | 6 +- lib/std/log.zig | 2 +- lib/std/process.zig | 2 +- lib/std/testing.zig | 21 +-- lib/std/zig.zig | 21 +-- lib/std/zig/ErrorBundle.zig | 72 ++++---- lib/std/zig/parser_test.zig | 16 +- src/Air/print.zig | 12 +- src/Compilation.zig | 45 +++-- src/InternPool.zig | 10 +- src/Zcu/PerThread.zig | 8 +- src/codegen/aarch64/Select.zig | 7 +- src/crash_report.zig | 6 +- src/libs/mingw.zig | 24 +-- src/libs/mingw/def.zig | 6 +- src/link/Coff.zig | 6 +- src/link/Elf2.zig | 6 +- src/main.zig | 12 +- 35 files changed, 446 insertions(+), 458 deletions(-) (limited to 'lib/std/process.zig') diff --git a/lib/compiler/aro/aro/Diagnostics.zig b/lib/compiler/aro/aro/Diagnostics.zig index bebcf4e3d2..b33efa3f85 100644 --- a/lib/compiler/aro/aro/Diagnostics.zig +++ b/lib/compiler/aro/aro/Diagnostics.zig @@ -24,20 +24,21 @@ pub const Message = struct { @"fatal error", }; - pub fn write(msg: Message, w: *std.Io.Writer, config: std.Io.File.Writer.Mode, details: bool) std.Io.tty.Config.SetColorError!void { - try config.setColor(w, .bold); + pub fn write(msg: Message, t: std.Io.Terminal, details: bool) std.Io.Terminal.SetColorError!void { + const w = t.writer; + try t.setColor(.bold); if (msg.location) |loc| { try w.print("{s}:{d}:{d}: ", .{ loc.path, loc.line_no, loc.col }); } switch (msg.effective_kind) { - .@"fatal error", .@"error" => try config.setColor(w, .bright_red), - .note => try config.setColor(w, .bright_cyan), - .warning => try config.setColor(w, .bright_magenta), + .@"fatal error", .@"error" => try t.setColor(.bright_red), + .note => try t.setColor(.bright_cyan), + .warning => try t.setColor(.bright_magenta), .off => unreachable, } try w.print("{s}: ", .{@tagName(msg.effective_kind)}); - try config.setColor(w, .white); + try t.setColor(.white); try w.writeAll(msg.text); if (msg.opt) |some| { if (msg.effective_kind == .@"error" and msg.kind != .@"error") { @@ -55,17 +56,17 @@ pub const Message = struct { if (!details or msg.location == null) { try w.writeAll("\n"); - try config.setColor(w, .reset); + try t.setColor(.reset); } else { const loc = msg.location.?; const trailer = if (loc.end_with_splice) "\\ " else ""; - try config.setColor(w, .reset); + try t.setColor(.reset); try w.print("\n{s}{s}\n", .{ loc.line, trailer }); try w.splatByteAll(' ', loc.width); - try config.setColor(w, .bold); - try config.setColor(w, .bright_green); + try t.setColor(.bold); + try t.setColor(.bright_green); try w.writeAll("^\n"); - try config.setColor(w, .reset); + try t.setColor(.reset); } try w.flush(); } @@ -290,10 +291,7 @@ pub const State = struct { const Diagnostics = @This(); output: union(enum) { - to_writer: struct { - writer: *std.Io.Writer, - color: std.Io.File.Writer.Mode, - }, + to_writer: std.Io.Terminal, to_list: struct { messages: std.ArrayList(Message) = .empty, arena: std.heap.ArenaAllocator, diff --git a/lib/compiler/build_runner.zig b/lib/compiler/build_runner.zig index d284d85747..1c30ecaadc 100644 --- a/lib/compiler/build_runner.zig +++ b/lib/compiler/build_runner.zig @@ -286,8 +286,8 @@ pub fn main() !void { const next_arg = nextArg(args, &arg_idx) orelse fatalWithHint("expected u16 after '{s}'", .{arg}); debounce_interval_ms = std.fmt.parseUnsigned(u16, next_arg, 0) catch |err| { - fatal("unable to parse debounce interval '{s}' as unsigned 16-bit integer: {s}\n", .{ - next_arg, @errorName(err), + fatal("unable to parse debounce interval '{s}' as unsigned 16-bit integer: {t}\n", .{ + next_arg, err, }); }; } else if (mem.eql(u8, arg, "--webui")) { @@ -429,6 +429,12 @@ pub fn main() !void { } } + graph.stderr_mode = switch (color) { + .auto => try .detect(io, .stderr()), + .on => .escape_codes, + .off => .no_color, + }; + if (webui_listen != null) { if (watch) fatal("using '--webui' and '--watch' together is not yet supported; consider omitting '--watch' in favour of the web UI \"Rebuild\" button", .{}); if (builtin.single_threaded) fatal("'--webui' is not yet supported on single-threaded hosts", .{}); @@ -522,7 +528,7 @@ pub fn main() !void { // Perhaps in the future there could be an Advanced Options flag // such as --debug-build-runner-leaks which would make this code // return instead of calling exit. - _ = io.lockStderrWriter(&.{}) catch {}; + _ = io.lockStderr(&.{}, graph.stderr_mode) catch {}; process.exit(1); }, else => |e| return e, @@ -554,9 +560,9 @@ pub fn main() !void { } rebuild: while (true) : (if (run.error_style.clearOnUpdate()) { - const stderr = try io.lockStderrWriter(&stdio_buffer_allocation); - defer io.unlockStderrWriter(); - try stderr.writeAllUnescaped("\x1B[2J\x1B[3J\x1B[H"); + const stderr = try io.lockStderr(&stdio_buffer_allocation, graph.stderr_mode); + defer io.unlockStderr(); + try stderr.file_writer.interface.writeAll("\x1B[2J\x1B[3J\x1B[H"); }) { if (run.web_server) |*ws| ws.startBuild(); @@ -730,7 +736,8 @@ fn runStepNames( fuzz: ?std.Build.Fuzz.Mode, ) !void { const gpa = run.gpa; - const io = b.graph.io; + const graph = b.graph; + const io = graph.io; const step_stack = &run.step_stack; { @@ -856,20 +863,19 @@ fn runStepNames( .none => break :summary, } - const stderr = try io.lockStderrWriter(&stdio_buffer_allocation); - defer io.unlockStderrWriter(); - - const w = &stderr.interface; - const fwm = stderr.mode; + const stderr = try io.lockStderr(&stdio_buffer_allocation, graph.stderr_mode); + defer io.unlockStderr(); + const t = stderr.terminal(); + const w = &stderr.file_writer.interface; const total_count = success_count + failure_count + pending_count + skipped_count; - fwm.setColor(w, .cyan) catch {}; - fwm.setColor(w, .bold) catch {}; + t.setColor(.cyan) catch {}; + t.setColor(.bold) catch {}; w.writeAll("Build Summary: ") catch {}; - fwm.setColor(w, .reset) catch {}; + t.setColor(.reset) catch {}; w.print("{d}/{d} steps succeeded", .{ success_count, total_count }) catch {}; { - fwm.setColor(w, .dim) catch {}; + t.setColor(.dim) catch {}; var first = true; if (skipped_count > 0) { w.print("{s}{d} skipped", .{ if (first) " (" else ", ", skipped_count }) catch {}; @@ -880,12 +886,12 @@ fn runStepNames( first = false; } if (!first) w.writeByte(')') catch {}; - fwm.setColor(w, .reset) catch {}; + t.setColor(.reset) catch {}; } if (test_count > 0) { w.print("; {d}/{d} tests passed", .{ test_pass_count, test_count }) catch {}; - fwm.setColor(w, .dim) catch {}; + t.setColor(.dim) catch {}; var first = true; if (test_skip_count > 0) { w.print("{s}{d} skipped", .{ if (first) " (" else ", ", test_skip_count }) catch {}; @@ -904,7 +910,7 @@ fn runStepNames( first = false; } if (!first) w.writeByte(')') catch {}; - fwm.setColor(w, .reset) catch {}; + t.setColor(.reset) catch {}; } w.writeAll("\n") catch {}; @@ -918,7 +924,7 @@ fn runStepNames( var print_node: PrintNode = .{ .parent = null }; if (step_names.len == 0) { print_node.last = true; - printTreeStep(b, b.default_step, run, w, fwm, &print_node, &step_stack_copy) catch {}; + printTreeStep(b, b.default_step, run, t, &print_node, &step_stack_copy) catch {}; } else { const last_index = if (run.summary == .all) b.top_level_steps.count() else blk: { var i: usize = step_names.len; @@ -937,7 +943,7 @@ fn runStepNames( for (step_names, 0..) |step_name, i| { const tls = b.top_level_steps.get(step_name).?; print_node.last = i + 1 == last_index; - printTreeStep(b, &tls.step, run, w, fwm, &print_node, &step_stack_copy) catch {}; + printTreeStep(b, &tls.step, run, t, &print_node, &step_stack_copy) catch {}; } } w.writeByte('\n') catch {}; @@ -954,7 +960,7 @@ fn runStepNames( if (run.error_style.verboseContext()) break :code 1; // failure; print build command break :code 2; // failure; do not print build command }; - _ = io.lockStderrWriter(&.{}) catch {}; + _ = io.lockStderr(&.{}, graph.stderr_mode) catch {}; process.exit(code); } @@ -963,33 +969,30 @@ const PrintNode = struct { last: bool = false, }; -fn printPrefix(node: *PrintNode, w: *Writer, fwm: File.Writer.Mode) !void { +fn printPrefix(node: *PrintNode, stderr: Io.Terminal) !void { const parent = node.parent orelse return; + const writer = stderr.writer; if (parent.parent == null) return; - try printPrefix(parent, w, fwm); + try printPrefix(parent, stderr); if (parent.last) { - try w.writeAll(" "); + try writer.writeAll(" "); } else { - try w.writeAll(switch (fwm) { - .terminal_escaped => "\x1B\x28\x30\x78\x1B\x28\x42 ", // │ + try writer.writeAll(switch (stderr.mode) { + .escape_codes => "\x1B\x28\x30\x78\x1B\x28\x42 ", // │ else => "| ", }); } } -fn printChildNodePrefix(w: *Writer, fwm: File.Writer.Mode) !void { - try w.writeAll(switch (fwm) { - .terminal_escaped => "\x1B\x28\x30\x6d\x71\x1B\x28\x42 ", // └─ +fn printChildNodePrefix(stderr: Io.Terminal) !void { + try stderr.writer.writeAll(switch (stderr.mode) { + .escape_codes => "\x1B\x28\x30\x6d\x71\x1B\x28\x42 ", // └─ else => "+- ", }); } -fn printStepStatus( - s: *Step, - stderr: *Writer, - fwm: File.Writer.Mode, - run: *const Run, -) !void { +fn printStepStatus(s: *Step, stderr: Io.Terminal, run: *const Run) !void { + const writer = stderr.writer; switch (s.state) { .precheck_unstarted => unreachable, .precheck_started => unreachable, @@ -997,139 +1000,135 @@ fn printStepStatus( .running => unreachable, .dependency_failure => { - try fwm.setColor(stderr, .dim); - try stderr.writeAll(" transitive failure\n"); - try fwm.setColor(stderr, .reset); + try stderr.setColor(.dim); + try writer.writeAll(" transitive failure\n"); + try stderr.setColor(.reset); }, .success => { - try fwm.setColor(stderr, .green); + try stderr.setColor(.green); if (s.result_cached) { - try stderr.writeAll(" cached"); + try writer.writeAll(" cached"); } else if (s.test_results.test_count > 0) { const pass_count = s.test_results.passCount(); assert(s.test_results.test_count == pass_count + s.test_results.skip_count); - try stderr.print(" {d} pass", .{pass_count}); + try writer.print(" {d} pass", .{pass_count}); if (s.test_results.skip_count > 0) { - try fwm.setColor(stderr, .reset); - try stderr.writeAll(", "); - try fwm.setColor(stderr, .yellow); - try stderr.print("{d} skip", .{s.test_results.skip_count}); + try stderr.setColor(.reset); + try writer.writeAll(", "); + try stderr.setColor(.yellow); + try writer.print("{d} skip", .{s.test_results.skip_count}); } - try fwm.setColor(stderr, .reset); - try stderr.print(" ({d} total)", .{s.test_results.test_count}); + try stderr.setColor(.reset); + try writer.print(" ({d} total)", .{s.test_results.test_count}); } else { - try stderr.writeAll(" success"); + try writer.writeAll(" success"); } - try fwm.setColor(stderr, .reset); + try stderr.setColor(.reset); if (s.result_duration_ns) |ns| { - try fwm.setColor(stderr, .dim); + try stderr.setColor(.dim); if (ns >= std.time.ns_per_min) { - try stderr.print(" {d}m", .{ns / std.time.ns_per_min}); + try writer.print(" {d}m", .{ns / std.time.ns_per_min}); } else if (ns >= std.time.ns_per_s) { - try stderr.print(" {d}s", .{ns / std.time.ns_per_s}); + try writer.print(" {d}s", .{ns / std.time.ns_per_s}); } else if (ns >= std.time.ns_per_ms) { - try stderr.print(" {d}ms", .{ns / std.time.ns_per_ms}); + try writer.print(" {d}ms", .{ns / std.time.ns_per_ms}); } else if (ns >= std.time.ns_per_us) { - try stderr.print(" {d}us", .{ns / std.time.ns_per_us}); + try writer.print(" {d}us", .{ns / std.time.ns_per_us}); } else { - try stderr.print(" {d}ns", .{ns}); + try writer.print(" {d}ns", .{ns}); } - try fwm.setColor(stderr, .reset); + try stderr.setColor(.reset); } if (s.result_peak_rss != 0) { const rss = s.result_peak_rss; - try fwm.setColor(stderr, .dim); + try stderr.setColor(.dim); if (rss >= 1000_000_000) { - try stderr.print(" MaxRSS:{d}G", .{rss / 1000_000_000}); + try writer.print(" MaxRSS:{d}G", .{rss / 1000_000_000}); } else if (rss >= 1000_000) { - try stderr.print(" MaxRSS:{d}M", .{rss / 1000_000}); + try writer.print(" MaxRSS:{d}M", .{rss / 1000_000}); } else if (rss >= 1000) { - try stderr.print(" MaxRSS:{d}K", .{rss / 1000}); + try writer.print(" MaxRSS:{d}K", .{rss / 1000}); } else { - try stderr.print(" MaxRSS:{d}B", .{rss}); + try writer.print(" MaxRSS:{d}B", .{rss}); } - try fwm.setColor(stderr, .reset); + try stderr.setColor(.reset); } - try stderr.writeAll("\n"); + try writer.writeAll("\n"); }, .skipped, .skipped_oom => |skip| { - try fwm.setColor(stderr, .yellow); - try stderr.writeAll(" skipped"); + try stderr.setColor(.yellow); + try writer.writeAll(" skipped"); if (skip == .skipped_oom) { - try stderr.writeAll(" (not enough memory)"); - try fwm.setColor(stderr, .dim); - try stderr.print(" upper bound of {d} exceeded runner limit ({d})", .{ s.max_rss, run.max_rss }); - try fwm.setColor(stderr, .yellow); + try writer.writeAll(" (not enough memory)"); + try stderr.setColor(.dim); + try writer.print(" upper bound of {d} exceeded runner limit ({d})", .{ s.max_rss, run.max_rss }); + try stderr.setColor(.yellow); } - try stderr.writeAll("\n"); - try fwm.setColor(stderr, .reset); + try writer.writeAll("\n"); + try stderr.setColor(.reset); }, .failure => { - try printStepFailure(s, stderr, fwm, false); - try fwm.setColor(stderr, .reset); + try printStepFailure(s, stderr, false); + try stderr.setColor(.reset); }, } } -fn printStepFailure( - s: *Step, - stderr: *Writer, - fwm: File.Writer.Mode, - dim: bool, -) !void { +fn printStepFailure(s: *Step, stderr: Io.Terminal, dim: bool) !void { + const w = stderr.writer; if (s.result_error_bundle.errorMessageCount() > 0) { - try fwm.setColor(stderr, .red); - try stderr.print(" {d} errors\n", .{ + try stderr.setColor(.red); + try w.print(" {d} errors\n", .{ s.result_error_bundle.errorMessageCount(), }); } else if (!s.test_results.isSuccess()) { // These first values include all of the test "statuses". Every test is either passsed, // skipped, failed, crashed, or timed out. - try fwm.setColor(stderr, .green); - try stderr.print(" {d} pass", .{s.test_results.passCount()}); - try fwm.setColor(stderr, .reset); - if (dim) try fwm.setColor(stderr, .dim); + try stderr.setColor(.green); + try w.print(" {d} pass", .{s.test_results.passCount()}); + try stderr.setColor(.reset); + if (dim) try stderr.setColor(.dim); if (s.test_results.skip_count > 0) { - try stderr.writeAll(", "); - try fwm.setColor(stderr, .yellow); - try stderr.print("{d} skip", .{s.test_results.skip_count}); - try fwm.setColor(stderr, .reset); - if (dim) try fwm.setColor(stderr, .dim); + try w.writeAll(", "); + try stderr.setColor(.yellow); + try w.print("{d} skip", .{s.test_results.skip_count}); + try stderr.setColor(.reset); + if (dim) try stderr.setColor(.dim); } if (s.test_results.fail_count > 0) { - try stderr.writeAll(", "); - try fwm.setColor(stderr, .red); - try stderr.print("{d} fail", .{s.test_results.fail_count}); - try fwm.setColor(stderr, .reset); - if (dim) try fwm.setColor(stderr, .dim); + try w.writeAll(", "); + try stderr.setColor(.red); + try w.print("{d} fail", .{s.test_results.fail_count}); + try stderr.setColor(.reset); + if (dim) try stderr.setColor(.dim); } if (s.test_results.crash_count > 0) { - try stderr.writeAll(", "); - try fwm.setColor(stderr, .red); - try stderr.print("{d} crash", .{s.test_results.crash_count}); - try fwm.setColor(stderr, .reset); - if (dim) try fwm.setColor(stderr, .dim); + try w.writeAll(", "); + try stderr.setColor(.red); + try w.print("{d} crash", .{s.test_results.crash_count}); + try stderr.setColor(.reset); + if (dim) try stderr.setColor(.dim); } if (s.test_results.timeout_count > 0) { - try stderr.writeAll(", "); - try fwm.setColor(stderr, .red); - try stderr.print("{d} timeout", .{s.test_results.timeout_count}); - try fwm.setColor(stderr, .reset); - if (dim) try fwm.setColor(stderr, .dim); + try w.writeAll(", "); + try stderr.setColor(.red); + try w.print("{d} timeout", .{s.test_results.timeout_count}); + try stderr.setColor(.reset); + if (dim) try stderr.setColor(.dim); } - try stderr.print(" ({d} total)", .{s.test_results.test_count}); + try w.print(" ({d} total)", .{s.test_results.test_count}); // Memory leaks are intentionally written after the total, because is isn't a test *status*, // but just a flag that any tests -- even passed ones -- can have. We also use a different // separator, so it looks like: // 2 pass, 1 skip, 2 fail (5 total); 2 leaks if (s.test_results.leak_count > 0) { - try stderr.writeAll("; "); - try fwm.setColor(stderr, .red); - try stderr.print("{d} leaks", .{s.test_results.leak_count}); - try fwm.setColor(stderr, .reset); - if (dim) try fwm.setColor(stderr, .dim); + try w.writeAll("; "); + try stderr.setColor(.red); + try w.print("{d} leaks", .{s.test_results.leak_count}); + try stderr.setColor(.reset); + if (dim) try stderr.setColor(.dim); } // It's usually not helpful to know how many error logs there were because they tend to @@ -1142,21 +1141,21 @@ fn printStepFailure( break :show alt_results.isSuccess(); }; if (show_err_logs) { - try stderr.writeAll("; "); - try fwm.setColor(stderr, .red); - try stderr.print("{d} error logs", .{s.test_results.log_err_count}); - try fwm.setColor(stderr, .reset); - if (dim) try fwm.setColor(stderr, .dim); + try w.writeAll("; "); + try stderr.setColor(.red); + try w.print("{d} error logs", .{s.test_results.log_err_count}); + try stderr.setColor(.reset); + if (dim) try stderr.setColor(.dim); } - try stderr.writeAll("\n"); + try w.writeAll("\n"); } else if (s.result_error_msgs.items.len > 0) { - try fwm.setColor(stderr, .red); - try stderr.writeAll(" failure\n"); + try stderr.setColor(.red); + try w.writeAll(" failure\n"); } else { assert(s.result_stderr.len > 0); - try fwm.setColor(stderr, .red); - try stderr.writeAll(" stderr\n"); + try stderr.setColor(.red); + try w.writeAll(" w\n"); } } @@ -1164,11 +1163,11 @@ fn printTreeStep( b: *std.Build, s: *Step, run: *const Run, - stderr: *Writer, - fwm: File.Writer.Mode, + stderr: Io.Terminal, parent_node: *PrintNode, step_stack: *std.AutoArrayHashMapUnmanaged(*Step, void), ) !void { + const writer = stderr.writer; const first = step_stack.swapRemove(s); const summary = run.summary; const skip = switch (summary) { @@ -1178,26 +1177,26 @@ fn printTreeStep( .failures => s.state == .success, }; if (skip) return; - try printPrefix(parent_node, stderr, fwm); + try printPrefix(parent_node, stderr); if (parent_node.parent != null) { if (parent_node.last) { - try printChildNodePrefix(stderr, fwm); + try printChildNodePrefix(stderr); } else { - try stderr.writeAll(switch (fwm) { - .terminal_escaped => "\x1B\x28\x30\x74\x71\x1B\x28\x42 ", // ├─ + try writer.writeAll(switch (stderr.mode) { + .escape_codes => "\x1B\x28\x30\x74\x71\x1B\x28\x42 ", // ├─ else => "+- ", }); } } - if (!first) try fwm.setColor(stderr, .dim); + if (!first) try stderr.setColor(.dim); // dep_prefix omitted here because it is redundant with the tree. - try stderr.writeAll(s.name); + try writer.writeAll(s.name); if (first) { - try printStepStatus(s, stderr, fwm, run); + try printStepStatus(s, stderr, run); const last_index = if (summary == .all) s.dependencies.items.len -| 1 else blk: { var i: usize = s.dependencies.items.len; @@ -1219,17 +1218,17 @@ fn printTreeStep( .parent = parent_node, .last = i == last_index, }; - try printTreeStep(b, dep, run, stderr, fwm, &print_node, step_stack); + try printTreeStep(b, dep, run, stderr, &print_node, step_stack); } } else { if (s.dependencies.items.len == 0) { - try stderr.writeAll(" (reused)\n"); + try writer.writeAll(" (reused)\n"); } else { - try stderr.print(" (+{d} more reused dependencies)\n", .{ + try writer.print(" (+{d} more reused dependencies)\n", .{ s.dependencies.items.len, }); } - try fwm.setColor(stderr, .reset); + try stderr.setColor(.reset); } } @@ -1300,7 +1299,8 @@ fn workerMakeOneStep( prog_node: std.Progress.Node, run: *Run, ) void { - const io = b.graph.io; + const graph = b.graph; + const io = graph.io; const gpa = run.gpa; // First, check the conditions for running this step. If they are not met, @@ -1369,11 +1369,11 @@ fn workerMakeOneStep( const show_error_msgs = s.result_error_msgs.items.len > 0; const show_stderr = s.result_stderr.len > 0; if (show_error_msgs or show_compile_errors or show_stderr) { - const stderr = io.lockStderrWriter(&stdio_buffer_allocation) catch |err| switch (err) { + const stderr = io.lockStderr(&stdio_buffer_allocation, graph.stderr_mode) catch |err| switch (err) { error.Canceled => return, }; - defer io.unlockStderrWriter(); - printErrorMessages(gpa, s, .{}, &stderr.interface, stderr.mode, run.error_style, run.multiline_errors) catch {}; + defer io.unlockStderr(); + printErrorMessages(gpa, s, .{}, stderr.terminal(), run.error_style, run.multiline_errors) catch {}; } handle_result: { @@ -1440,11 +1440,11 @@ pub fn printErrorMessages( gpa: Allocator, failing_step: *Step, options: std.zig.ErrorBundle.RenderOptions, - stderr: *Writer, - fwm: File.Writer.Mode, + stderr: Io.Terminal, error_style: ErrorStyle, multiline_errors: MultilineErrors, ) !void { + const writer = stderr.writer; if (error_style.verboseContext()) { // Provide context for where these error messages are coming from by // printing the corresponding Step subtree. @@ -1456,70 +1456,70 @@ pub fn printErrorMessages( } // Now, `step_stack` has the subtree that we want to print, in reverse order. - try fwm.setColor(stderr, .dim); + try stderr.setColor(.dim); var indent: usize = 0; while (step_stack.pop()) |s| : (indent += 1) { if (indent > 0) { - try stderr.splatByteAll(' ', (indent - 1) * 3); - try printChildNodePrefix(stderr, fwm); + try writer.splatByteAll(' ', (indent - 1) * 3); + try printChildNodePrefix(stderr); } - try stderr.writeAll(s.name); + try writer.writeAll(s.name); if (s == failing_step) { - try printStepFailure(s, stderr, fwm, true); + try printStepFailure(s, stderr, true); } else { - try stderr.writeAll("\n"); + try writer.writeAll("\n"); } } - try fwm.setColor(stderr, .reset); + try stderr.setColor(.reset); } else { // Just print the failing step itself. - try fwm.setColor(stderr, .dim); - try stderr.writeAll(failing_step.name); - try printStepFailure(failing_step, stderr, fwm, true); - try fwm.setColor(stderr, .reset); + try stderr.setColor(.dim); + try writer.writeAll(failing_step.name); + try printStepFailure(failing_step, stderr, true); + try stderr.setColor(.reset); } if (failing_step.result_stderr.len > 0) { - try stderr.writeAll(failing_step.result_stderr); + try writer.writeAll(failing_step.result_stderr); if (!mem.endsWith(u8, failing_step.result_stderr, "\n")) { - try stderr.writeAll("\n"); + try writer.writeAll("\n"); } } - try failing_step.result_error_bundle.renderToWriter(options, stderr, fwm); + try failing_step.result_error_bundle.renderToTerminal(options, stderr); for (failing_step.result_error_msgs.items) |msg| { - try fwm.setColor(stderr, .red); - try stderr.writeAll("error:"); - try fwm.setColor(stderr, .reset); + try stderr.setColor(.red); + try writer.writeAll("error:"); + try stderr.setColor(.reset); if (std.mem.indexOfScalar(u8, msg, '\n') == null) { - try stderr.print(" {s}\n", .{msg}); + try writer.print(" {s}\n", .{msg}); } else switch (multiline_errors) { .indent => { var it = std.mem.splitScalar(u8, msg, '\n'); - try stderr.print(" {s}\n", .{it.first()}); + try writer.print(" {s}\n", .{it.first()}); while (it.next()) |line| { - try stderr.print(" {s}\n", .{line}); + try writer.print(" {s}\n", .{line}); } }, - .newline => try stderr.print("\n{s}\n", .{msg}), - .none => try stderr.print(" {s}\n", .{msg}), + .newline => try writer.print("\n{s}\n", .{msg}), + .none => try writer.print(" {s}\n", .{msg}), } } if (error_style.verboseContext()) { if (failing_step.result_failed_command) |cmd_str| { - try fwm.setColor(stderr, .red); - try stderr.writeAll("failed command: "); - try fwm.setColor(stderr, .reset); - try stderr.writeAll(cmd_str); - try stderr.writeByte('\n'); + try stderr.setColor(.red); + try writer.writeAll("failed command: "); + try stderr.setColor(.reset); + try writer.writeAll(cmd_str); + try writer.writeByte('\n'); } } - try stderr.writeByte('\n'); + try writer.writeByte('\n'); } fn printSteps(builder: *std.Build, w: *Writer) !void { diff --git a/lib/compiler/resinator/cli.zig b/lib/compiler/resinator/cli.zig index 08befbe1fa..ddf3ca18b7 100644 --- a/lib/compiler/resinator/cli.zig +++ b/lib/compiler/resinator/cli.zig @@ -126,14 +126,14 @@ pub const Diagnostics = struct { } pub fn renderToStderr(self: *Diagnostics, io: Io, args: []const []const u8) void { - const stderr = io.lockStderrWriter(&.{}); - defer io.unlockStderrWriter(); - self.renderToWriter(args, &stderr.interface, stderr.mode) catch return; + const stderr = io.lockStderr(&.{}, null); + defer io.unlockStderr(); + self.renderToWriter(args, stderr.terminal()) catch return; } - pub fn renderToWriter(self: *Diagnostics, args: []const []const u8, writer: *std.Io.Writer, config: std.Io.tty.Config) !void { + pub fn renderToWriter(self: *Diagnostics, args: []const []const u8, t: Io.Terminal) !void { for (self.errors.items) |err_details| { - try renderErrorMessage(writer, config, err_details, args); + try renderErrorMessage(t, err_details, args); } } diff --git a/lib/compiler/resinator/errors.zig b/lib/compiler/resinator/errors.zig index 031b5d3574..6146f777cd 100644 --- a/lib/compiler/resinator/errors.zig +++ b/lib/compiler/resinator/errors.zig @@ -69,10 +69,10 @@ pub const Diagnostics = struct { pub fn renderToStderr(self: *Diagnostics, cwd: Io.Dir, source: []const u8, source_mappings: ?SourceMappings) void { const io = self.io; - const stderr = io.lockStderrWriter(&.{}); - defer io.unlockStderrWriter(); + const stderr = io.lockStderr(&.{}, null); + defer io.unlockStderr(); for (self.errors.items) |err_details| { - renderErrorMessage(io, &stderr.interface, stderr.mode, cwd, err_details, source, self.strings.items, source_mappings) catch return; + renderErrorMessage(io, stderr.terminal(), cwd, err_details, source, self.strings.items, source_mappings) catch return; } } diff --git a/lib/compiler/resinator/main.zig b/lib/compiler/resinator/main.zig index bd2fcc912e..81a3a8689a 100644 --- a/lib/compiler/resinator/main.zig +++ b/lib/compiler/resinator/main.zig @@ -35,8 +35,8 @@ pub fn main() !void { const args = try std.process.argsAlloc(arena); if (args.len < 2) { - const stderr = io.lockStderrWriter(&.{}); - try renderErrorMessage(&stderr.interface, stderr.mode, .err, "expected zig lib dir as first argument", .{}); + const stderr = try io.lockStderr(&.{}, null); + try renderErrorMessage(stderr.terminal(), .err, "expected zig lib dir as first argument", .{}); std.process.exit(1); } const zig_lib_dir = args[1]; @@ -80,9 +80,9 @@ pub fn main() !void { // so that there is a clear separation between the cli diagnostics and whatever // gets printed after if (cli_diagnostics.errors.items.len > 0) { - const stderr = io.lockStderrWriter(&.{}); - defer io.unlockStderrWriter(); - try stderr.interface.writeByte('\n'); + const stderr = try io.lockStderr(&.{}, null); + defer io.unlockStderr(); + try stderr.file_writer.interface.writeByte('\n'); } } break :options options; @@ -130,15 +130,12 @@ pub fn main() !void { var stderr_buf: [512]u8 = undefined; var diagnostics: aro.Diagnostics = .{ .output = output: { if (zig_integration) break :output .{ .to_list = .{ .arena = .init(gpa) } }; - const stderr = io.lockStderrWriter(&stderr_buf); - break :output .{ .to_writer = .{ - .writer = &stderr.interface, - .color = stderr.mode, - } }; + const stderr = try io.lockStderr(&stderr_buf, null); + break :output .{ .to_writer = stderr.terminal() }; } }; defer { diagnostics.deinit(); - if (!zig_integration) std.debug.unlockStderrWriter(); + if (!zig_integration) std.debug.unlockStderr(); } var comp = aro.Compilation.init(aro_arena, aro_arena, io, &diagnostics, Io.Dir.cwd()); @@ -699,9 +696,9 @@ const ErrorHandler = union(enum) { }, .stderr => { // aro errors have already been emitted - const stderr = io.lockStderrWriter(&.{}); - defer io.unlockStderrWriter(); - try renderErrorMessage(&stderr.interface, stderr.mode, .err, "{s}", .{fail_msg}); + const stderr = io.lockStderr(&.{}, null); + defer io.unlockStderr(); + try renderErrorMessage(stderr.terminal(), .err, "{s}", .{fail_msg}); }, } } @@ -745,9 +742,9 @@ const ErrorHandler = union(enum) { try server.serveErrorBundle(error_bundle); }, .stderr => { - const stderr = io.lockStderrWriter(&.{}); - defer io.unlockStderrWriter(); - try renderErrorMessage(&stderr.interface, stderr.mode, msg_type, format, args); + const stderr = try io.lockStderr(&.{}, null); + defer io.unlockStderr(); + try renderErrorMessage(stderr.terminal(), msg_type, format, args); }, } } diff --git a/lib/compiler/test_runner.zig b/lib/compiler/test_runner.zig index 3a135ab8fd..3d97944a7f 100644 --- a/lib/compiler/test_runner.zig +++ b/lib/compiler/test_runner.zig @@ -405,18 +405,18 @@ pub fn fuzz( testOne(ctx, input.toSlice()) catch |err| switch (err) { error.SkipZigTest => return, else => { - const stderr = std.debug.lockStderrWriter(&.{}); + const stderr = std.debug.lockStderr(&.{}, null).terminal(); p: { if (@errorReturnTrace()) |trace| { - std.debug.writeStackTrace(trace, &stderr.interface, stderr.mode) catch break :p; + std.debug.writeStackTrace(trace, stderr) catch break :p; } - stderr.interface.print("failed with error.{t}\n", .{err}) catch break :p; + stderr.writer.print("failed with error.{t}\n", .{err}) catch break :p; } std.process.exit(1); }, }; if (log_err_count != 0) { - const stderr = std.debug.lockStderrWriter(&.{}); + const stderr = std.debug.lockStderr(&.{}, .no_color); stderr.interface.print("error logs detected\n", .{}) catch {}; std.process.exit(1); } diff --git a/lib/std/Build.zig b/lib/std/Build.zig index de2985d2b5..cf0b9e5b0d 100644 --- a/lib/std/Build.zig +++ b/lib/std/Build.zig @@ -129,6 +129,9 @@ pub const Graph = struct { dependency_cache: InitializedDepMap = .empty, allow_so_scripts: ?bool = null, time_report: bool, + /// Similar to the `Io.Terminal.Mode` returned by `Io.lockStderr`, but also + /// respects the '--color' flag. + stderr_mode: ?Io.Terminal.Mode = null, }; const AvailableDeps = []const struct { []const u8, []const u8 }; @@ -2255,9 +2258,10 @@ pub const GeneratedFile = struct { pub fn getPath3(gen: GeneratedFile, src_builder: *Build, asking_step: ?*Step) Io.Cancelable![]const u8 { return gen.path orelse { - const io = gen.step.owner.graph.io; - const stderr = try io.lockStderrWriter(&.{}); - dumpBadGetPathHelp(gen.step, &stderr.interface, stderr.mode, src_builder, asking_step) catch {}; + const graph = gen.step.owner.graph; + const io = graph.io; + const stderr = try io.lockStderr(&.{}, graph.stderr_mode); + dumpBadGetPathHelp(gen.step, stderr.terminal(), src_builder, asking_step) catch {}; @panic("misconfigured build script"); }; } @@ -2468,13 +2472,15 @@ pub const LazyPath = union(enum) { // TODO make gen.file.path not be absolute and use that as the // basis for not traversing up too many directories. + const graph = src_builder.graph; + var file_path: Cache.Path = .{ .root_dir = Cache.Directory.cwd(), .sub_path = gen.file.path orelse { - const io = src_builder.graph.io; - const stderr = try io.lockStderrWriter(&.{}); - dumpBadGetPathHelp(gen.file.step, &stderr.interface, stderr.mode, src_builder, asking_step) catch {}; - io.unlockStderrWriter(); + const io = graph.io; + const stderr = try io.lockStderr(&.{}, graph.stderr_mode); + dumpBadGetPathHelp(gen.file.step, stderr.terminal(), src_builder, asking_step) catch {}; + io.unlockStderr(); @panic("misconfigured build script"); }, }; @@ -2564,43 +2570,36 @@ fn dumpBadDirnameHelp( comptime msg: []const u8, args: anytype, ) anyerror!void { - const stderr = std.debug.lockStderrWriter(&.{}); - defer std.debug.unlockStderrWriter(); - - const w = &stderr.interface; - const fwm = stderr.mode; + const stderr = std.debug.lockStderr(&.{}).terminal(); + defer std.debug.unlockStderr(); + const w = stderr.writer; try w.print(msg, args); if (fail_step) |s| { - fwm.setColor(w, .red) catch {}; + stderr.setColor(.red) catch {}; try w.writeAll(" The step was created by this stack trace:\n"); - fwm.setColor(w, .reset) catch {}; + stderr.setColor(.reset) catch {}; - s.dump(w, fwm); + s.dump(stderr); } if (asking_step) |as| { - fwm.setColor(w, .red) catch {}; + stderr.setColor(.red) catch {}; try w.print(" The step '{s}' that is missing a dependency on the above step was created by this stack trace:\n", .{as.name}); - fwm.setColor(w, .reset) catch {}; + stderr.setColor(.reset) catch {}; - as.dump(w, fwm); + as.dump(stderr); } - fwm.setColor(w, .red) catch {}; - try w.writeAll(" Hope that helps. Proceeding to panic.\n"); - fwm.setColor(w, .reset) catch {}; + stderr.setColor(.red) catch {}; + try w.writeAll(" Proceeding to panic.\n"); + stderr.setColor(.reset) catch {}; } /// In this function the stderr mutex has already been locked. -pub fn dumpBadGetPathHelp( - s: *Step, - w: *Io.Writer, - fwm: File.Writer.Mode, - src_builder: *Build, - asking_step: ?*Step, -) anyerror!void { +pub fn dumpBadGetPathHelp(s: *Step, t: Io.Terminal, src_builder: *Build, asking_step: ?*Step) anyerror!void { + const w = t.writer; try w.print( \\getPath() was called on a GeneratedFile that wasn't built yet. \\ source package path: {s} @@ -2611,21 +2610,21 @@ pub fn dumpBadGetPathHelp( s.name, }); - fwm.setColor(w, .red) catch {}; + t.setColor(.red) catch {}; try w.writeAll(" The step was created by this stack trace:\n"); - fwm.setColor(w, .reset) catch {}; + t.setColor(.reset) catch {}; - s.dump(w, fwm); + s.dump(t); if (asking_step) |as| { - fwm.setColor(w, .red) catch {}; + t.setColor(.red) catch {}; try w.print(" The step '{s}' that is missing a dependency on the above step was created by this stack trace:\n", .{as.name}); - fwm.setColor(w, .reset) catch {}; + t.setColor(.reset) catch {}; - as.dump(w, fwm); + as.dump(t); } - fwm.setColor(w, .red) catch {}; - try w.writeAll(" Hope that helps. Proceeding to panic.\n"); - fwm.setColor(w, .reset) catch {}; + t.setColor(.red) catch {}; + try w.writeAll(" Proceeding to panic.\n"); + t.setColor(.reset) catch {}; } pub const InstallDir = union(enum) { diff --git a/lib/std/Build/Fuzz.zig b/lib/std/Build/Fuzz.zig index 4770e03a0c..d308efdf70 100644 --- a/lib/std/Build/Fuzz.zig +++ b/lib/std/Build/Fuzz.zig @@ -158,7 +158,8 @@ fn rebuildTestsWorkerRun(run: *Step.Run, gpa: Allocator, parent_prog_node: std.P } fn rebuildTestsWorkerRunFallible(run: *Step.Run, gpa: Allocator, parent_prog_node: std.Progress.Node) !void { - const io = run.step.owner.graph.io; + const graph = run.step.owner.graph; + const io = graph.io; const compile = run.producer.?; const prog_node = parent_prog_node.start(compile.step.name, 0); defer prog_node.end(); @@ -171,9 +172,9 @@ fn rebuildTestsWorkerRunFallible(run: *Step.Run, gpa: Allocator, parent_prog_nod if (show_error_msgs or show_compile_errors or show_stderr) { var buf: [256]u8 = undefined; - const stderr = try io.lockStderrWriter(&buf); - defer io.unlockStderrWriter(); - build_runner.printErrorMessages(gpa, &compile.step, .{}, &stderr.interface, stderr.mode, .verbose, .indent) catch {}; + const stderr = try io.lockStderr(&buf, graph.stderr_mode); + defer io.unlockStderr(); + build_runner.printErrorMessages(gpa, &compile.step, .{}, stderr.terminal(), .verbose, .indent) catch {}; } const rebuilt_bin_path = result catch |err| switch (err) { @@ -186,7 +187,8 @@ fn rebuildTestsWorkerRunFallible(run: *Step.Run, gpa: Allocator, parent_prog_nod fn fuzzWorkerRun(fuzz: *Fuzz, run: *Step.Run, unit_test_index: u32) void { const owner = run.step.owner; const gpa = owner.allocator; - const io = owner.graph.io; + const graph = owner.graph; + const io = graph.io; const test_name = run.cached_test_metadata.?.testName(unit_test_index); const prog_node = fuzz.prog_node.start(test_name, 0); @@ -195,11 +197,11 @@ fn fuzzWorkerRun(fuzz: *Fuzz, run: *Step.Run, unit_test_index: u32) void { run.rerunInFuzzMode(fuzz, unit_test_index, prog_node) catch |err| switch (err) { error.MakeFailed => { var buf: [256]u8 = undefined; - const stderr = io.lockStderrWriter(&buf) catch |e| switch (e) { + const stderr = io.lockStderr(&buf, graph.stderr_mode) catch |e| switch (e) { error.Canceled => return, }; - defer io.unlockStderrWriter(); - build_runner.printErrorMessages(gpa, &run.step, .{}, &stderr.interface, stderr.mode, .verbose, .indent) catch {}; + defer io.unlockStderr(); + build_runner.printErrorMessages(gpa, &run.step, .{}, stderr.terminal(), .verbose, .indent) catch {}; return; }, else => { diff --git a/lib/std/Build/Step.zig b/lib/std/Build/Step.zig index 0df58b24b7..74b41634a7 100644 --- a/lib/std/Build/Step.zig +++ b/lib/std/Build/Step.zig @@ -328,16 +328,17 @@ pub fn cast(step: *Step, comptime T: type) ?*T { } /// For debugging purposes, prints identifying information about this Step. -pub fn dump(step: *Step, w: *Io.Writer, fwm: Io.File.Writer.Mode) void { +pub fn dump(step: *Step, t: Io.Terminal) void { + const w = t.writer; if (step.debug_stack_trace.instruction_addresses.len > 0) { w.print("name: '{s}'. creation stack trace:\n", .{step.name}) catch {}; - std.debug.writeStackTrace(&step.debug_stack_trace, w, fwm) catch {}; + std.debug.writeStackTrace(&step.debug_stack_trace, t) catch {}; } else { const field = "debug_stack_frames_count"; comptime assert(@hasField(Build, field)); - fwm.setColor(w, .yellow) catch {}; + t.setColor(.yellow) catch {}; w.print("name: '{s}'. no stack trace collected for this step, see std.Build." ++ field ++ "\n", .{step.name}) catch {}; - fwm.setColor(w, .reset) catch {}; + t.setColor(.reset) catch {}; } } diff --git a/lib/std/Build/Step/Compile.zig b/lib/std/Build/Step/Compile.zig index 888775884d..4752046089 100644 --- a/lib/std/Build/Step/Compile.zig +++ b/lib/std/Build/Step/Compile.zig @@ -925,20 +925,21 @@ const CliNamedModules = struct { fn getGeneratedFilePath(compile: *Compile, comptime tag_name: []const u8, asking_step: ?*Step) ![]const u8 { const step = &compile.step; const b = step.owner; - const io = b.graph.io; + const graph = b.graph; + const io = graph.io; const maybe_path: ?*GeneratedFile = @field(compile, tag_name); const generated_file = maybe_path orelse { - const stderr = try io.lockStderrWriter(&.{}); - std.Build.dumpBadGetPathHelp(&compile.step, &stderr.interface, stderr.mode, compile.step.owner, asking_step) catch {}; - io.unlockStderrWriter(); + const stderr = try io.lockStderr(&.{}, graph.stderr_mode); + std.Build.dumpBadGetPathHelp(&compile.step, stderr.terminal(), compile.step.owner, asking_step) catch {}; + io.unlockStderr(); @panic("missing emit option for " ++ tag_name); }; const path = generated_file.path orelse { - const stderr = try io.lockStderrWriter(&.{}); - std.Build.dumpBadGetPathHelp(&compile.step, &stderr.interface, stderr.mode, compile.step.owner, asking_step) catch {}; - io.unlockStderrWriter(); + const stderr = try io.lockStderr(&.{}, graph.stderr_mode); + std.Build.dumpBadGetPathHelp(&compile.step, stderr.terminal(), compile.step.owner, asking_step) catch {}; + io.unlockStderr(); @panic(tag_name ++ " is null. Is there a missing step dependency?"); }; @@ -1907,7 +1908,7 @@ fn checkCompileErrors(compile: *Compile) !void { try actual_eb.renderToWriter(.{ .include_reference_trace = false, .include_source_line = false, - }, &aw.writer, .streaming); + }, &aw.writer); break :ae try aw.toOwnedSlice(); }; diff --git a/lib/std/Build/Step/Run.zig b/lib/std/Build/Step/Run.zig index 67cf201374..157a0292e7 100644 --- a/lib/std/Build/Step/Run.zig +++ b/lib/std/Build/Step/Run.zig @@ -1559,7 +1559,8 @@ fn spawnChildAndCollect( ) !?EvalGenericResult { const b = run.step.owner; const arena = b.allocator; - const io = b.graph.io; + const graph = b.graph; + const io = graph.io; if (fuzz_context != null) { assert(!has_side_effects); @@ -1625,11 +1626,12 @@ fn spawnChildAndCollect( if (!run.disable_zig_progress and !inherit) { child.progress_node = options.progress_node; } - if (inherit) { - const stderr = try io.lockStderrWriter(&.{}); - try setColorEnvironmentVariables(run, env_map, stderr.mode); - } - defer if (inherit) io.unlockStderrWriter(); + const terminal_mode: Io.Terminal.Mode = if (inherit) m: { + const stderr = try io.lockStderr(&.{}, graph.stderr_mode); + break :m stderr.terminal_mode; + } else .no_color; + defer if (inherit) io.unlockStderr(); + try setColorEnvironmentVariables(run, env_map, terminal_mode); var timer = try std.time.Timer.start(); const res = try evalGeneric(run, &child); run.step.result_duration_ns = timer.read(); @@ -1637,7 +1639,7 @@ fn spawnChildAndCollect( } } -fn setColorEnvironmentVariables(run: *Run, env_map: *EnvMap, fwm: Io.File.Writer.Mode) !void { +fn setColorEnvironmentVariables(run: *Run, env_map: *EnvMap, terminal_mode: Io.Terminal.Mode) !void { color: switch (run.color) { .manual => {}, .enable => { @@ -1648,9 +1650,9 @@ fn setColorEnvironmentVariables(run: *Run, env_map: *EnvMap, fwm: Io.File.Writer try env_map.put("NO_COLOR", "1"); env_map.remove("CLICOLOR_FORCE"); }, - .inherit => switch (fwm) { - .terminal_escaped => continue :color .enable, - else => continue :color .disable, + .inherit => switch (terminal_mode) { + .no_color, .windows_api => continue :color .disable, + .escape_codes => continue :color .enable, }, .auto => { const capture_stderr = run.captured_stderr != null or switch (run.stdio) { diff --git a/lib/std/Io.zig b/lib/std/Io.zig index 6fa05b16d2..78049633a0 100644 --- a/lib/std/Io.zig +++ b/lib/std/Io.zig @@ -713,7 +713,7 @@ pub const VTable = struct { processExecutableOpen: *const fn (?*anyopaque, File.OpenFlags) std.process.OpenExecutableError!File, processExecutablePath: *const fn (?*anyopaque, buffer: []u8) std.process.ExecutablePathError!usize, lockStderr: *const fn (?*anyopaque, buffer: []u8, ?Terminal.Mode) Cancelable!LockedStderr, - tryLockStderr: *const fn (?*anyopaque, buffer: []u8) Cancelable!?LockedStderr, + tryLockStderr: *const fn (?*anyopaque, buffer: []u8, ?Terminal.Mode) Cancelable!?LockedStderr, unlockStderr: *const fn (?*anyopaque) void, now: *const fn (?*anyopaque, Clock) Clock.Error!Timestamp, diff --git a/lib/std/Io/File/Writer.zig b/lib/std/Io/File/Writer.zig index 1d0494c674..56a1c09340 100644 --- a/lib/std/Io/File/Writer.zig +++ b/lib/std/Io/File/Writer.zig @@ -229,7 +229,7 @@ pub fn seekToUnbuffered(w: *Writer, offset: u64) SeekError!void { .positional, .positional_simple => { w.pos = offset; }, - .streaming, .streaming_simple, .terminal_escaped, .terminal_winapi => { + .streaming, .streaming_simple => { if (w.seek_err) |err| return err; io.vtable.fileSeekTo(io.userdata, w.file, offset) catch |err| { w.seek_err = err; diff --git a/lib/std/Io/Terminal.zig b/lib/std/Io/Terminal.zig index cd0ec56caa..a39f2175d2 100644 --- a/lib/std/Io/Terminal.zig +++ b/lib/std/Io/Terminal.zig @@ -130,25 +130,3 @@ pub fn setColor(t: Terminal, color: Color) Io.Writer.Error!void { }, } } - -pub fn disableEscape(t: *Terminal) Mode { - const prev = t.mode; - t.mode = t.mode.toUnescaped(); - return prev; -} - -pub fn restoreEscape(t: *Terminal, mode: Mode) void { - t.mode = mode; -} - -pub fn writeAllUnescaped(t: *Terminal, bytes: []const u8) Io.Writer.Error!void { - const prev_mode = t.disableEscape(); - defer t.restoreEscape(prev_mode); - return t.interface.writeAll(bytes); -} - -pub fn printUnescaped(t: *Terminal, comptime fmt: []const u8, args: anytype) Io.Writer.Error!void { - const prev_mode = t.disableEscape(); - defer t.restoreEscape(prev_mode); - return t.interface.print(fmt, args); -} diff --git a/lib/std/Io/Threaded.zig b/lib/std/Io/Threaded.zig index 3e7b6950d4..b07a9bb9a8 100644 --- a/lib/std/Io/Threaded.zig +++ b/lib/std/Io/Threaded.zig @@ -7178,7 +7178,7 @@ fn fileWriteFileStreaming( break :o .{ &off, @min(@intFromEnum(limit), size - file_reader.pos, max_count) }; }, .streaming => .{ null, limit.minInt(max_count) }, - .streaming_reading, .positional_reading => break :sf, + .streaming_simple, .positional_simple => break :sf, .failure => return error.ReadFailed, }; const current_thread = Thread.getCurrent(t); @@ -7251,7 +7251,7 @@ fn fileWriteFileStreaming( } var off_in: i64 = undefined; const off_in_ptr: ?*i64 = switch (file_reader.mode) { - .positional_reading, .streaming_reading => return error.Unimplemented, + .positional_simple, .streaming_simple => return error.Unimplemented, .positional => p: { off_in = @intCast(file_reader.pos); break :p &off_in; @@ -7427,7 +7427,7 @@ fn fileWriteFilePositional( } var off_in: i64 = undefined; const off_in_ptr: ?*i64 = switch (file_reader.mode) { - .positional_reading, .streaming_reading => return error.Unimplemented, + .positional_simple, .streaming_simple => return error.Unimplemented, .positional => p: { off_in = @intCast(file_reader.pos); break :p &off_in; diff --git a/lib/std/debug.zig b/lib/std/debug.zig index fd3a456ff7..e33a2bd575 100644 --- a/lib/std/debug.zig +++ b/lib/std/debug.zig @@ -283,12 +283,12 @@ var static_single_threaded_io: Io.Threaded = .init_single_threaded; /// /// Alternatively, use the higher-level `Io.lockStderr` to integrate with the /// application's chosen `Io` implementation. -pub fn lockStderr(buffer: []u8) Io.Terminal { - return (static_single_threaded_io.ioBasic().lockStderr(buffer, null) catch |err| switch (err) { +pub fn lockStderr(buffer: []u8) Io.LockedStderr { + return static_single_threaded_io.ioBasic().lockStderr(buffer, null) catch |err| switch (err) { // Impossible to cancel because no calls to cancel using // `static_single_threaded_io` exist. error.Canceled => unreachable, - }).terminal(); + }; } pub fn unlockStderr() void { @@ -311,7 +311,7 @@ pub fn print(comptime fmt: []const u8, args: anytype) void { var buffer: [64]u8 = undefined; const stderr = lockStderr(&buffer); defer unlockStderr(); - stderr.writer.print(fmt, args) catch return; + stderr.file_writer.interface.print(fmt, args) catch return; } } @@ -327,7 +327,7 @@ pub inline fn getSelfDebugInfo() !*SelfInfo { /// Tries to print a hexadecimal view of the bytes, unbuffered, and ignores any error returned. /// Obtains the stderr mutex while dumping. pub fn dumpHex(bytes: []const u8) void { - const stderr = lockStderr(&.{}); + const stderr = lockStderr(&.{}).terminal(); defer unlockStderr(); dumpHexFallible(stderr, bytes) catch {}; } @@ -551,7 +551,7 @@ pub fn defaultPanic( _ = panicking.fetchAdd(1, .seq_cst); trace: { - const stderr = lockStderr(&.{}); + const stderr = lockStderr(&.{}).terminal(); defer unlockStderr(); const writer = stderr.writer; @@ -581,7 +581,7 @@ pub fn defaultPanic( // A panic happened while trying to print a previous panic message. // We're still holding the mutex but that's fine as we're going to // call abort(). - const stderr = lockStderr(&.{}); + const stderr = lockStderr(&.{}).terminal(); stderr.writer.writeAll("aborting due to recursive panic\n") catch {}; }, else => {}, // Panicked while printing the recursive panic message. @@ -751,7 +751,7 @@ pub noinline fn writeCurrentStackTrace(options: StackUnwindOptions, t: Io.Termin } /// A thin wrapper around `writeCurrentStackTrace` which writes to stderr and ignores write errors. pub fn dumpCurrentStackTrace(options: StackUnwindOptions) void { - const stderr = lockStderr(&.{}); + const stderr = lockStderr(&.{}).terminal(); defer unlockStderr(); writeCurrentStackTrace(.{ .first_address = a: { @@ -814,7 +814,7 @@ pub fn writeStackTrace(st: *const StackTrace, t: Io.Terminal) Writer.Error!void } /// A thin wrapper around `writeStackTrace` which writes to stderr and ignores write errors. pub fn dumpStackTrace(st: *const StackTrace) void { - const stderr = lockStderr(&.{}); + const stderr = lockStderr(&.{}).terminal(); defer unlockStderr(); writeStackTrace(st, stderr) catch |err| switch (err) { error.WriteFailed => {}, @@ -1550,7 +1550,7 @@ pub fn defaultHandleSegfault(addr: ?usize, name: []const u8, opt_ctx: ?CpuContex _ = panicking.fetchAdd(1, .seq_cst); trace: { - const stderr = lockStderr(&.{}); + const stderr = lockStderr(&.{}).terminal(); defer unlockStderr(); if (addr) |a| { @@ -1571,7 +1571,7 @@ pub fn defaultHandleSegfault(addr: ?usize, name: []const u8, opt_ctx: ?CpuContex // A segfault happened while trying to print a previous panic message. // We're still holding the mutex but that's fine as we're going to // call abort(). - const stderr = lockStderr(&.{}); + const stderr = lockStderr(&.{}).terminal(); stderr.writer.writeAll("aborting due to recursive panic\n") catch {}; }, else => {}, // Panicked while printing the recursive panic message. @@ -1678,7 +1678,7 @@ pub fn ConfigurableTrace(comptime size: usize, comptime stack_frame_count: usize pub fn dump(t: @This()) void { if (!enabled) return; - const stderr = lockStderr(&.{}); + const stderr = lockStderr(&.{}).terminal(); defer unlockStderr(); const end = @min(t.index, size); for (t.addrs[0..end], 0..) |frames_array, i| { diff --git a/lib/std/debug/simple_panic.zig b/lib/std/debug/simple_panic.zig index 2d0e5abf4e..34e8f7b43c 100644 --- a/lib/std/debug/simple_panic.zig +++ b/lib/std/debug/simple_panic.zig @@ -14,9 +14,9 @@ const std = @import("../std.zig"); pub fn call(msg: []const u8, ra: ?usize) noreturn { @branchHint(.cold); _ = ra; - const stderr = std.debug.lockStderrWriter(&.{}); - stderr.interface.writeAll(msg) catch {}; - stderr.interface.flush(msg) catch {}; + const stderr_writer = std.debug.lockStderr(&.{}, null).terminal().writer; + stderr_writer.writeAll(msg) catch {}; + stderr_writer.flush(msg) catch {}; @trap(); } diff --git a/lib/std/json/dynamic.zig b/lib/std/json/dynamic.zig index f85916670b..e38ea9cb17 100644 --- a/lib/std/json/dynamic.zig +++ b/lib/std/json/dynamic.zig @@ -47,9 +47,9 @@ pub const Value = union(enum) { } pub fn dump(v: Value) void { - const stderr = std.debug.lockStderrWriter(&.{}); - defer std.debug.unlockStderrWriter(); - json.Stringify.value(v, .{}, &stderr.interface) catch return; + const stderr = std.debug.lockStderr(&.{}, null); + defer std.debug.unlockStderr(); + json.Stringify.value(v, .{}, &stderr.file_writer.interface) catch return; } pub fn jsonStringify(value: @This(), jws: anytype) !void { diff --git a/lib/std/log.zig b/lib/std/log.zig index 8579bb04e0..81bfc4ebb6 100644 --- a/lib/std/log.zig +++ b/lib/std/log.zig @@ -92,7 +92,7 @@ pub fn defaultLog( args: anytype, ) void { var buffer: [64]u8 = undefined; - const stderr = std.debug.lockStderr(&buffer); + const stderr = std.debug.lockStderr(&buffer).terminal(); defer std.debug.unlockStderr(); return defaultLogFileTerminal(level, scope, format, args, stderr) catch {}; } diff --git a/lib/std/process.zig b/lib/std/process.zig index 45cd575133..23a3d92e9b 100644 --- a/lib/std/process.zig +++ b/lib/std/process.zig @@ -1856,7 +1856,7 @@ pub fn totalSystemMemory() TotalSystemMemoryError!u64 { /// and does not return. pub fn cleanExit(io: Io) void { if (builtin.mode == .Debug) return; - _ = io.lockStderrWriter(&.{}) catch {}; + _ = io.lockStderr(&.{}, .no_color) catch {}; exit(0); } diff --git a/lib/std/testing.zig b/lib/std/testing.zig index be6f316804..98fca2f2d5 100644 --- a/lib/std/testing.zig +++ b/lib/std/testing.zig @@ -368,8 +368,8 @@ pub fn expectEqualSlices(comptime T: type, expected: []const T, actual: []const break :diff_index if (expected.len == actual.len) return else shortest; }; if (!backend_can_print) return error.TestExpectedEqual; - if (io.lockStderrWriter(&.{})) |stderr| { - defer io.unlockStderrWriter(); + if (io.lockStderr(&.{}, null)) |stderr| { + defer io.unlockStderr(); failEqualSlices(T, expected, actual, diff_index, &stderr.interface, stderr.mode) catch {}; } else |_| {} return error.TestExpectedEqual; @@ -381,7 +381,7 @@ fn failEqualSlices( actual: []const T, diff_index: usize, w: *Io.Writer, - fwm: Io.File.Writer.Mode, + terminal_mode: Io.Terminal.Mode, ) !void { try w.print("slices differ. first difference occurs at index {d} (0x{X})\n", .{ diff_index, diff_index }); @@ -404,12 +404,12 @@ fn failEqualSlices( var differ = if (T == u8) BytesDiffer{ .expected = expected_window, .actual = actual_window, - .file_writer_mode = fwm, + .terminal_mode = terminal_mode, } else SliceDiffer(T){ .start_index = window_start, .expected = expected_window, .actual = actual_window, - .file_writer_mode = fwm, + .terminal_mode = terminal_mode, }; // Print indexes as hex for slices of u8 since it's more likely to be binary data where @@ -466,21 +466,22 @@ fn SliceDiffer(comptime T: type) type { start_index: usize, expected: []const T, actual: []const T, - file_writer_mode: Io.File.Writer.Mode, + terminal_mode: Io.Terminal.Mode, const Self = @This(); pub fn write(self: Self, writer: *Io.Writer) !void { + const t: Io.Terminal = .{ .writer = writer, .mode = self.terminal_mode }; for (self.expected, 0..) |value, i| { const full_index = self.start_index + i; const diff = if (i < self.actual.len) !std.meta.eql(self.actual[i], value) else true; - if (diff) try self.file_writer_mode.setColor(writer, .red); + if (diff) try t.setColor(writer, .red); if (@typeInfo(T) == .pointer) { try writer.print("[{}]{*}: {any}\n", .{ full_index, value, value }); } else { try writer.print("[{}]: {any}\n", .{ full_index, value }); } - if (diff) try self.file_writer_mode.setColor(writer, .reset); + if (diff) try t.setColor(writer, .reset); } } }; @@ -489,7 +490,7 @@ fn SliceDiffer(comptime T: type) type { const BytesDiffer = struct { expected: []const u8, actual: []const u8, - file_writer_mode: Io.File.Writer.Mode, + terminal_mode: Io.Terminal.Mode, pub fn write(self: BytesDiffer, writer: *Io.Writer) !void { var expected_iterator = std.mem.window(u8, self.expected, 16, 16); @@ -516,7 +517,7 @@ const BytesDiffer = struct { try self.writeDiff(writer, "{c}", .{byte}, diff); } else { // TODO: remove this `if` when https://github.com/ziglang/zig/issues/7600 is fixed - if (self.file_writer_mode == .terminal_winapi) { + if (self.terminal_mode == .windows_api) { try self.writeDiff(writer, ".", .{}, diff); continue; } diff --git a/lib/std/zig.zig b/lib/std/zig.zig index 6e89c75d91..6212264005 100644 --- a/lib/std/zig.zig +++ b/lib/std/zig.zig @@ -46,25 +46,18 @@ pub const SrcHasher = std.crypto.hash.Blake3; pub const SrcHash = [16]u8; pub const Color = enum { - /// Determine whether stderr is a terminal or not automatically. + /// Auto-detect whether stream supports terminal colors. auto, - /// Assume stderr is not a terminal. + /// Force-enable colors. off, - /// Assume stderr is a terminal. + /// Suppress colors. on, - pub fn getTtyConf(color: Color, detected: Io.File.Writer.Mode) Io.File.Writer.Mode { + pub fn terminalMode(color: Color) ?Io.Terminal.Mode { return switch (color) { - .auto => detected, - .on => .terminal_escaped, - .off => .streaming, - }; - } - pub fn detectTtyConf(color: Color, io: Io) Io.File.Writer.Mode { - return switch (color) { - .auto => .detect(io, .stderr()), - .on => .terminal_escaped, - .off => .streaming, + .auto => null, + .on => .escape_codes, + .off => .no_color, }; } }; diff --git a/lib/std/zig/ErrorBundle.zig b/lib/std/zig/ErrorBundle.zig index 556b1167a3..c5275729ae 100644 --- a/lib/std/zig/ErrorBundle.zig +++ b/lib/std/zig/ErrorBundle.zig @@ -166,51 +166,53 @@ pub const RenderToStderrError = Io.Cancelable || Io.File.Writer.Error; pub fn renderToStderr(eb: ErrorBundle, io: Io, options: RenderOptions, color: std.zig.Color) RenderToStderrError!void { var buffer: [256]u8 = undefined; - const stderr = try io.lockStderrWriter(&buffer); - defer io.unlockStderrWriter(); - renderToWriter(eb, options, &stderr.interface, color.getTtyConf(stderr.mode)) catch |err| switch (err) { - error.WriteFailed => return stderr.err.?, + const stderr = try io.lockStderr(&buffer, color.terminalMode()); + defer io.unlockStderr(); + renderToTerminal(eb, options, stderr.terminal()) catch |err| switch (err) { + error.WriteFailed => return stderr.file_writer.err.?, else => |e| return e, }; } -pub fn renderToWriter( - eb: ErrorBundle, - options: RenderOptions, - w: *Writer, - fwm: Io.File.Writer.Mode, -) Io.File.Writer.Mode.SetColorError!void { +pub fn renderToWriter(eb: ErrorBundle, options: RenderOptions, w: *Writer) Writer.Error!void { + return renderToTerminal(eb, options, .{ .writer = w, .mode = .no_color }) catch |err| switch (err) { + error.WriteFailed => |e| return e, + else => unreachable, + }; +} + +pub fn renderToTerminal(eb: ErrorBundle, options: RenderOptions, t: Io.Terminal) Io.Terminal.SetColorError!void { if (eb.extra.len == 0) return; for (eb.getMessages()) |err_msg| { - try renderErrorMessageToWriter(eb, options, err_msg, w, fwm, "error", .red, 0); + try renderErrorMessage(eb, options, err_msg, t, "error", .red, 0); } if (options.include_log_text) { const log_text = eb.getCompileLogOutput(); if (log_text.len != 0) { - try w.writeAll("\nCompile Log Output:\n"); - try w.writeAll(log_text); + try t.writer.writeAll("\nCompile Log Output:\n"); + try t.writer.writeAll(log_text); } } } -fn renderErrorMessageToWriter( +fn renderErrorMessage( eb: ErrorBundle, options: RenderOptions, err_msg_index: MessageIndex, - w: *Writer, - fwm: Io.File.Writer.Mode, + t: Io.Terminal, kind: []const u8, - color: Io.File.Writer.Color, + color: Io.Terminal.Color, indent: usize, -) Io.File.Writer.Mode.SetColorError!void { +) Io.Terminal.SetColorError!void { + const w = t.writer; const err_msg = eb.getErrorMessage(err_msg_index); if (err_msg.src_loc != .none) { const src = eb.extraData(SourceLocation, @intFromEnum(err_msg.src_loc)); var prefix: Writer.Discarding = .init(&.{}); try w.splatByteAll(' ', indent); prefix.count += indent; - try fwm.setColor(w, .bold); + try t.setColor(.bold); try w.print("{s}:{d}:{d}: ", .{ eb.nullTerminatedString(src.data.src_path), src.data.line + 1, @@ -221,7 +223,7 @@ fn renderErrorMessageToWriter( src.data.line + 1, src.data.column + 1, }); - try fwm.setColor(w, color); + try t.setColor(color); try w.writeAll(kind); prefix.count += kind.len; try w.writeAll(": "); @@ -229,17 +231,17 @@ fn renderErrorMessageToWriter( // This is the length of the part before the error message: // e.g. "file.zig:4:5: error: " const prefix_len: usize = @intCast(prefix.count); - try fwm.setColor(w, .reset); - try fwm.setColor(w, .bold); + try t.setColor(.reset); + try t.setColor(.bold); if (err_msg.count == 1) { try writeMsg(eb, err_msg, w, prefix_len); try w.writeByte('\n'); } else { try writeMsg(eb, err_msg, w, prefix_len); - try fwm.setColor(w, .dim); + try t.setColor(.dim); try w.print(" ({d} times)\n", .{err_msg.count}); } - try fwm.setColor(w, .reset); + try t.setColor(.reset); if (src.data.source_line != 0 and options.include_source_line) { const line = eb.nullTerminatedString(src.data.source_line); for (line) |b| switch (b) { @@ -252,19 +254,19 @@ fn renderErrorMessageToWriter( // -1 since span.main includes the caret const after_caret = src.data.span_end -| src.data.span_main -| 1; try w.splatByteAll(' ', src.data.column - before_caret); - try fwm.setColor(w, .green); + try t.setColor(.green); try w.splatByteAll('~', before_caret); try w.writeByte('^'); try w.splatByteAll('~', after_caret); try w.writeByte('\n'); - try fwm.setColor(w, .reset); + try t.setColor(.reset); } for (eb.getNotes(err_msg_index)) |note| { - try renderErrorMessageToWriter(eb, options, note, w, fwm, "note", .cyan, indent); + try renderErrorMessage(eb, options, note, t, "note", .cyan, indent); } if (src.data.reference_trace_len > 0 and options.include_reference_trace) { - try fwm.setColor(w, .reset); - try fwm.setColor(w, .dim); + try t.setColor(.reset); + try t.setColor(.dim); try w.print("referenced by:\n", .{}); var ref_index = src.end; for (0..src.data.reference_trace_len) |_| { @@ -291,25 +293,25 @@ fn renderErrorMessageToWriter( ); } } - try fwm.setColor(w, .reset); + try t.setColor(.reset); } } else { - try fwm.setColor(w, color); + try t.setColor(color); try w.splatByteAll(' ', indent); try w.writeAll(kind); try w.writeAll(": "); - try fwm.setColor(w, .reset); + try t.setColor(.reset); const msg = eb.nullTerminatedString(err_msg.msg); if (err_msg.count == 1) { try w.print("{s}\n", .{msg}); } else { try w.print("{s}", .{msg}); - try fwm.setColor(w, .dim); + try t.setColor(.dim); try w.print(" ({d} times)\n", .{err_msg.count}); } - try fwm.setColor(w, .reset); + try t.setColor(.reset); for (eb.getNotes(err_msg_index)) |note| { - try renderErrorMessageToWriter(eb, options, note, w, fwm, "note", .cyan, indent + 4); + try renderErrorMessage(eb, options, note, t, "note", .cyan, indent + 4); } } } diff --git a/lib/std/zig/parser_test.zig b/lib/std/zig/parser_test.zig index 6c11c3021b..94d877b406 100644 --- a/lib/std/zig/parser_test.zig +++ b/lib/std/zig/parser_test.zig @@ -6333,25 +6333,25 @@ var fixed_buffer_mem: [100 * 1024]u8 = undefined; fn testParse(io: Io, source: [:0]const u8, allocator: Allocator, anything_changed: *bool) ![]u8 { var buffer: [64]u8 = undefined; - const stderr = try io.lockStderrWriter(&buffer); - defer io.unlockStderrWriter(); + const stderr = try io.lockStderr(&buffer, null); + defer io.unlockStderr(); var tree = try std.zig.Ast.parse(allocator, source, .zig); defer tree.deinit(allocator); for (tree.errors) |parse_error| { const loc = tree.tokenLocation(0, parse_error.token); - try stderr.printUnescaped("(memory buffer):{d}:{d}: error: ", .{ loc.line + 1, loc.column + 1 }); - try tree.renderError(parse_error, &stderr.interface); - try stderr.interface.print("\n{s}\n", .{source[loc.line_start..loc.line_end]}); + try stderr.writer.print("(memory buffer):{d}:{d}: error: ", .{ loc.line + 1, loc.column + 1 }); + try tree.renderError(parse_error, stderr.writer); + try stderr.writer.print("\n{s}\n", .{source[loc.line_start..loc.line_end]}); { var i: usize = 0; while (i < loc.column) : (i += 1) { - try stderr.writeAllUnescaped(" "); + try stderr.writer.writeAll(" "); } - try stderr.writeAllUnescaped("^"); + try stderr.writer.writeAll("^"); } - try stderr.writeAllUnescaped("\n"); + try stderr.writer.writeAll("\n"); } if (tree.errors.len != 0) { return error.ParseError; diff --git a/src/Air/print.zig b/src/Air/print.zig index 05614a0ed3..98b0a0b242 100644 --- a/src/Air/print.zig +++ b/src/Air/print.zig @@ -76,9 +76,9 @@ pub fn dump(air: Air, pt: Zcu.PerThread, liveness: ?Air.Liveness) void { const comp = pt.zcu.comp; const io = comp.io; var buffer: [512]u8 = undefined; - const stderr = try io.lockStderrWriter(&buffer); - defer io.unlockStderrWriter(); - const w = &stderr.interface; + const stderr = try io.lockStderr(&buffer, null); + defer io.unlockStderr(); + const w = &stderr.file_writer.interface; air.write(w, pt, liveness); } @@ -86,9 +86,9 @@ pub fn dumpInst(air: Air, inst: Air.Inst.Index, pt: Zcu.PerThread, liveness: ?Ai const comp = pt.zcu.comp; const io = comp.io; var buffer: [512]u8 = undefined; - const stderr = try io.lockStderrWriter(&buffer); - defer io.unlockStderrWriter(); - const w = &stderr.interface; + const stderr = try io.lockStderr(&buffer, null); + defer io.unlockStderr(); + const w = &stderr.file_writer.interface; air.writeInst(w, inst, pt, liveness); } diff --git a/src/Compilation.zig b/src/Compilation.zig index 37e15ab171..f3fcef40a0 100644 --- a/src/Compilation.zig +++ b/src/Compilation.zig @@ -2092,14 +2092,16 @@ pub fn create(gpa: Allocator, arena: Allocator, io: Io, diag: *CreateDiagnostic, } if (options.verbose_llvm_cpu_features) { - if (options.root_mod.resolved_target.llvm_cpu_features) |cf| print: { - const stderr = try io.lockStderrWriter(&.{}); - defer io.unlockStderrWriter(); - const w = &stderr.interface; - w.print("compilation: {s}\n", .{options.root_name}) catch break :print; - w.print(" target: {s}\n", .{try target.zigTriple(arena)}) catch break :print; - w.print(" cpu: {s}\n", .{target.cpu.model.name}) catch break :print; - w.print(" features: {s}\n", .{cf}) catch {}; + if (options.root_mod.resolved_target.llvm_cpu_features) |cf| { + const stderr = try io.lockStderr(&.{}, null); + defer io.unlockStderr(); + const w = &stderr.file_writer.interface; + printVerboseLlvmCpuFeatures(w, arena, options.root_name, target, cf) catch |err| switch (err) { + error.WriteFailed => switch (stderr.file_writer.err.?) { + error.Canceled => |e| return e, + else => {}, + }, + }; } } @@ -2700,6 +2702,19 @@ pub fn create(gpa: Allocator, arena: Allocator, io: Io, diag: *CreateDiagnostic, return comp; } +fn printVerboseLlvmCpuFeatures( + w: *Writer, + arena: Allocator, + root_name: []const u8, + target: *const std.Target, + cf: [*:0]const u8, +) Writer.Error!void { + try w.print("compilation: {s}\n", .{root_name}); + try w.print(" target: {s}\n", .{try target.zigTriple(arena)}); + try w.print(" cpu: {s}\n", .{target.cpu.model.name}); + try w.print(" features: {s}\n", .{cf}); +} + pub fn destroy(comp: *Compilation) void { const gpa = comp.gpa; const io = comp.io; @@ -4259,9 +4274,9 @@ pub fn getAllErrorsAlloc(comp: *Compilation) error{OutOfMemory}!ErrorBundle { // However, we haven't reported any such error. // This is a compiler bug. print_ctx: { - const stderr = std.debug.lockStderrWriter(&.{}); - defer std.debug.unlockStderrWriter(); - const w = &stderr.interface; + const stderr = std.debug.lockStderr(&.{}).terminal(); + defer std.debug.unlockStderr(); + const w = stderr.writer; w.writeAll("referenced transitive analysis errors, but none actually emitted\n") catch break :print_ctx; w.print("{f} [transitive failure]\n", .{zcu.fmtAnalUnit(failed_unit)}) catch break :print_ctx; while (ref) |r| { @@ -7772,11 +7787,11 @@ pub fn lockAndSetMiscFailure( pub fn dumpArgv(io: Io, argv: []const []const u8) Io.Cancelable!void { var buffer: [64]u8 = undefined; - const stderr = try io.lockStderrWriter(&buffer); - defer io.unlockStderrWriter(); - const w = &stderr.interface; + const stderr = try io.lockStderr(&buffer); + defer io.unlockStderr(); + const w = &stderr.file_writer.interface; return dumpArgvWriter(w, argv) catch |err| switch (err) { - error.WriteFailed => switch (stderr.err.?) { + error.WriteFailed => switch (stderr.file_writer.err.?) { error.Canceled => return error.Canceled, else => return, }, diff --git a/src/InternPool.zig b/src/InternPool.zig index 539cb441c5..98bde244c5 100644 --- a/src/InternPool.zig +++ b/src/InternPool.zig @@ -11169,9 +11169,9 @@ pub fn mutateVarInit(ip: *InternPool, io: Io, index: Index, init_index: Index) v pub fn dump(ip: *const InternPool, io: Io) Io.Cancelable!void { var buffer: [4096]u8 = undefined; - const stderr_writer = try io.lockStderrWriter(&buffer); - defer io.unlockStderrWriter(); - const w = &stderr_writer.interface; + const stderr = try io.lockStderr(&buffer, null); + defer io.unlockStderr(); + const w = &stderr.file_writer.interface; try dumpStatsFallible(ip, w, std.heap.page_allocator); try dumpAllFallible(ip, w); } @@ -11536,8 +11536,8 @@ fn dumpAllFallible(ip: *const InternPool, w: *Io.Writer) anyerror!void { pub fn dumpGenericInstances(ip: *const InternPool, io: Io, allocator: Allocator) Io.Cancelable!void { var buffer: [4096]u8 = undefined; - const stderr_writer = try io.lockStderrWriter(&buffer); - defer io.unlockStderrWriter(); + const stderr_writer = try io.lockStderr(&buffer, null); + defer io.unlockStderr(); const w = &stderr_writer.interface; try ip.dumpGenericInstancesFallible(allocator, w); } diff --git a/src/Zcu/PerThread.zig b/src/Zcu/PerThread.zig index 76ab3e229c..103cbaaaae 100644 --- a/src/Zcu/PerThread.zig +++ b/src/Zcu/PerThread.zig @@ -4560,10 +4560,10 @@ fn runCodegenInner(pt: Zcu.PerThread, func_index: InternPool.Index, air: *Air) e if (build_options.enable_debug_extensions and comp.verbose_air) p: { const io = comp.io; - const stderr = try io.lockStderrWriter(&.{}); - defer io.unlockStderrWriter(); - printVerboseAir(pt, liveness, fqn, air, &stderr.interface) catch |err| switch (err) { - error.WriteFailed => switch (stderr.err.?) { + const stderr = try io.lockStderr(&.{}, null); + defer io.unlockStderr(); + printVerboseAir(pt, liveness, fqn, air, &stderr.file_writer.interface) catch |err| switch (err) { + error.WriteFailed => switch (stderr.file_writer.err.?) { error.Canceled => |e| return e, else => break :p, }, diff --git a/src/codegen/aarch64/Select.zig b/src/codegen/aarch64/Select.zig index 138c70fecf..93a6e0a768 100644 --- a/src/codegen/aarch64/Select.zig +++ b/src/codegen/aarch64/Select.zig @@ -11274,16 +11274,15 @@ fn initValueAdvanced( } pub fn dumpValues(isel: *Select, which: enum { only_referenced, all }) void { const zcu = isel.pt.zcu; - const io = zcu.comp.io; const gpa = zcu.gpa; const ip = &zcu.intern_pool; const nav = ip.getNav(isel.nav_index); errdefer |err| @panic(@errorName(err)); - const stderr_writer = io.lockStderrWriter(&.{}) catch return; - defer io.unlockStderrWriter(); - const stderr = &stderr_writer.interface; + const locked_stderr = std.debug.lockStderr(&.{}, null); + defer std.debug.unlockStderr(); + const stderr = &locked_stderr.file_writer.interface; var reverse_live_values: std.AutoArrayHashMapUnmanaged(Value.Index, std.ArrayList(Air.Inst.Index)) = .empty; defer { diff --git a/src/crash_report.zig b/src/crash_report.zig index f23806b758..e56bc7cec5 100644 --- a/src/crash_report.zig +++ b/src/crash_report.zig @@ -95,9 +95,9 @@ fn dumpCrashContext() Io.Writer.Error!void { // TODO: this does mean that a different thread could grab the stderr mutex between the context // and the actual panic printing, which would be quite confusing. - const stderr = std.debug.lockStderrWriter(&.{}); - defer std.debug.unlockStderrWriter(); - const w = &stderr.interface; + const stderr = std.debug.lockStderr(&.{}); + defer std.debug.unlockStderr(); + const w = &stderr.file_writer.interface; try w.writeAll("Compiler crash context:\n"); diff --git a/src/libs/mingw.zig b/src/libs/mingw.zig index a76fec9237..8cf1dfa303 100644 --- a/src/libs/mingw.zig +++ b/src/libs/mingw.zig @@ -314,14 +314,14 @@ pub fn buildImportLib(comp: *Compilation, lib_name: []const u8) !void { if (comp.verbose_cc) { var buffer: [256]u8 = undefined; - const stderr = try io.lockStderrWriter(&buffer); - defer io.unlockStderrWriter(); - const w = &stderr.interface; + const stderr = try io.lockStderr(&buffer, null); + defer io.unlockStderr(); + const w = &stderr.file_writer.interface; w.print("def file: {s}\n", .{def_file_path}) catch |err| switch (err) { - error.WriteFailed => return stderr.err.?, + error.WriteFailed => return stderr.file_writer.err.?, }; w.print("include dir: {s}\n", .{include_dir}) catch |err| switch (err) { - error.WriteFailed => return stderr.err.?, + error.WriteFailed => return stderr.file_writer.err.?, }; } @@ -339,12 +339,12 @@ pub fn buildImportLib(comp: *Compilation, lib_name: []const u8) !void { if (aro_comp.diagnostics.output.to_list.messages.items.len != 0) { var buffer: [64]u8 = undefined; - const stderr = try io.lockStderrWriter(&buffer); - defer io.unlockStderrWriter(); + const stderr = try io.lockStderr(&buffer, null); + defer io.unlockStderr(); for (aro_comp.diagnostics.output.to_list.messages.items) |msg| { if (msg.kind == .@"fatal error" or msg.kind == .@"error") { - msg.write(&stderr.interface, stderr.mode, true) catch |err| switch (err) { - error.WriteFailed => return stderr.err.?, + msg.write(stderr.terminal(), true) catch |err| switch (err) { + error.WriteFailed => return stderr.file_writer.err.?, }; return error.AroPreprocessorFailed; } @@ -365,9 +365,9 @@ pub fn buildImportLib(comp: *Compilation, lib_name: []const u8) !void { error.OutOfMemory => |e| return e, error.ParseError => { var buffer: [64]u8 = undefined; - const stderr = try io.lockStderrWriter(&buffer); - defer io.unlockStderrWriter(); - const w = &stderr.interface; + const stderr = try io.lockStderr(&buffer, null); + defer io.unlockStderr(); + const w = &stderr.file_writer.interface; try w.writeAll("error: "); try def_diagnostics.writeMsg(w, input); try w.writeByte('\n'); diff --git a/src/libs/mingw/def.zig b/src/libs/mingw/def.zig index 9a105b6182..f1c112d16e 100644 --- a/src/libs/mingw/def.zig +++ b/src/libs/mingw/def.zig @@ -1039,9 +1039,9 @@ fn testParse( const module = parse(std.testing.allocator, source, machine_type, .mingw, &diagnostics) catch |err| switch (err) { error.OutOfMemory => |e| return e, error.ParseError => { - const stderr = try io.lockStderrWriter(&.{}); - defer io.unlockStderrWriter(); - const w = &stderr.interface; + const stderr = try io.lockStderr(&.{}, null); + defer io.unlockStderr(); + const w = &stderr.file_writer.interface; try diagnostics.writeMsg(w, source); try w.writeByte('\n'); return err; diff --git a/src/link/Coff.zig b/src/link/Coff.zig index d379669613..03b757f5b4 100644 --- a/src/link/Coff.zig +++ b/src/link/Coff.zig @@ -2382,9 +2382,9 @@ pub fn dump(coff: *Coff, tid: Zcu.PerThread.Id) Io.Cancelable!void { const comp = coff.base.comp; const io = comp.io; var buffer: [512]u8 = undefined; - const stderr = try io.lockStderrWriter(&buffer); - defer io.unlockStderrWriter(); - const w = &stderr.interface; + const stderr = try io.lockStderr(&buffer, null); + defer io.unlockStderr(); + const w = &stderr.file_writer.interface; coff.printNode(tid, w, .root, 0) catch |err| switch (err) { error.WriteFailed => return stderr.err.?, }; diff --git a/src/link/Elf2.zig b/src/link/Elf2.zig index 3c511f1ee9..bbdb439385 100644 --- a/src/link/Elf2.zig +++ b/src/link/Elf2.zig @@ -3733,9 +3733,9 @@ pub fn dump(elf: *Elf, tid: Zcu.PerThread.Id) Io.Cancelable!void { const comp = elf.base.comp; const io = comp.io; var buffer: [512]u8 = undefined; - const stderr = try io.lockStderrWriter(&buffer); - defer io.unlockStderrWriter(); - const w = &stderr.interface; + const stderr = try io.lockStderr(&buffer, null); + defer io.lockStderr(); + const w = &stderr.file_writer.interface; elf.printNode(tid, w, .root, 0) catch |err| switch (err) { error.WriteFailed => return stderr.err.?, }; diff --git a/src/main.zig b/src/main.zig index c5ed278921..8e30febb81 100644 --- a/src/main.zig +++ b/src/main.zig @@ -4429,9 +4429,9 @@ fn runOrTest( // the error message and invocation below. if (process.can_execv and arg_mode == .run) { // execv releases the locks; no need to destroy the Compilation here. - _ = try io.lockStderrWriter(&.{}); + _ = try io.lockStderr(&.{}, .no_color); const err = process.execve(gpa, argv.items, &env_map); - io.unlockStderrWriter(); + io.unlockStderr(); try warnAboutForeignBinaries(io, arena, arg_mode, target, link_libc); const cmd = try std.mem.join(arena, " ", argv.items); fatal("the following command failed to execve with '{t}':\n{s}", .{ err, cmd }); @@ -4448,8 +4448,8 @@ fn runOrTest( comp_destroyed.* = true; const term_result = t: { - _ = try io.lockStderrWriter(&.{}); - defer io.unlockStderrWriter(); + _ = try io.lockStderr(&.{}, .no_color); + defer io.unlockStderr(); break :t child.spawnAndWait(io); }; const term = term_result catch |err| { @@ -5418,8 +5418,8 @@ fn cmdBuild(gpa: Allocator, arena: Allocator, io: Io, args: []const []const u8) child.stderr_behavior = .Inherit; const term = t: { - _ = try io.lockStderrWriter(&.{}); - defer io.unlockStderrWriter(); + _ = try io.lockStderr(&.{}, .no_color); + defer io.unlockStderr(); break :t child.spawnAndWait(io) catch |err| fatal("failed to spawn build runner {s}: {t}", .{ child_argv.items[0], err }); }; -- cgit v1.2.3 From 406950f7566f7f220847ab2faa2c26914dcda257 Mon Sep 17 00:00:00 2001 From: Ryan Liptak Date: Fri, 19 Dec 2025 03:11:15 -0800 Subject: std.process: Fix executableDirPath functions --- lib/std/process.zig | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) (limited to 'lib/std/process.zig') diff --git a/lib/std/process.zig b/lib/std/process.zig index 23a3d92e9b..d6d671e251 100644 --- a/lib/std/process.zig +++ b/lib/std/process.zig @@ -2193,17 +2193,21 @@ pub fn executablePath(io: Io, out_buffer: []u8) ExecutablePathError!usize { /// /// On Windows, the result is encoded as [WTF-8](https://wtf-8.codeberg.page/). /// On other platforms, the result is an opaque sequence of bytes with no particular encoding. -pub fn executableDirPath(out_buffer: []u8) ExecutablePathError!usize { - const n = try executablePath(out_buffer); +pub fn executableDirPath(io: Io, out_buffer: []u8) ExecutablePathError!usize { + const n = try executablePath(io, out_buffer); // Assert that the OS APIs return absolute paths, and therefore dirname // will not return null. - return std.fs.path.dirname(out_buffer[0..n]).?; + return std.fs.path.dirname(out_buffer[0..n]).?.len; } /// Same as `executableDirPath` except allocates the result. -pub fn executableDirPathAlloc(allocator: Allocator) ![]u8 { +pub fn executableDirPathAlloc(io: Io, allocator: Allocator) ExecutablePathAllocError![]u8 { var buffer: [max_path_bytes]u8 = undefined; - return allocator.dupe(u8, try executableDirPath(&buffer)); + const dir_path_len = executableDirPath(io, &buffer) catch |err| switch (err) { + error.NameTooLong => unreachable, + else => |e| return e, + }; + return allocator.dupe(u8, buffer[0..dir_path_len]); } pub const OpenExecutableError = File.OpenError || ExecutablePathError || File.LockError; -- cgit v1.2.3 From 405db921dc0a053d7bebca6195c5a0031a5b187b Mon Sep 17 00:00:00 2001 From: Andrew Kelley Date: Fri, 19 Dec 2025 12:55:00 -0800 Subject: std: fix compilation targeting WASI --- lib/std/Io/File.zig | 2 +- lib/std/Io/Threaded.zig | 99 +++++++++++++++++++++++++++++++++++-------------- lib/std/Progress.zig | 4 +- lib/std/c.zig | 5 ++- lib/std/process.zig | 4 +- 5 files changed, 81 insertions(+), 33 deletions(-) (limited to 'lib/std/process.zig') diff --git a/lib/std/Io/File.zig b/lib/std/Io/File.zig index dc64a4d2c3..ae6c69285f 100644 --- a/lib/std/Io/File.zig +++ b/lib/std/Io/File.zig @@ -429,7 +429,7 @@ pub const Permissions = std.options.FilePermissions orelse if (is_windows) enum( executable_file = 0o777, _, - pub const has_executable_bit = true; + pub const has_executable_bit = native_os != .wasi; pub fn toMode(self: @This()) std.posix.mode_t { return @intFromEnum(self); diff --git a/lib/std/Io/Threaded.zig b/lib/std/Io/Threaded.zig index 424869f3d4..37136879a3 100644 --- a/lib/std/Io/Threaded.zig +++ b/lib/std/Io/Threaded.zig @@ -947,6 +947,16 @@ const have_fchmodat2 = native_os == .linux and const have_fchmodat_flags = native_os != .linux or (!builtin.abi.isAndroid() and std.c.versionCheck(.{ .major = 2, .minor = 32, .patch = 0 })); +const have_fchown = switch (native_os) { + .wasi, .windows => false, + else => true, +}; + +const have_fchmod = switch (native_os) { + .wasi, .windows => false, + else => true, +}; + const openat_sym = if (posix.lfs64_abi) posix.system.openat64 else posix.system.openat; const fstat_sym = if (posix.lfs64_abi) posix.system.fstat64 else posix.system.fstat; const fstatat_sym = if (posix.lfs64_abi) posix.system.fstatat64 else posix.system.fstatat; @@ -1480,6 +1490,7 @@ fn futexWaitUncancelable(userdata: ?*anyopaque, ptr: *const u32, expected: u32) } fn futexWake(userdata: ?*anyopaque, ptr: *const u32, max_waiters: u32) void { + if (builtin.single_threaded) unreachable; // Nothing to wake up. const t: *Threaded = @ptrCast(@alignCast(userdata)); _ = t; switch (native_os) { @@ -4190,7 +4201,6 @@ fn dirDeleteFileWasi(userdata: ?*anyopaque, dir: Dir, sub_path: []const u8) Dir. .NOTDIR => return error.NotDir, .NOMEM => return error.SystemResources, .ROFS => return error.ReadOnlyFileSystem, - .NOTEMPTY => return error.DirNotEmpty, .NOTCAPABLE => return error.AccessDenied, .ILSEQ => return error.BadPathName, .INVAL => |err| return errnoBug(err), // invalid flags, or pathname has . as last component @@ -4464,7 +4474,6 @@ fn dirDeleteDirWasi(userdata: ?*anyopaque, dir: Dir, sub_path: []const u8) Dir.D .BUSY => return error.FileBusy, .FAULT => |err| return errnoBug(err), .IO => return error.FileSystem, - .ISDIR => return error.IsDir, .LOOP => return error.SymLinkLoop, .NAMETOOLONG => return error.NameTooLong, .NOENT => return error.FileNotFound, @@ -4877,14 +4886,14 @@ fn dirSymLinkWindows( @memcpy(buffer[@sizeOf(SYMLINK_DATA)..][0 .. final_target_path.len * 2], @as([*]const u8, @ptrCast(final_target_path))); const paths_start = @sizeOf(SYMLINK_DATA) + final_target_path.len * 2; @memcpy(buffer[paths_start..][0 .. final_target_path.len * 2], @as([*]const u8, @ptrCast(final_target_path))); - _ = w.DeviceIoControl(symlink_handle, w.FSCTL.SET_REPARSE_POINT, .{ .in = buffer[0..buf_len] }) catch |err| switch (err) { - error.PipeClosing => unreachable, - error.PipeAlreadyConnected => unreachable, - error.PipeAlreadyListening => unreachable, - error.Pending => unreachable, - error.UnrecognizedVolume => unreachable, - else => |e| return e, - }; + const rc = w.DeviceIoControl(symlink_handle, w.FSCTL.SET_REPARSE_POINT, .{ .in = buffer[0..buf_len] }); + switch (rc) { + .SUCCESS => {}, + .PRIVILEGE_NOT_HELD => return error.PermissionDenied, + .ACCESS_DENIED => return error.AccessDenied, + .INVALID_DEVICE_REQUEST => return error.FileSystem, + else => return windows.unexpectedStatus(rc), + } } fn dirSymLinkWasi( @@ -4894,7 +4903,7 @@ fn dirSymLinkWasi( sym_link_path: []const u8, flags: Dir.SymLinkFlags, ) Dir.SymLinkError!void { - if (builtin.link_libc) return dirSymLinkPosix(dir, target_path, sym_link_path, flags); + if (builtin.link_libc) return dirSymLinkPosix(userdata, dir, target_path, sym_link_path, flags); const t: *Threaded = @ptrCast(@alignCast(userdata)); const current_thread = Thread.getCurrent(t); @@ -5021,7 +5030,7 @@ fn dirReadLinkWasi(userdata: ?*anyopaque, dir: Dir, sub_path: []const u8, buffer switch (std.os.wasi.path_readlink(dir.handle, sub_path.ptr, sub_path.len, buffer.ptr, buffer.len, &n)) { .SUCCESS => { current_thread.endSyscall(); - return buffer[0..n]; + return n; }, .CANCELED => return current_thread.endSyscallCanceled(), .INTR => { @@ -5104,6 +5113,7 @@ fn dirSetPermissionsWindows(userdata: ?*anyopaque, dir: Dir, permissions: Dir.Pe } fn dirSetPermissionsPosix(userdata: ?*anyopaque, dir: Dir, permissions: Dir.Permissions) Dir.SetPermissionsError!void { + if (@sizeOf(Dir.Permissions) == 0) return; const t: *Threaded = @ptrCast(@alignCast(userdata)); const current_thread = Thread.getCurrent(t); return setPermissionsPosix(current_thread, dir.handle, permissions.toMode()); @@ -5116,7 +5126,7 @@ fn dirSetFilePermissions( permissions: Dir.Permissions, options: Dir.SetFilePermissionsOptions, ) Dir.SetFilePermissionsError!void { - if (!Dir.Permissions.has_executable_bit) return error.Unexpected; + if (@sizeOf(Dir.Permissions) == 0) return; if (is_windows) @panic("TODO"); const t: *Threaded = @ptrCast(@alignCast(userdata)); const current_thread = Thread.getCurrent(t); @@ -5250,14 +5260,16 @@ fn dirSetOwnerUnsupported(userdata: ?*anyopaque, dir: Dir, owner: ?File.Uid, gro } fn dirSetOwnerPosix(userdata: ?*anyopaque, dir: Dir, owner: ?File.Uid, group: ?File.Gid) Dir.SetOwnerError!void { + if (!have_fchown) return error.Unexpected; // Unsupported OS, don't call this function. const t: *Threaded = @ptrCast(@alignCast(userdata)); const current_thread = Thread.getCurrent(t); const uid = owner orelse ~@as(posix.uid_t, 0); const gid = group orelse ~@as(posix.gid_t, 0); - return setOwnerPosix(current_thread, dir.handle, uid, gid); + return posixFchown(current_thread, dir.handle, uid, gid); } -fn setOwnerPosix(current_thread: *Thread, fd: posix.fd_t, uid: posix.uid_t, gid: posix.gid_t) File.SetOwnerError!void { +fn posixFchown(current_thread: *Thread, fd: posix.fd_t, uid: posix.uid_t, gid: posix.gid_t) File.SetOwnerError!void { + comptime assert(have_fchown); try current_thread.beginSyscall(); while (true) { switch (posix.errno(posix.system.fchown(fd, uid, gid))) { @@ -5296,13 +5308,10 @@ fn dirSetFileOwner( group: ?File.Gid, options: Dir.SetFileOwnerOptions, ) Dir.SetFileOwnerError!void { + if (!have_fchown) return error.Unexpected; // Unsupported OS, don't call this function. const t: *Threaded = @ptrCast(@alignCast(userdata)); const current_thread = Thread.getCurrent(t); - if (is_windows) { - @panic("TODO"); - } - var path_buffer: [posix.PATH_MAX]u8 = undefined; const sub_path_posix = try pathToPosix(sub_path, &path_buffer); @@ -5317,6 +5326,7 @@ fn dirSetFileOwner( const fileSync = switch (native_os) { .windows => fileSyncWindows, + .wasi => fileSyncWasi, else => fileSyncPosix, }; @@ -5366,6 +5376,34 @@ fn fileSyncPosix(userdata: ?*anyopaque, file: File) File.SyncError!void { } } +fn fileSyncWasi(userdata: ?*anyopaque, file: File) File.SyncError!void { + const t: *Threaded = @ptrCast(@alignCast(userdata)); + const current_thread = Thread.getCurrent(t); + try current_thread.beginSyscall(); + while (true) { + switch (std.os.wasi.fd_sync(file.handle)) { + .SUCCESS => return current_thread.endSyscall(), + .CANCELED => return current_thread.endSyscallCanceled(), + .INTR => { + try current_thread.checkCancel(); + continue; + }, + else => |e| { + current_thread.endSyscall(); + switch (e) { + .BADF => |err| return errnoBug(err), + .INVAL => |err| return errnoBug(err), + .ROFS => |err| return errnoBug(err), + .IO => return error.InputOutput, + .NOSPC => return error.NoSpaceLeft, + .DQUOT => return error.DiskQuota, + else => |err| return posix.unexpectedErrno(err), + } + }, + } + } +} + fn fileIsTty(userdata: ?*anyopaque, file: File) Io.Cancelable!bool { const t: *Threaded = @ptrCast(@alignCast(userdata)); const current_thread = Thread.getCurrent(t); @@ -5659,18 +5697,16 @@ fn fileSetLength(userdata: ?*anyopaque, file: File, length: u64) File.SetLengthE } fn fileSetOwner(userdata: ?*anyopaque, file: File, owner: ?File.Uid, group: ?File.Gid) File.SetOwnerError!void { - switch (native_os) { - .windows, .wasi => return error.Unexpected, - else => {}, - } + if (!have_fchown) return error.Unexpected; // Unsupported OS, don't call this function. const t: *Threaded = @ptrCast(@alignCast(userdata)); const current_thread = Thread.getCurrent(t); const uid = owner orelse ~@as(posix.uid_t, 0); const gid = group orelse ~@as(posix.gid_t, 0); - return setOwnerPosix(current_thread, file.handle, uid, gid); + return posixFchown(current_thread, file.handle, uid, gid); } fn fileSetPermissions(userdata: ?*anyopaque, file: File, permissions: File.Permissions) File.SetPermissionsError!void { + if (@sizeOf(File.Permissions) == 0) return; const t: *Threaded = @ptrCast(@alignCast(userdata)); const current_thread = Thread.getCurrent(t); switch (native_os) { @@ -5704,6 +5740,7 @@ fn fileSetPermissions(userdata: ?*anyopaque, file: File, permissions: File.Permi } fn setPermissionsPosix(current_thread: *Thread, fd: posix.fd_t, mode: posix.mode_t) File.SetPermissionsError!void { + comptime assert(have_fchmod); try current_thread.beginSyscall(); while (true) { switch (posix.errno(posix.system.fchmod(fd, mode))) { @@ -5987,6 +6024,7 @@ const windows_lock_range_off: windows.LARGE_INTEGER = 0; const windows_lock_range_len: windows.LARGE_INTEGER = 1; fn fileLock(userdata: ?*anyopaque, file: File, lock: File.Lock) File.LockError!void { + if (native_os == .wasi) return error.FileLocksUnsupported; const t: *Threaded = @ptrCast(@alignCast(userdata)); const current_thread = Thread.getCurrent(t); @@ -6066,6 +6104,7 @@ fn fileLock(userdata: ?*anyopaque, file: File, lock: File.Lock) File.LockError!v } fn fileTryLock(userdata: ?*anyopaque, file: File, lock: File.Lock) File.LockError!bool { + if (native_os == .wasi) return error.FileLocksUnsupported; const t: *Threaded = @ptrCast(@alignCast(userdata)); const current_thread = Thread.getCurrent(t); @@ -6150,6 +6189,7 @@ fn fileTryLock(userdata: ?*anyopaque, file: File, lock: File.Lock) File.LockErro } fn fileUnlock(userdata: ?*anyopaque, file: File) void { + if (native_os == .wasi) return; const t: *Threaded = @ptrCast(@alignCast(userdata)); _ = t; @@ -6186,6 +6226,7 @@ fn fileUnlock(userdata: ?*anyopaque, file: File) void { } fn fileDowngradeLock(userdata: ?*anyopaque, file: File) File.DowngradeLockError!void { + if (native_os == .wasi) return; const t: *Threaded = @ptrCast(@alignCast(userdata)); const current_thread = Thread.getCurrent(t); @@ -7072,7 +7113,7 @@ fn processExecutablePath(userdata: ?*anyopaque, out_buffer: []u8) std.process.Ex const end_index = std.unicode.wtf16LeToWtf8(out_buffer, wide_slice); return end_index; }, - else => @compileError("unsupported OS"), + else => return error.OperationUnsupported, } } @@ -10034,7 +10075,10 @@ fn netWriteUnavailable( /// This is either usize or u32. Since, either is fine, let's use the same /// `addBuf` function for both writing to a file and sending network messages. -const iovlen_t = @FieldType(posix.msghdr_const, "iovlen"); +const iovlen_t = switch (native_os) { + .wasi => u32, + else => @FieldType(posix.msghdr_const, "iovlen"), +}; fn addBuf(v: []posix.iovec_const, i: *iovlen_t, bytes: []const u8) void { // OS checks ptr addr before length so zero length vectors must be omitted. @@ -10775,8 +10819,9 @@ fn statFromPosix(st: *const posix.Stat) File.Stat { fn statFromWasi(st: *const std.os.wasi.filestat_t) File.Stat { return .{ .inode = st.ino, + .nlink = st.nlink, .size = @bitCast(st.size), - .mode = 0, + .permissions = .default_file, .kind = switch (st.filetype) { .BLOCK_DEVICE => .block_device, .CHARACTER_DEVICE => .character_device, diff --git a/lib/std/Progress.zig b/lib/std/Progress.zig index 8bd120dbd1..970cf90382 100644 --- a/lib/std/Progress.zig +++ b/lib/std/Progress.zig @@ -498,8 +498,8 @@ pub fn start(io: Io, options: Options) Node { if (stderr.enableAnsiEscapeCodes(io)) |_| { global_progress.terminal_mode = .ansi_escape_codes; } else |_| if (is_windows) { - if (stderr.isTty(io)) { - global_progress.terminal_mode = TerminalMode{ .windows_api = .{ + if (stderr.isTty(io)) |is_tty| { + if (is_tty) global_progress.terminal_mode = TerminalMode{ .windows_api = .{ .code_page = windows.kernel32.GetConsoleOutputCP(), } }; } else |err| switch (err) { diff --git a/lib/std/c.zig b/lib/std/c.zig index 0627882e3c..703dda810e 100644 --- a/lib/std/c.zig +++ b/lib/std/c.zig @@ -148,9 +148,10 @@ pub const dev_t = switch (native_os) { pub const mode_t = switch (native_os) { .linux => linux.mode_t, .emscripten => emscripten.mode_t, - .openbsd, .haiku, .netbsd, .illumos, .wasi, .windows => u32, + .openbsd, .haiku, .netbsd, .illumos, .windows => u32, // https://github.com/SerenityOS/serenity/blob/b98f537f117b341788023ab82e0c11ca9ae29a57/Kernel/API/POSIX/sys/types.h#L44 .freebsd, .driverkit, .ios, .maccatalyst, .macos, .tvos, .visionos, .watchos, .dragonfly, .serenity => u16, + .wasi => if (builtin.link_libc) u32 else u0, // WASI libc emulates mode. else => u0, }; @@ -10608,7 +10609,7 @@ pub extern "c" fn munmap(addr: *align(page_size) const anyopaque, len: usize) c_ pub extern "c" fn mremap(addr: ?*align(page_size) const anyopaque, old_len: usize, new_len: usize, flags: MREMAP, ...) *anyopaque; pub extern "c" fn mprotect(addr: *align(page_size) anyopaque, len: usize, prot: c_uint) c_int; pub extern "c" fn link(oldpath: [*:0]const u8, newpath: [*:0]const u8) c_int; -pub extern "c" fn linkat(oldfd: fd_t, oldpath: [*:0]const u8, newfd: fd_t, newpath: [*:0]const u8, flags: c_int) c_int; +pub extern "c" fn linkat(oldfd: fd_t, oldpath: [*:0]const u8, newfd: fd_t, newpath: [*:0]const u8, flags: c_uint) c_int; pub extern "c" fn unlink(path: [*:0]const u8) c_int; pub extern "c" fn unlinkat(dirfd: fd_t, path: [*:0]const u8, flags: c_uint) c_int; pub extern "c" fn getcwd(buf: [*]u8, size: usize) ?[*]u8; diff --git a/lib/std/process.zig b/lib/std/process.zig index d6d671e251..07d2b7117f 100644 --- a/lib/std/process.zig +++ b/lib/std/process.zig @@ -2121,7 +2121,9 @@ pub fn fatal(comptime format: []const u8, format_arguments: anytype) noreturn { pub const ExecutablePathBaseError = error{ FileNotFound, AccessDenied, - NotSupported, + /// The operating system does not support an executable learning its own + /// path. + OperationUnsupported, NotDir, SymLinkLoop, InputOutput, -- cgit v1.2.3 From 3e624e17a40fd65b68dbec45030280008c6b4018 Mon Sep 17 00:00:00 2001 From: Andrew Kelley Date: Fri, 19 Dec 2025 14:14:21 -0800 Subject: std: fix compilation errors on FreeBSD --- lib/std/Io/File/Writer.zig | 2 ++ lib/std/Io/Threaded.zig | 45 +++++++++++++++++++++++++++++++++------------ lib/std/c.zig | 1 + lib/std/process.zig | 5 ++++- 4 files changed, 40 insertions(+), 13 deletions(-) (limited to 'lib/std/process.zig') diff --git a/lib/std/Io/File/Writer.zig b/lib/std/Io/File/Writer.zig index 3487416719..226b60993f 100644 --- a/lib/std/Io/File/Writer.zig +++ b/lib/std/Io/File/Writer.zig @@ -49,6 +49,8 @@ pub const Error = error{ pub const WriteFileError = Error || error{ /// Descriptor is not valid or locked, or an mmap(2)-like operation is not available for in_fd. Unimplemented, + /// Can happen on FreeBSD when using copy_file_range. + CorruptedData, EndOfStream, ReadFailed, }; diff --git a/lib/std/Io/Threaded.zig b/lib/std/Io/Threaded.zig index 37136879a3..7dd9ef5100 100644 --- a/lib/std/Io/Threaded.zig +++ b/lib/std/Io/Threaded.zig @@ -366,12 +366,12 @@ const Thread = struct { fn futexWake(ptr: *const u32, max_waiters: u32) void { @branchHint(.cold); + assert(max_waiters != 0); if (builtin.single_threaded) return; // nothing to wake up if (builtin.cpu.arch.isWasm()) { comptime assert(builtin.cpu.has(.wasm, .atomics)); - assert(max_waiters != 0); const woken_count = asm volatile ( \\local.get %[ptr] \\local.get %[waiters] @@ -416,7 +416,6 @@ const Thread = struct { } }, .windows => { - assert(max_waiters != 0); switch (max_waiters) { 1 => windows.ntdll.RtlWakeAddressSingle(ptr), else => windows.ntdll.RtlWakeAddressAll(ptr), @@ -953,7 +952,8 @@ const have_fchown = switch (native_os) { }; const have_fchmod = switch (native_os) { - .wasi, .windows => false, + .windows => false, + .wasi => builtin.link_libc, else => true, }; @@ -6772,7 +6772,7 @@ fn fileSeekBy(userdata: ?*anyopaque, file: File, offset: i64) File.SeekError!voi var result: u64 = undefined; try current_thread.beginSyscall(); while (true) { - switch (posix.errno(posix.system.llseek(fd, offset, &result, posix.SEEK.CUR))) { + switch (posix.errno(posix.system.llseek(fd, @bitCast(offset), &result, posix.SEEK.CUR))) { .SUCCESS => { current_thread.endSyscall(); return; @@ -7000,7 +7000,7 @@ fn processExecutablePath(userdata: ?*anyopaque, out_buffer: []u8) std.process.Ex if (rc != 0) return error.NameTooLong; var real_path_buf: [posix.PATH_MAX]u8 = undefined; - const n = Io.Dir.realPathAbsolute(ioBasic(t), &symlink_path_buf, &real_path_buf) catch |err| switch (err) { + const n = Io.Dir.realPathFileAbsolute(ioBasic(t), &symlink_path_buf, &real_path_buf) catch |err| switch (err) { error.NetworkNotFound => unreachable, // Windows-only else => |e| return e, }; @@ -7020,11 +7020,32 @@ fn processExecutablePath(userdata: ?*anyopaque, out_buffer: []u8) std.process.Ex }, .freebsd, .dragonfly => { const current_thread = Thread.getCurrent(t); - try current_thread.checkCancel(); var mib: [4]c_int = .{ posix.CTL.KERN, posix.KERN.PROC, posix.KERN.PROC_PATHNAME, -1 }; var out_len: usize = out_buffer.len; - try posix.sysctl(&mib, out_buffer.ptr, &out_len, null, 0); - return out_len; + try current_thread.beginSyscall(); + while (true) { + switch (posix.errno(posix.system.sysctl(&mib, mib.len, out_buffer.ptr, &out_len, null, 0))) { + .SUCCESS => { + current_thread.endSyscall(); + return out_len; + }, + .INTR => { + try current_thread.checkCancel(); + continue; + }, + .CANCELED => return current_thread.endSyscallCanceled(), + else => |e| { + current_thread.endSyscall(); + switch (e) { + .FAULT => |err| return errnoBug(err), + .PERM => return error.PermissionDenied, + .NOMEM => return error.SystemResources, + .NOENT => |err| return errnoBug(err), + else => |err| return posix.unexpectedErrno(err), + } + }, + } + } }, .netbsd => { const current_thread = Thread.getCurrent(t); @@ -7043,7 +7064,7 @@ fn processExecutablePath(userdata: ?*anyopaque, out_buffer: []u8) std.process.Ex if (std.mem.indexOf(u8, argv0, "/") != null) { // argv[0] is a path (relative or absolute): use realpath(3) directly var real_path_buf: [posix.PATH_MAX]u8 = undefined; - const real_path = Io.Dir.realPathAbsolute(ioBasic(t), std.os.argv[0], &real_path_buf) catch |err| switch (err) { + const real_path = Io.Dir.realPathFileAbsolute(ioBasic(t), std.os.argv[0], &real_path_buf) catch |err| switch (err) { error.NetworkNotFound => unreachable, // Windows-only else => |e| return e, }; @@ -7063,7 +7084,7 @@ fn processExecutablePath(userdata: ?*anyopaque, out_buffer: []u8) std.process.Ex }, 0) catch continue; var real_path_buf: [posix.PATH_MAX]u8 = undefined; - if (Io.Dir.realPathAbsolute(ioBasic(t), resolved_path, &real_path_buf)) |real_path| { + if (Io.Dir.realPathFileAbsolute(ioBasic(t), resolved_path, &real_path_buf)) |real_path| { // found a file, and hope it is the right file if (real_path.len > out_buffer.len) return error.NameTooLong; @@ -7730,8 +7751,8 @@ fn fileWriteFileStreaming( .FBIG => return error.FileTooBig, .IO => return error.InputOutput, .INTEGRITY => return error.CorruptedData, - .ISDIR => return error.IsDir, .NOSPC => return error.NoSpaceLeft, + .ISDIR => |err| errnoBug(err), .BADF => |err| errnoBug(err), else => |err| posix.unexpectedErrno(err), }); @@ -7908,11 +7929,11 @@ fn fileWriteFilePositional( .FBIG => return error.FileTooBig, .IO => return error.InputOutput, .INTEGRITY => return error.CorruptedData, - .ISDIR => return error.IsDir, .NOSPC => return error.NoSpaceLeft, .OVERFLOW => return error.Unseekable, .NXIO => return error.Unseekable, .SPIPE => return error.Unseekable, + .ISDIR => |err| errnoBug(err), .BADF => |err| errnoBug(err), else => |err| posix.unexpectedErrno(err), }); diff --git a/lib/std/c.zig b/lib/std/c.zig index 703dda810e..25e52bac0c 100644 --- a/lib/std/c.zig +++ b/lib/std/c.zig @@ -163,6 +163,7 @@ pub const nlink_t = switch (native_os) { .freebsd, .serenity => u64, .openbsd, .netbsd, .illumos => u32, .haiku => i32, + .driverkit, .ios, .maccatalyst, .macos, .tvos, .visionos, .watchos => u16, else => u0, }; diff --git a/lib/std/process.zig b/lib/std/process.zig index 07d2b7117f..cf751f7a28 100644 --- a/lib/std/process.zig +++ b/lib/std/process.zig @@ -2287,7 +2287,10 @@ pub fn exit(status: u8) noreturn { } else switch (native_os) { .windows => windows.ntdll.RtlExitUserProcess(status), .wasi => std.os.wasi.proc_exit(status), - .linux => if (!builtin.single_threaded) std.os.linux.exit_group(status), + .linux => { + if (!builtin.single_threaded) std.os.linux.exit_group(status); + posix.system.exit(status); + }, .uefi => { const uefi = std.os.uefi; // exit() is only available if exitBootServices() has not been called yet. -- cgit v1.2.3 From fa79d346744308250c165519f842fedfc26a1c14 Mon Sep 17 00:00:00 2001 From: Andrew Kelley Date: Tue, 23 Dec 2025 22:07:31 -0800 Subject: std: add changing cur dir back There's a good argument to not have this in the std lib but it's more work to remove it than to leave it in, and this branch is already 20,000+ lines changed. --- lib/std/Io.zig | 1 + lib/std/Io/Threaded.zig | 74 ++++++++++++++++++++++++- lib/std/os/windows.zig | 34 ------------ lib/std/posix.zig | 21 ++----- lib/std/process.zig | 25 +++++++++ lib/std/testing.zig | 1 + test/standalone/posix/cwd.zig | 72 +++++++++++++++++++----- test/standalone/posix/relpaths.zig | 91 +++++++++++++++++++------------ test/standalone/windows_bat_args/fuzz.zig | 4 +- test/standalone/windows_bat_args/test.zig | 4 +- test/standalone/windows_spawn/main.zig | 6 +- tools/fetch_them_macos_headers.zig | 12 ++-- 12 files changed, 229 insertions(+), 116 deletions(-) (limited to 'lib/std/process.zig') diff --git a/lib/std/Io.zig b/lib/std/Io.zig index 70b6c5bcc3..162fedca5f 100644 --- a/lib/std/Io.zig +++ b/lib/std/Io.zig @@ -718,6 +718,7 @@ pub const VTable = struct { lockStderr: *const fn (?*anyopaque, buffer: []u8, ?Terminal.Mode) Cancelable!LockedStderr, tryLockStderr: *const fn (?*anyopaque, buffer: []u8, ?Terminal.Mode) Cancelable!?LockedStderr, unlockStderr: *const fn (?*anyopaque) void, + processSetCurrentDir: *const fn (?*anyopaque, Dir) std.process.SetCurrentDirError!void, now: *const fn (?*anyopaque, Clock) Clock.Error!Timestamp, sleep: *const fn (?*anyopaque, Timeout) SleepError!void, diff --git a/lib/std/Io/Threaded.zig b/lib/std/Io/Threaded.zig index ddabccf00d..8d383d1c01 100644 --- a/lib/std/Io/Threaded.zig +++ b/lib/std/Io/Threaded.zig @@ -648,7 +648,7 @@ pub fn init( .main_thread = .{ .signal_id = Thread.currentSignalId(), .current_closure = null, - .cancel_protection = undefined, + .cancel_protection = .unblocked, }, .argv0 = options.argv0, .environ = options.environ, @@ -689,7 +689,7 @@ pub const init_single_threaded: Threaded = .{ .main_thread = .{ .signal_id = undefined, .current_closure = null, - .cancel_protection = undefined, + .cancel_protection = .unblocked, }, .robust_cancel = .disabled, .argv0 = .{}, @@ -742,7 +742,7 @@ fn worker(t: *Threaded) void { var thread: Thread = .{ .signal_id = Thread.currentSignalId(), .current_closure = null, - .cancel_protection = undefined, + .cancel_protection = .unblocked, }; Thread.current = &thread; @@ -844,6 +844,7 @@ pub fn io(t: *Threaded) Io { .lockStderr = lockStderr, .tryLockStderr = tryLockStderr, .unlockStderr = unlockStderr, + .processSetCurrentDir = processSetCurrentDir, .now = now, .sleep = sleep, @@ -979,6 +980,7 @@ pub fn ioBasic(t: *Threaded) Io { .lockStderr = lockStderr, .tryLockStderr = tryLockStderr, .unlockStderr = unlockStderr, + .processSetCurrentDir = processSetCurrentDir, .now = now, .sleep = sleep, @@ -7370,6 +7372,7 @@ fn processExecutablePath(userdata: ?*anyopaque, out_buffer: []u8) std.process.Ex }; defer w.CloseHandle(h_file); + // TODO move GetFinalPathNameByHandle logic into std.Io.Threaded and add cancel checks const wide_slice = try w.GetFinalPathNameByHandle(h_file, .{}, &path_name_w_buf.data); const len = std.unicode.calcWtf8Len(wide_slice); @@ -10796,6 +10799,71 @@ fn unlockStderr(userdata: ?*anyopaque) void { std.process.stderr_thread_mutex.unlock(); } +fn processSetCurrentDir(userdata: ?*anyopaque, dir: Dir) std.process.SetCurrentDirError!void { + if (native_os == .wasi) return error.OperationUnsupported; + const t: *Threaded = @ptrCast(@alignCast(userdata)); + const current_thread = Thread.getCurrent(t); + + if (is_windows) { + try current_thread.checkCancel(); + var dir_path_buffer: [windows.PATH_MAX_WIDE]u16 = undefined; + // TODO move GetFinalPathNameByHandle logic into std.Io.Threaded and add cancel checks + const dir_path = try windows.GetFinalPathNameByHandle(dir.handle, .{}, &dir_path_buffer); + const path_len_bytes = std.math.cast(u16, dir_path.len * 2) orelse return error.NameTooLong; + try current_thread.checkCancel(); + var nt_name: windows.UNICODE_STRING = .{ + .Length = path_len_bytes, + .MaximumLength = path_len_bytes, + .Buffer = @constCast(dir_path.ptr), + }; + switch (windows.ntdll.RtlSetCurrentDirectory_U(&nt_name)) { + .SUCCESS => return, + .OBJECT_NAME_INVALID => return error.BadPathName, + .OBJECT_NAME_NOT_FOUND => return error.FileNotFound, + .OBJECT_PATH_NOT_FOUND => return error.FileNotFound, + .NO_MEDIA_IN_DEVICE => return error.NoDevice, + .INVALID_PARAMETER => |err| return windows.statusBug(err), + .ACCESS_DENIED => return error.AccessDenied, + .OBJECT_PATH_SYNTAX_BAD => |err| return windows.statusBug(err), + .NOT_A_DIRECTORY => return error.NotDir, + else => |status| return windows.unexpectedStatus(status), + } + } + + if (dir.handle == posix.AT.FDCWD) return; + + try current_thread.beginSyscall(); + while (true) { + switch (posix.errno(posix.system.fchdir(dir.handle))) { + .SUCCESS => return current_thread.endSyscall(), + .INTR => { + try current_thread.checkCancel(); + continue; + }, + .ACCES => { + current_thread.endSyscall(); + return error.AccessDenied; + }, + .BADF => |err| { + current_thread.endSyscall(); + return errnoBug(err); + }, + .NOTDIR => { + current_thread.endSyscall(); + return error.NotDir; + }, + .IO => { + current_thread.endSyscall(); + return error.FileSystem; + }, + else => |err| { + current_thread.endSyscall(); + return posix.unexpectedErrno(err); + }, + } + } +} + pub const PosixAddress = extern union { any: posix.sockaddr, in: posix.sockaddr.in, diff --git a/lib/std/os/windows.zig b/lib/std/os/windows.zig index 3ea48585ff..12ec0427a9 100644 --- a/lib/std/os/windows.zig +++ b/lib/std/os/windows.zig @@ -2939,40 +2939,6 @@ pub fn WriteFile( return bytes_written; } -pub const SetCurrentDirectoryError = error{ - NameTooLong, - FileNotFound, - NotDir, - AccessDenied, - NoDevice, - BadPathName, - Unexpected, -}; - -pub fn SetCurrentDirectory(path_name: []const u16) SetCurrentDirectoryError!void { - const path_len_bytes = math.cast(u16, path_name.len * 2) orelse return error.NameTooLong; - - var nt_name: UNICODE_STRING = .{ - .Length = path_len_bytes, - .MaximumLength = path_len_bytes, - .Buffer = @constCast(path_name.ptr), - }; - - const rc = ntdll.RtlSetCurrentDirectory_U(&nt_name); - switch (rc) { - .SUCCESS => {}, - .OBJECT_NAME_INVALID => return error.BadPathName, - .OBJECT_NAME_NOT_FOUND => return error.FileNotFound, - .OBJECT_PATH_NOT_FOUND => return error.FileNotFound, - .NO_MEDIA_IN_DEVICE => return error.NoDevice, - .INVALID_PARAMETER => unreachable, - .ACCESS_DENIED => return error.AccessDenied, - .OBJECT_PATH_SYNTAX_BAD => unreachable, - .NOT_A_DIRECTORY => return error.NotDir, - else => return unexpectedStatus(rc), - } -} - pub const GetCurrentDirectoryError = error{ NameTooLong, Unexpected, diff --git a/lib/std/posix.zig b/lib/std/posix.zig index 133dfc0293..4bd19acb1a 100644 --- a/lib/std/posix.zig +++ b/lib/std/posix.zig @@ -1171,11 +1171,9 @@ pub const ChangeCurDirError = error{ /// On other platforms, `dir_path` is an opaque sequence of bytes with no particular encoding. pub fn chdir(dir_path: []const u8) ChangeCurDirError!void { if (native_os == .wasi and !builtin.link_libc) { - @compileError("WASI does not support os.chdir"); + @compileError("unsupported OS"); } else if (native_os == .windows) { - var wtf16_dir_path: [windows.PATH_MAX_WIDE]u16 = undefined; - const len = try windows.wtf8ToWtf16Le(&wtf16_dir_path, dir_path); - return chdirW(wtf16_dir_path[0..len]); + @compileError("unsupported OS"); } else { const dir_path_c = try toPosixPath(dir_path); return chdirZ(&dir_path_c); @@ -1188,12 +1186,9 @@ pub fn chdir(dir_path: []const u8) ChangeCurDirError!void { /// On other platforms, `dir_path` is an opaque sequence of bytes with no particular encoding. pub fn chdirZ(dir_path: [*:0]const u8) ChangeCurDirError!void { if (native_os == .windows) { - const dir_path_span = mem.span(dir_path); - var wtf16_dir_path: [windows.PATH_MAX_WIDE]u16 = undefined; - const len = try windows.wtf8ToWtf16Le(&wtf16_dir_path, dir_path_span); - return chdirW(wtf16_dir_path[0..len]); + @compileError("unsupported OS"); } else if (native_os == .wasi and !builtin.link_libc) { - return chdir(mem.span(dir_path)); + @compileError("unsupported OS"); } switch (errno(system.chdir(dir_path))) { .SUCCESS => return, @@ -1210,14 +1205,6 @@ pub fn chdirZ(dir_path: [*:0]const u8) ChangeCurDirError!void { } } -/// Windows-only. Same as `chdir` except the parameter is WTF16 LE encoded. -pub fn chdirW(dir_path: []const u16) ChangeCurDirError!void { - windows.SetCurrentDirectory(dir_path) catch |err| switch (err) { - error.NoDevice => return error.FileSystem, - else => |e| return e, - }; -} - pub const FchdirError = error{ AccessDenied, NotDir, diff --git a/lib/std/process.zig b/lib/std/process.zig index cf751f7a28..865376d907 100644 --- a/lib/std/process.zig +++ b/lib/std/process.zig @@ -2304,3 +2304,28 @@ pub fn exit(status: u8) noreturn { else => posix.system.exit(status), } } + +pub const SetCurrentDirError = error{ + AccessDenied, + BadPathName, + FileNotFound, + FileSystem, + NameTooLong, + NoDevice, + NotDir, + OperationUnsupported, + UnrecognizedVolume, +} || Io.Cancelable || Io.UnexpectedError; + +/// Changes the current working directory to the open directory handle. +/// Corresponds to "fchdir" in libc. +/// +/// This modifies global process state and can have surprising effects in +/// multithreaded applications. Most applications and especially libraries +/// should not call this function as a general rule, however it can have use +/// cases in, for example, implementing a shell, or child process execution. +/// +/// Calling this function makes code less portable and less reusable. +pub fn setCurrentDir(io: Io, dir: Io.Dir) !void { + return io.vtable.processSetCurrentDir(io.userdata, dir); +} diff --git a/lib/std/testing.zig b/lib/std/testing.zig index 5b5ec852e9..9519eeb427 100644 --- a/lib/std/testing.zig +++ b/lib/std/testing.zig @@ -629,6 +629,7 @@ pub const TmpDir = struct { }; pub fn tmpDir(opts: Io.Dir.OpenOptions) TmpDir { + comptime assert(builtin.is_test); var random_bytes: [TmpDir.random_bytes_count]u8 = undefined; std.crypto.random.bytes(&random_bytes); var sub_path: [TmpDir.sub_path_len]u8 = undefined; diff --git a/test/standalone/posix/cwd.zig b/test/standalone/posix/cwd.zig index 43dcc63bfe..1bafdfebac 100644 --- a/test/standalone/posix/cwd.zig +++ b/test/standalone/posix/cwd.zig @@ -1,6 +1,10 @@ -const std = @import("std"); const builtin = @import("builtin"); +const std = @import("std"); +const Io = std.Io; +const Allocator = std.mem.Allocator; +const assert = std.debug.assert; + const path_max = std.fs.max_path_bytes; pub fn main() !void { @@ -9,13 +13,17 @@ pub fn main() !void { return; } - var Allocator = std.heap.DebugAllocator(.{}){}; - const a = Allocator.allocator(); - defer std.debug.assert(Allocator.deinit() == .ok); + var debug_allocator: std.heap.DebugAllocator(.{}) = .{}; + defer assert(debug_allocator.deinit() == .ok); + const gpa = debug_allocator.allocator(); + + var threaded: std.Io.Threaded = .init(gpa, .{}); + defer threaded.deinit(); + const io = threaded.io(); try test_chdir_self(); try test_chdir_absolute(); - try test_chdir_relative(a); + try test_chdir_relative(gpa, io); } // get current working directory and expect it to match given path @@ -46,20 +54,20 @@ fn test_chdir_absolute() !void { try expect_cwd(parent); } -fn test_chdir_relative(a: std.mem.Allocator) !void { - var tmp = std.testing.tmpDir(.{}); - defer tmp.cleanup(); +fn test_chdir_relative(gpa: Allocator, io: Io) !void { + var tmp = tmpDir(io, .{}); + defer tmp.cleanup(io); // Use the tmpDir parent_dir as the "base" for the test. Then cd into the child - try tmp.parent_dir.setAsCwd(); + try std.process.setCurrentDir(io, tmp.parent_dir); // Capture base working directory path, to build expected full path var base_cwd_buf: [path_max]u8 = undefined; const base_cwd = try std.posix.getcwd(base_cwd_buf[0..]); const relative_dir_name = &tmp.sub_path; - const expected_path = try std.fs.path.resolve(a, &.{ base_cwd, relative_dir_name }); - defer a.free(expected_path); + const expected_path = try std.fs.path.resolve(gpa, &.{ base_cwd, relative_dir_name }); + defer gpa.free(expected_path); // change current working directory to new test directory try std.posix.chdir(relative_dir_name); @@ -68,8 +76,46 @@ fn test_chdir_relative(a: std.mem.Allocator) !void { const new_cwd = try std.posix.getcwd(new_cwd_buf[0..]); // On Windows, fs.path.resolve returns an uppercase drive letter, but the drive letter returned by getcwd may be lowercase - const resolved_cwd = try std.fs.path.resolve(a, &.{new_cwd}); - defer a.free(resolved_cwd); + const resolved_cwd = try std.fs.path.resolve(gpa, &.{new_cwd}); + defer gpa.free(resolved_cwd); try std.testing.expectEqualStrings(expected_path, resolved_cwd); } + +pub fn tmpDir(io: Io, opts: Io.Dir.OpenOptions) TmpDir { + var random_bytes: [TmpDir.random_bytes_count]u8 = undefined; + std.crypto.random.bytes(&random_bytes); + var sub_path: [TmpDir.sub_path_len]u8 = undefined; + _ = std.fs.base64_encoder.encode(&sub_path, &random_bytes); + + const cwd = Io.Dir.cwd(); + var cache_dir = cwd.createDirPathOpen(io, ".zig-cache", .{}) catch + @panic("unable to make tmp dir for testing: unable to make and open .zig-cache dir"); + defer cache_dir.close(io); + const parent_dir = cache_dir.createDirPathOpen(io, "tmp", .{}) catch + @panic("unable to make tmp dir for testing: unable to make and open .zig-cache/tmp dir"); + const dir = parent_dir.createDirPathOpen(io, &sub_path, .{ .open_options = opts }) catch + @panic("unable to make tmp dir for testing: unable to make and open the tmp dir"); + + return .{ + .dir = dir, + .parent_dir = parent_dir, + .sub_path = sub_path, + }; +} + +pub const TmpDir = struct { + dir: Io.Dir, + parent_dir: Io.Dir, + sub_path: [sub_path_len]u8, + + const random_bytes_count = 12; + const sub_path_len = std.fs.base64_encoder.calcSize(random_bytes_count); + + pub fn cleanup(self: *TmpDir, io: Io) void { + self.dir.close(io); + self.parent_dir.deleteTree(io, &self.sub_path) catch {}; + self.parent_dir.close(io); + self.* = undefined; + } +}; diff --git a/test/standalone/posix/relpaths.zig b/test/standalone/posix/relpaths.zig index 447285c444..5a8b438242 100644 --- a/test/standalone/posix/relpaths.zig +++ b/test/standalone/posix/relpaths.zig @@ -14,21 +14,21 @@ pub fn main() !void { const gpa = debug_allocator.allocator(); defer std.debug.assert(debug_allocator.deinit() == .ok); - const io = std.Io.Threaded.global_single_threaded.ioBasic(); + var threaded: std.Io.Threaded = .init(gpa, .{}); + defer threaded.deinit(); + const io = threaded.io(); - // TODO this API isn't supposed to be used outside of unit testing. make it compilation error if used - // outside of unit testing. - var tmp = std.testing.tmpDir(.{}); - defer tmp.cleanup(); + var tmp = tmpDir(io, .{}); + defer tmp.cleanup(io); // Want to test relative paths, so cd into the tmpdir for these tests - try tmp.dir.setAsCwd(); + try std.process.setCurrentDir(io, tmp.dir); try test_symlink(gpa, io, tmp); try test_link(io, tmp); } -fn test_symlink(gpa: Allocator, io: Io, tmp: std.testing.TmpDir) !void { +fn test_symlink(gpa: Allocator, io: Io, tmp: TmpDir) !void { const target_name = "symlink-target"; const symlink_name = "symlinker"; @@ -47,32 +47,15 @@ fn test_symlink(gpa: Allocator, io: Io, tmp: std.testing.TmpDir) !void { else => return err, }; } else { - try std.posix.symlink(target_name, symlink_name); + try Io.Dir.cwd().symLink(io, target_name, symlink_name, .{}); } var buffer: [std.fs.max_path_bytes]u8 = undefined; - const given = try std.posix.readlink(symlink_name, buffer[0..]); + const given = buffer[0..try Io.Dir.cwd().readLink(io, symlink_name, &buffer)]; try std.testing.expectEqualStrings(target_name, given); } -fn getLinkInfo(fd: std.posix.fd_t) !struct { std.posix.ino_t, std.posix.nlink_t } { - if (builtin.target.os.tag == .linux) { - const stx = try std.os.linux.wrapped.statx( - fd, - "", - std.posix.AT.EMPTY_PATH, - .{ .INO = true, .NLINK = true }, - ); - std.debug.assert(stx.mask.INO); - std.debug.assert(stx.mask.NLINK); - return .{ stx.ino, stx.nlink }; - } - - const st = try std.posix.fstat(fd); - return .{ st.ino, st.nlink }; -} - -fn test_link(io: Io, tmp: std.testing.TmpDir) !void { +fn test_link(io: Io, tmp: TmpDir) !void { switch (builtin.target.os.tag) { .linux, .illumos => {}, else => return, @@ -84,7 +67,7 @@ fn test_link(io: Io, tmp: std.testing.TmpDir) !void { try tmp.dir.writeFile(io, .{ .sub_path = target_name, .data = "example" }); // Test 1: create the relative link from inside tmp - try std.posix.link(target_name, link_name); + try Io.Dir.hardLink(.cwd(), target_name, .cwd(), link_name, io, .{}); // Verify const efd = try tmp.dir.openFile(io, target_name, .{}); @@ -94,16 +77,54 @@ fn test_link(io: Io, tmp: std.testing.TmpDir) !void { defer nfd.close(io); { - const eino, _ = try getLinkInfo(efd.handle); - const nino, const nlink = try getLinkInfo(nfd.handle); - try std.testing.expectEqual(eino, nino); - try std.testing.expectEqual(@as(std.posix.nlink_t, 2), nlink); + const e_stat = try efd.stat(io); + const n_stat = try nfd.stat(io); + try std.testing.expectEqual(e_stat.inode, n_stat.inode); + try std.testing.expectEqual(2, n_stat.nlink); } // Test 2: Remove the link and see the stats update - try std.posix.unlink(link_name); + try Io.Dir.cwd().deleteFile(io, link_name); { - _, const elink = try getLinkInfo(efd.handle); - try std.testing.expectEqual(@as(std.posix.nlink_t, 1), elink); + const e_stat = try efd.stat(io); + try std.testing.expectEqual(1, e_stat.nlink); } } + +pub fn tmpDir(io: Io, opts: Io.Dir.OpenOptions) TmpDir { + var random_bytes: [TmpDir.random_bytes_count]u8 = undefined; + std.crypto.random.bytes(&random_bytes); + var sub_path: [TmpDir.sub_path_len]u8 = undefined; + _ = std.fs.base64_encoder.encode(&sub_path, &random_bytes); + + const cwd = Io.Dir.cwd(); + var cache_dir = cwd.createDirPathOpen(io, ".zig-cache", .{}) catch + @panic("unable to make tmp dir for testing: unable to make and open .zig-cache dir"); + defer cache_dir.close(io); + const parent_dir = cache_dir.createDirPathOpen(io, "tmp", .{}) catch + @panic("unable to make tmp dir for testing: unable to make and open .zig-cache/tmp dir"); + const dir = parent_dir.createDirPathOpen(io, &sub_path, .{ .open_options = opts }) catch + @panic("unable to make tmp dir for testing: unable to make and open the tmp dir"); + + return .{ + .dir = dir, + .parent_dir = parent_dir, + .sub_path = sub_path, + }; +} + +pub const TmpDir = struct { + dir: Io.Dir, + parent_dir: Io.Dir, + sub_path: [sub_path_len]u8, + + const random_bytes_count = 12; + const sub_path_len = std.fs.base64_encoder.calcSize(random_bytes_count); + + pub fn cleanup(self: *TmpDir, io: Io) void { + self.dir.close(io); + self.parent_dir.deleteTree(io, &self.sub_path) catch {}; + self.parent_dir.close(io); + self.* = undefined; + } +}; diff --git a/test/standalone/windows_bat_args/fuzz.zig b/test/standalone/windows_bat_args/fuzz.zig index db6043d3e9..b68639dda4 100644 --- a/test/standalone/windows_bat_args/fuzz.zig +++ b/test/standalone/windows_bat_args/fuzz.zig @@ -44,8 +44,8 @@ pub fn main() anyerror!void { var tmp = std.testing.tmpDir(.{}); defer tmp.cleanup(); - try tmp.dir.setAsCwd(); - defer tmp.parent_dir.setAsCwd() catch {}; + try std.process.setCurrentDir(io, tmp.dir); + defer std.process.setCurrentDir(io, tmp.parent_dir) catch {}; // `child_exe_path_orig` might be relative; make it relative to our new cwd. const child_exe_path = try std.fs.path.resolve(gpa, &.{ "..\\..\\..", child_exe_path_orig }); diff --git a/test/standalone/windows_bat_args/test.zig b/test/standalone/windows_bat_args/test.zig index 48c32826e5..73f0ee9453 100644 --- a/test/standalone/windows_bat_args/test.zig +++ b/test/standalone/windows_bat_args/test.zig @@ -18,8 +18,8 @@ pub fn main() anyerror!void { var tmp = std.testing.tmpDir(.{}); defer tmp.cleanup(); - try tmp.dir.setAsCwd(); - defer tmp.parent_dir.setAsCwd() catch {}; + try std.process.setCurrentDir(io, tmp.dir); + defer std.process.setCurrentDir(io, tmp.parent_dir) catch {}; // `child_exe_path_orig` might be relative; make it relative to our new cwd. const child_exe_path = try std.fs.path.resolve(gpa, &.{ "..\\..\\..", child_exe_path_orig }); diff --git a/test/standalone/windows_spawn/main.zig b/test/standalone/windows_spawn/main.zig index db771c88eb..f5df4dee25 100644 --- a/test/standalone/windows_spawn/main.zig +++ b/test/standalone/windows_spawn/main.zig @@ -127,8 +127,8 @@ pub fn main() anyerror!void { try testExecError(error.FileNotFound, gpa, "goodbye"); // Now let's set the tmp dir as the cwd and set the path only include the "something" sub dir - try tmp.dir.setAsCwd(); - defer tmp.parent_dir.setAsCwd() catch {}; + try std.process.setCurrentDir(io, tmp.dir); + defer std.process.setCurrentDir(io, tmp.parent_dir) catch {}; const something_subdir_abs_path = try std.mem.concatWithSentinel(gpa, u16, &.{ tmp_absolute_path_w, utf16Literal("\\something") }, 0); defer gpa.free(something_subdir_abs_path); @@ -191,7 +191,7 @@ pub fn main() anyerror!void { defer subdir_cwd.close(io); try renameExe(tmp.dir, "something/goodbye.exe", "hello.exe"); - try subdir_cwd.setAsCwd(); + try std.process.setCurrentDir(io, subdir_cwd); // clear the PATH again std.debug.assert(windows.kernel32.SetEnvironmentVariableW( diff --git a/tools/fetch_them_macos_headers.zig b/tools/fetch_them_macos_headers.zig index ed9b362452..c55a569e9f 100644 --- a/tools/fetch_them_macos_headers.zig +++ b/tools/fetch_them_macos_headers.zig @@ -4,7 +4,6 @@ const Dir = std.Io.Dir; const mem = std.mem; const process = std.process; const assert = std.debug.assert; -const tmpDir = std.testing.tmpDir; const fatal = std.process.fatal; const info = std.log.info; @@ -111,15 +110,14 @@ pub fn main() anyerror!void { const os_ver: OsVer = @enumFromInt(version.major); info("found SDK deployment target macOS {f} aka '{t}'", .{ version, os_ver }); - var tmp = tmpDir(.{}); - defer tmp.cleanup(); + const tmp_dir: Io.Dir = .cwd(); for (&[_]Arch{ .aarch64, .x86_64 }) |arch| { const target: Target = .{ .arch = arch, .os_ver = os_ver, }; - try fetchTarget(allocator, io, argv.items, sysroot_path, target, version, tmp); + try fetchTarget(allocator, io, argv.items, sysroot_path, target, version, tmp_dir); } } @@ -130,11 +128,11 @@ fn fetchTarget( sysroot: []const u8, target: Target, ver: Version, - tmp: std.testing.TmpDir, + tmp_dir: Io.Dir, ) !void { const tmp_filename = "macos-headers"; const headers_list_filename = "macos-headers.o.d"; - const tmp_path = try tmp.dir.realPathFileAlloc(io, ".", arena); + const tmp_path = try tmp_dir.realPathFileAlloc(io, ".", arena); const tmp_file_path = try Dir.path.join(arena, &[_][]const u8{ tmp_path, tmp_filename }); const headers_list_path = try Dir.path.join(arena, &[_][]const u8{ tmp_path, headers_list_filename }); @@ -173,7 +171,7 @@ fn fetchTarget( } // Read in the contents of `macos-headers.o.d` - const headers_list_file = try tmp.dir.openFile(io, headers_list_filename, .{}); + const headers_list_file = try tmp_dir.openFile(io, headers_list_filename, .{}); defer headers_list_file.close(io); var headers_dir = Dir.cwd().openDir(io, headers_source_prefix, .{}) catch |err| switch (err) { -- cgit v1.2.3