diff options
| author | Andrew Kelley <andrew@ziglang.org> | 2025-12-22 14:37:41 -0800 |
|---|---|---|
| committer | Andrew Kelley <andrew@ziglang.org> | 2025-12-23 22:15:12 -0800 |
| commit | 3c2f5adf41f0e75fd5e8f6661891dd7d4fa770a9 (patch) | |
| tree | 5ae7765b93dfde9f5638b83ba2436fbbdd0da24d /lib/std | |
| parent | 86e9e32cf0d5a028d6ebb32f8d0f3d0a23e717b6 (diff) | |
| download | zig-3c2f5adf41f0e75fd5e8f6661891dd7d4fa770a9.tar.gz zig-3c2f5adf41f0e75fd5e8f6661891dd7d4fa770a9.zip | |
std: integrate Io.Threaded with environment variables
* std.option allows overriding the debug Io instance
* if the default is used, start code initializes environ and argv0
also fix some places that needed recancel(), thanks mlugg!
See #30562
Diffstat (limited to 'lib/std')
| -rw-r--r-- | lib/std/Io/File.zig | 2 | ||||
| -rw-r--r-- | lib/std/Io/Terminal.zig | 14 | ||||
| -rw-r--r-- | lib/std/Io/Threaded.zig | 99 | ||||
| -rw-r--r-- | lib/std/Thread.zig | 2 | ||||
| -rw-r--r-- | lib/std/debug.zig | 20 | ||||
| -rw-r--r-- | lib/std/dynamic_library.zig | 2 | ||||
| -rw-r--r-- | lib/std/start.zig | 10 | ||||
| -rw-r--r-- | lib/std/std.zig | 19 |
8 files changed, 107 insertions, 61 deletions
diff --git a/lib/std/Io/File.zig b/lib/std/Io/File.zig index ae6c69285f..804b0c9155 100644 --- a/lib/std/Io/File.zig +++ b/lib/std/Io/File.zig @@ -391,7 +391,7 @@ pub fn setOwner(file: File, io: Io, owner: ?Uid, group: ?Gid) SetOwnerError!void /// On POSIX systems this corresponds to "mode" and on Windows this corresponds to "attributes". /// /// Overridable via `std.options`. -pub const Permissions = std.options.FilePermissions orelse if (is_windows) enum(std.os.windows.DWORD) { +pub const Permissions = std.io_options.FilePermissions orelse if (is_windows) enum(std.os.windows.DWORD) { default_file = 0, _, diff --git a/lib/std/Io/Terminal.zig b/lib/std/Io/Terminal.zig index 549adcf207..beacc4d301 100644 --- a/lib/std/Io/Terminal.zig +++ b/lib/std/Io/Terminal.zig @@ -48,7 +48,15 @@ pub const Mode = union(enum) { /// stdout/stderr). /// /// Will attempt to enable ANSI escape code support if necessary/possible. - pub fn detect(io: Io, file: File) Io.Cancelable!Mode { + /// + /// * `NO_COLOR` indicates whether "NO_COLOR" environment variable is + /// present and non-empty. + /// * `CLICOLOR_FORCE` indicates whether "CLICOLOR_FORCE" environment + /// variable is present and non-empty. + pub fn detect(io: Io, file: File, NO_COLOR: bool, CLICOLOR_FORCE: bool) Io.Cancelable!Mode { + const force_color: ?bool = if (NO_COLOR) false else if (CLICOLOR_FORCE) true else null; + if (force_color == false) return .no_color; + if (file.enableAnsiEscapeCodes(io)) |_| { return .escape_codes; } else |err| switch (err) { @@ -65,10 +73,8 @@ pub const Mode = union(enum) { .reset_attributes = info.wAttributes, } }; } - return .escape_codes; } - - return .no_color; + return if (force_color == true) .escape_codes else .no_color; } }; diff --git a/lib/std/Io/Threaded.zig b/lib/std/Io/Threaded.zig index 054752fb03..4f1fe48310 100644 --- a/lib/std/Io/Threaded.zig +++ b/lib/std/Io/Threaded.zig @@ -60,30 +60,34 @@ stderr_writer: File.Writer = .{ stderr_mode: Io.Terminal.Mode = .no_color, stderr_writer_initialized: bool = false, +argv0: Argv0, environ: Environ, -args: Args, -pub const Environ = switch (native_os) { +pub const Argv0 = switch (native_os) { .openbsd, .haiku => struct { - PATH: ?[]const u8, - - pub const empty: @This() = .{ - .PATH = null, - }; - }, - else => struct { - pub const empty: @This() = .{}; + value: ?[*:0]const u8 = null, }, + else => struct {}, }; -pub const Args = switch (native_os) { - .openbsd, .haiku => struct { - list: []const []const u8, - pub const empty: @This() = .{ .list = &.{} }; - }, - else => struct { - pub const empty: @This() = .{}; - }, +pub const Environ = struct { + /// Unmodified data directly from the OS. + block: Block = &.{}, + /// Protected by `mutex`. Determines whether the other fields have been + /// memoized based on `block`. + initialized: bool = false, + /// Protected by `mutex`. Memoized based on `block`. Tracks whether the + /// environment variables are present and non-empty. + exist: struct { + NO_COLOR: bool = false, + CLICOLOR_FORCE: bool = false, + } = .{}, + /// Protected by `mutex`. Memoized based on `block`. + string: struct { + PATH: ?[:0]const u8 = null, + } = .{}, + + pub const Block = []const [*:0]const u8; }; pub const RobustCancel = if (std.Thread.use_pthreads or native_os == .linux) enum { @@ -591,18 +595,11 @@ pub const InitOptions = struct { robust_cancel: RobustCancel = .disabled, /// Affects the following operations: /// * `processExecutablePath` on OpenBSD and Haiku. - /// - /// The default value causes this to be a compile error on systems that need to - /// initialize this field. `Environ.empty` can be used to omit this field on - /// all targets. - environ: Environ = .{}, + argv0: Argv0 = .{}, /// Affects the following operations: - /// * `processExecutablePath` on OpenBSD and Haiku. - /// - /// The default value causes this to be a compile error on systems that need to - /// initialize this field. `Args.empty` can be used to omit this field on all - /// targets. - args: Args = .{}, + /// * `fileIsTty` + /// * `processExecutablePath` on OpenBSD and Haiku (observes "PATH"). + environ: Environ = .{}, }; /// Related: @@ -636,8 +633,8 @@ pub fn init( .current_closure = null, .cancel_protection = undefined, }, + .argv0 = options.argv0, .environ = options.environ, - .args = options.args, .robust_cancel = options.robust_cancel, }; @@ -678,8 +675,8 @@ pub const init_single_threaded: Threaded = .{ .cancel_protection = undefined, }, .robust_cancel = .disabled, - .environ = .empty, - .args = .empty, + .argv0 = .{}, + .environ = .{}, }; var global_single_threaded_instance: Threaded = .init_single_threaded; @@ -7242,17 +7239,16 @@ fn processExecutablePath(userdata: ?*anyopaque, out_buffer: []u8) std.process.Ex } }, .openbsd, .haiku => { - // The best we can do on these operating systems is check based on CLI args. - const argv = t.args.list; - if (argv.len == 0) return error.OperationUnsupported; - const argv0 = argv[0]; + // The best we can do on these operating systems is check based on + // the first process argument. + const argv0 = t.argv0.value orelse return error.OperationUnsupported; if (std.mem.findScalar(u8, argv0, '/') != null) { // argv[0] is a path (relative or absolute): use realpath(3) directly const current_thread = Thread.getCurrent(t); var resolved_buf: [std.c.PATH_MAX]u8 = undefined; try current_thread.beginSyscall(); while (true) { - if (std.c.realpath(argv[0], &resolved_buf)) |p| { + if (std.c.realpath(argv0, &resolved_buf)) |p| { assert(p == &resolved_buf); break current_thread.endSyscall(); } else switch (@as(std.c.E, @enumFromInt(std.c._errno().*))) { @@ -7283,13 +7279,14 @@ fn processExecutablePath(userdata: ?*anyopaque, out_buffer: []u8) std.process.Ex return resolved.len; } else if (argv0.len != 0) { // argv[0] is not empty (and not a path): search PATH + t.scanEnviron(); + const PATH = t.environ.string.PATH orelse return error.FileNotFound; const current_thread = Thread.getCurrent(t); - const PATH = t.environ.PATH orelse return error.FileNotFound; var it = std.mem.tokenizeScalar(u8, PATH, ':'); it: while (it.next()) |dir| { var resolved_path_buf: [std.c.PATH_MAX]u8 = undefined; const resolved_path = std.fmt.bufPrintSentinel(&resolved_path_buf, "{s}/{s}", .{ - dir, argv[0], + dir, argv0, }, 0) catch continue; var resolved_buf: [std.c.PATH_MAX]u8 = undefined; @@ -10752,7 +10749,10 @@ fn initLockedStderr( if (is_windows) t.stderr_writer.file = .stderr(); t.stderr_writer.io = io_t; t.stderr_writer_initialized = true; - t.stderr_mode = terminal_mode orelse try .detect(io_t, t.stderr_writer.file); + t.scanEnviron(); + const NO_COLOR = t.environ.exist.NO_COLOR; + const CLICOLOR_FORCE = t.environ.exist.CLICOLOR_FORCE; + t.stderr_mode = terminal_mode orelse try .detect(io_t, t.stderr_writer.file, NO_COLOR, CLICOLOR_FORCE); } std.Progress.clearWrittenWithEscapeCodes(&t.stderr_writer) catch |err| switch (err) { error.WriteFailed => switch (t.stderr_writer.err.?) { @@ -10777,7 +10777,7 @@ fn unlockStderr(userdata: ?*anyopaque) void { const t: *Threaded = @ptrCast(@alignCast(userdata)); t.stderr_writer.interface.flush() catch |err| switch (err) { error.WriteFailed => switch (t.stderr_writer.err.?) { - error.Canceled => @panic("TODO make this uncancelable"), + error.Canceled => recancel(t), else => {}, }, }; @@ -11910,6 +11910,23 @@ const pthreads_futex = struct { } }; +fn scanEnviron(t: *Threaded) void { + t.mutex.lock(); + defer t.mutex.unlock(); + + if (t.environ.initialized) return; + t.environ.initialized = true; + + if (native_os == .wasi) { + @panic("TODO"); + } + + for (t.environ.block) |kv| { + _ = kv; + @panic("TODO"); + } +} + test { _ = @import("Threaded/test.zig"); } diff --git a/lib/std/Thread.zig b/lib/std/Thread.zig index f25c664000..1900225099 100644 --- a/lib/std/Thread.zig +++ b/lib/std/Thread.zig @@ -322,7 +322,7 @@ pub fn getName(self: Thread, buffer_ptr: *[max_name_len:0]u8) GetNameError!?[]co var buf: [32]u8 = undefined; const path = try std.fmt.bufPrint(&buf, "/proc/self/task/{d}/comm", .{self.getHandle()}); - const io = Io.Threaded.global_single_threaded.ioBasic(); + const io = std.options.debug_io; const file = try Io.Dir.cwd().openFile(io, path, .{}); defer file.close(io); diff --git a/lib/std/debug.zig b/lib/std/debug.zig index 5129a70b02..25a0bc120d 100644 --- a/lib/std/debug.zig +++ b/lib/std/debug.zig @@ -261,10 +261,6 @@ 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. -const static_single_threaded_io = Io.Threaded.global_single_threaded.ioBasic(); - /// Allows the caller to freely write to stderr until `unlockStderr` is called. /// /// During the lock, any `std.Progress` information is cleared from the terminal. @@ -284,15 +280,15 @@ const static_single_threaded_io = Io.Threaded.global_single_threaded.ioBasic(); /// Alternatively, use the higher-level `Io.lockStderr` to integrate with the /// application's chosen `Io` implementation. pub fn lockStderr(buffer: []u8) Io.LockedStderr { - return static_single_threaded_io.lockStderr(buffer, null) catch |err| switch (err) { - // Impossible to cancel because no calls to cancel using - // `static_single_threaded_io` exist. - error.Canceled => unreachable, + const io = std.options.debug_io; + return io.lockStderr(buffer, null) catch |err| switch (err) { + error.Canceled => io.recancel(), }; } pub fn unlockStderr() void { - static_single_threaded_io.unlockStderr(); + const io = std.options.debug_io; + io.unlockStderr(); } /// Writes to stderr, ignoring errors. @@ -627,7 +623,7 @@ pub noinline fn captureCurrentStackTrace(options: StackUnwindOptions, addr_buf: defer it.deinit(); if (!it.stratOk(options.allow_unsafe_unwind)) return empty_trace; - const io = static_single_threaded_io; + const io = std.options.debug_io; var total_frames: usize = 0; var index: usize = 0; @@ -689,7 +685,7 @@ pub noinline fn writeCurrentStackTrace(options: StackUnwindOptions, t: Io.Termin var total_frames: usize = 0; var wait_for = options.first_address; var printed_any_frame = false; - const io = static_single_threaded_io; + const io = std.options.debug_io; while (true) switch (it.next(io)) { .switch_to_fp => |unwind_error| { switch (StackIterator.fp_usability) { @@ -797,7 +793,7 @@ pub fn writeStackTrace(st: *const StackTrace, t: Io.Terminal) Writer.Error!void return; }, }; - const io = static_single_threaded_io; + const io = std.options.debug_io; 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. diff --git a/lib/std/dynamic_library.zig b/lib/std/dynamic_library.zig index 70b236f3e5..b4acc8ae70 100644 --- a/lib/std/dynamic_library.zig +++ b/lib/std/dynamic_library.zig @@ -222,7 +222,7 @@ pub const ElfDynLib = struct { /// Trusts the file. Malicious file will be able to execute arbitrary code. pub fn open(path: []const u8) Error!ElfDynLib { - const io = Io.Threaded.global_single_threaded.ioBasic(); + const io = std.options.debug_io; const fd = try resolveFromName(io, path); defer posix.close(fd); diff --git a/lib/std/start.zig b/lib/std/start.zig index 64a1c17175..5a3e39e24a 100644 --- a/lib/std/start.zig +++ b/lib/std/start.zig @@ -669,6 +669,11 @@ inline fn callMainWithArgs(argc: usize, argv: [*][*:0]u8, envp: [][*:0]u8) u8 { std.os.argv = argv[0..argc]; std.os.environ = envp; + if (std.io_options.debug_threaded_io) |t| { + if (@sizeOf(std.Io.Threaded.Argv0) != 0) t.argv0.value = argv[0]; + t.environ = .{ .block = envp }; + } + std.debug.maybeEnableSegfaultHandler(); return callMain(); @@ -691,6 +696,11 @@ fn main(c_argc: c_int, c_argv: [*][*:0]c_char, c_envp: [*:null]?[*:0]c_char) cal fn mainWithoutEnv(c_argc: c_int, c_argv: [*][*:0]c_char) callconv(.c) c_int { std.os.argv = @as([*][*:0]u8, @ptrCast(c_argv))[0..@intCast(c_argc)]; + + if (@sizeOf(std.Io.Threaded.Argv0) != 0) { + if (std.io_options.debug_threaded_io) |t| t.argv0.value = std.os.argv[0]; + } + return callMain(); } diff --git a/lib/std/std.zig b/lib/std/std.zig index 1690c0575c..46f6415d09 100644 --- a/lib/std/std.zig +++ b/lib/std/std.zig @@ -108,8 +108,11 @@ pub const start = @import("start.zig"); const root = @import("root"); -/// Stdlib-wide options that can be overridden by the root file. +/// Compile-time known settings overridable by the root source file. pub const options: Options = if (@hasDecl(root, "std_options")) root.std_options else .{}; +/// Minimal set of `options` moved here to avoid dependency loop compilation +/// errors. +pub const io_options: IoOptions = if (@hasDecl(root, "std_io_options")) root.std_io_options else .{}; pub const Options = struct { enable_segfault_handler: bool = debug.default_enable_segfault_handler, @@ -174,8 +177,22 @@ pub const Options = struct { /// stack traces will just print an error to the relevant `Io.Writer` and return. allow_stack_tracing: bool = !@import("builtin").strip_debug_info, + /// The `Io` instance that `std.debug` uses for `std.debug.print`, + /// capturing stack traces, loading debug info, finding the executable's + /// own path, and environment variables that affect terminal mode + /// detection. The default is to use statically initialized singleton that + /// is independent from the application's `Io` instance in order to make + /// debugging more straightforward. For example, while debugging an `Io` + /// implementation based on coroutines, one likely wants `std.debug.print` + /// to directly write to stderr without trying to interact with the code + /// being debugged. + debug_io: Io = io_options.debug_threaded_io.?.ioBasic(), +}; + +pub const IoOptions = struct { /// Overrides `std.Io.File.Permissions`. FilePermissions: ?type = null, + debug_threaded_io: ?*Io.Threaded = Io.Threaded.global_single_threaded, }; // This forces the start.zig file to be imported, and the comptime logic inside that |
