aboutsummaryrefslogtreecommitdiff
path: root/lib/std
diff options
context:
space:
mode:
authorAndrew Kelley <andrew@ziglang.org>2025-12-22 14:37:41 -0800
committerAndrew Kelley <andrew@ziglang.org>2025-12-23 22:15:12 -0800
commit3c2f5adf41f0e75fd5e8f6661891dd7d4fa770a9 (patch)
tree5ae7765b93dfde9f5638b83ba2436fbbdd0da24d /lib/std
parent86e9e32cf0d5a028d6ebb32f8d0f3d0a23e717b6 (diff)
downloadzig-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.zig2
-rw-r--r--lib/std/Io/Terminal.zig14
-rw-r--r--lib/std/Io/Threaded.zig99
-rw-r--r--lib/std/Thread.zig2
-rw-r--r--lib/std/debug.zig20
-rw-r--r--lib/std/dynamic_library.zig2
-rw-r--r--lib/std/start.zig10
-rw-r--r--lib/std/std.zig19
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