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.
This commit is contained in:
Andrew Kelley 2025-12-23 22:07:31 -08:00
parent 98e9716c08
commit fa79d34674
12 changed files with 229 additions and 116 deletions

View file

@ -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,

View file

@ -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,

View file

@ -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,

View file

@ -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,

View file

@ -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);
}

View file

@ -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;

View file

@ -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;
}
};

View file

@ -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;
}
};

View file

@ -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 });

View file

@ -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 });

View file

@ -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(

View file

@ -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) {