authorgravatar for squeek502@hotmail.comRyan Liptak <squeek502@hotmail.com> 2026-03-07 22:17:20-08:00
committergravatar for andrew@ziglang.orgAndrew Kelley <andrew@ziglang.org> 2026-03-09 03:19:11+01:00
log6be202f46633d02e20d0f068a32296113ecb95ca
tree9a22fba7d28337a6072123e6737cf7ea96922b13
parent09bf51092ba8f11e342af7bd7d148af63f371709

Io: Add processSetCurrentPath

The logic used to allow providing a path for setting the CWD of a child process in https://codeberg.org/ziglang/zig/pulls/31090 applies here as well: - Windows must provide a path when setting the CWD, so the path of an `Io.Dir` must be resolved before actually calling RtlSetCurrentDirectory_U - A directory handle may have multiple paths associated with it, so providing the CWD as a string retains a legitimate use case in cases where the precise path matters

7 files changed, 77 insertions(+), 8 deletions(-)

lib/std/Io.zig+1
...@@ -218,6 +218,7 @@ pub const VTable = struct {...@@ -218,6 +218,7 @@ pub const VTable = struct {
218 unlockStderr: *const fn (?*anyopaque) void,218 unlockStderr: *const fn (?*anyopaque) void,
219 processCurrentPath: *const fn (?*anyopaque, buffer: []u8) std.process.CurrentPathError!usize,219 processCurrentPath: *const fn (?*anyopaque, buffer: []u8) std.process.CurrentPathError!usize,
220 processSetCurrentDir: *const fn (?*anyopaque, Dir) std.process.SetCurrentDirError!void,220 processSetCurrentDir: *const fn (?*anyopaque, Dir) std.process.SetCurrentDirError!void,
221 processSetCurrentPath: *const fn (?*anyopaque, []const u8) std.process.SetCurrentPathError!void,
221 processReplace: *const fn (?*anyopaque, std.process.ReplaceOptions) std.process.ReplaceError,222 processReplace: *const fn (?*anyopaque, std.process.ReplaceOptions) std.process.ReplaceError,
222 processReplacePath: *const fn (?*anyopaque, Dir, std.process.ReplaceOptions) std.process.ReplaceError,223 processReplacePath: *const fn (?*anyopaque, Dir, std.process.ReplaceOptions) std.process.ReplaceError,
223 processSpawn: *const fn (?*anyopaque, std.process.SpawnOptions) std.process.SpawnError!std.process.Child,224 processSpawn: *const fn (?*anyopaque, std.process.SpawnOptions) std.process.SpawnError!std.process.Child,
lib/std/Io/Dispatch.zig+2-1
...@@ -434,6 +434,7 @@ pub fn io(ev: *Evented) Io {...@@ -434,6 +434,7 @@ pub fn io(ev: *Evented) Io {
434 .unlockStderr = unlockStderr,434 .unlockStderr = unlockStderr,
435 .processCurrentPath = processCurrentPath,435 .processCurrentPath = processCurrentPath,
436 .processSetCurrentDir = processSetCurrentDir,436 .processSetCurrentDir = processSetCurrentDir,
437 .processSetCurrentPath = processSetCurrentPath,
437 .processReplace = processReplace,438 .processReplace = processReplace,
438 .processReplacePath = processReplacePath,439 .processReplacePath = processReplacePath,
439 .processSpawn = processSpawn,440 .processSpawn = processSpawn,
...@@ -4046,7 +4047,7 @@ fn processSetCurrentDir(userdata: ?*anyopaque, dir: Dir) process.SetCurrentDirEr...@@ -4046,7 +4047,7 @@ fn processSetCurrentDir(userdata: ?*anyopaque, dir: Dir) process.SetCurrentDirEr
4046 };4047 };
4047}4048}
40484049
4049fn processSetCurrentPath(userdata: ?*anyopaque, dir_path: []const u8) ChdirError!void {4050fn processSetCurrentPath(userdata: ?*anyopaque, dir_path: []const u8) process.SetCurrentPathError!void {
4050 const ev: *Evented = @ptrCast(@alignCast(userdata));4051 const ev: *Evented = @ptrCast(@alignCast(userdata));
4051 _ = ev;4052 _ = ev;
4052 var path_buffer: [c.PATH_MAX]u8 = undefined;4053 var path_buffer: [c.PATH_MAX]u8 = undefined;
lib/std/Io/Threaded.zig+39
...@@ -1841,6 +1841,7 @@ pub fn io(t: *Threaded) Io {...@@ -1841,6 +1841,7 @@ pub fn io(t: *Threaded) Io {
1841 .unlockStderr = unlockStderr,1841 .unlockStderr = unlockStderr,
1842 .processCurrentPath = processCurrentPath,1842 .processCurrentPath = processCurrentPath,
1843 .processSetCurrentDir = processSetCurrentDir,1843 .processSetCurrentDir = processSetCurrentDir,
1844 .processSetCurrentPath = processSetCurrentPath,
1844 .processReplace = processReplace,1845 .processReplace = processReplace,
1845 .processReplacePath = processReplacePath,1846 .processReplacePath = processReplacePath,
1846 .processSpawn = processSpawn,1847 .processSpawn = processSpawn,
...@@ -2006,6 +2007,7 @@ pub fn ioBasic(t: *Threaded) Io {...@@ -2006,6 +2007,7 @@ pub fn ioBasic(t: *Threaded) Io {
2006 .unlockStderr = unlockStderr,2007 .unlockStderr = unlockStderr,
2007 .processCurrentPath = processCurrentPath,2008 .processCurrentPath = processCurrentPath,
2008 .processSetCurrentDir = processSetCurrentDir,2009 .processSetCurrentDir = processSetCurrentDir,
2010 .processSetCurrentPath = processSetCurrentPath,
2009 .processReplace = processReplace,2011 .processReplace = processReplace,
2010 .processReplacePath = processReplacePath,2012 .processReplacePath = processReplacePath,
2011 .processSpawn = processSpawn,2013 .processSpawn = processSpawn,
...@@ -14176,6 +14178,43 @@ fn processSetCurrentDir(userdata: ?*anyopaque, dir: Dir) process.SetCurrentDirEr...@@ -14176,6 +14178,43 @@ fn processSetCurrentDir(userdata: ?*anyopaque, dir: Dir) process.SetCurrentDirEr
14176 return fchdir(dir.handle);14178 return fchdir(dir.handle);
14177}14179}
1417814180
14181fn processSetCurrentPath(userdata: ?*anyopaque, path: []const u8) process.SetCurrentPathError!void {
14182 const t: *Threaded = @ptrCast(@alignCast(userdata));
14183 _ = t;
14184
14185 if (native_os == .wasi) return error.OperationUnsupported;
14186
14187 if (is_windows) {
14188 var path_w_buf: [windows.PATH_MAX_WIDE]u16 = undefined;
14189 const len = std.unicode.calcWtf16LeLen(path) catch return error.InvalidWtf8;
14190 if (len > path_w_buf.len) return error.NameTooLong;
14191 const path_w_len = std.unicode.wtf8ToWtf16Le(&path_w_buf, path) catch |err| switch (err) {
14192 error.InvalidWtf8 => unreachable, // already validated
14193 };
14194 const path_w = path_w_buf[0..path_w_len];
14195
14196 const syscall: Syscall = try .start();
14197 while (true) switch (windows.ntdll.RtlSetCurrentDirectory_U(&.init(path_w))) {
14198 .SUCCESS => return syscall.finish(),
14199 .OBJECT_NAME_INVALID => return syscall.fail(error.BadPathName),
14200 .OBJECT_NAME_NOT_FOUND => return syscall.fail(error.FileNotFound),
14201 .OBJECT_PATH_NOT_FOUND => return syscall.fail(error.FileNotFound),
14202 .NO_MEDIA_IN_DEVICE => return syscall.fail(error.NoDevice),
14203 .INVALID_PARAMETER => |err| return syscall.ntstatusBug(err),
14204 .ACCESS_DENIED => return syscall.fail(error.AccessDenied),
14205 .OBJECT_PATH_SYNTAX_BAD => |err| return syscall.ntstatusBug(err),
14206 .NOT_A_DIRECTORY => return syscall.fail(error.NotDir),
14207 .CANCELLED => {
14208 try syscall.checkCancel();
14209 continue;
14210 },
14211 else => |status| return syscall.unexpectedNtstatus(status),
14212 };
14213 }
14214
14215 return chdir(path);
14216}
14217
14179pub const PosixAddress = extern union {14218pub const PosixAddress = extern union {
14180 any: posix.sockaddr,14219 any: posix.sockaddr,
14181 in: posix.sockaddr.in,14220 in: posix.sockaddr.in,
lib/std/Io/Uring.zig+2-1
...@@ -752,6 +752,7 @@ pub fn io(ev: *Evented) Io {...@@ -752,6 +752,7 @@ pub fn io(ev: *Evented) Io {
752 .unlockStderr = unlockStderr,752 .unlockStderr = unlockStderr,
753 .processCurrentPath = processCurrentPath,753 .processCurrentPath = processCurrentPath,
754 .processSetCurrentDir = processSetCurrentDir,754 .processSetCurrentDir = processSetCurrentDir,
755 .processSetCurrentPath = processSetCurrentPath,
755 .processReplace = processReplace,756 .processReplace = processReplace,
756 .processReplacePath = processReplacePath,757 .processReplacePath = processReplacePath,
757 .processSpawn = processSpawn,758 .processSpawn = processSpawn,
...@@ -4176,7 +4177,7 @@ fn processSetCurrentDir(userdata: ?*anyopaque, dir: Dir) process.SetCurrentDirEr...@@ -4176,7 +4177,7 @@ fn processSetCurrentDir(userdata: ?*anyopaque, dir: Dir) process.SetCurrentDirEr
4176 return fchdir(&sync, dir.handle);4177 return fchdir(&sync, dir.handle);
4177}4178}
41784179
4179fn processSetCurrentPath(userdata: ?*anyopaque, dir_path: []const u8) ChdirError!void {4180fn processSetCurrentPath(userdata: ?*anyopaque, dir_path: []const u8) process.SetCurrentPathError!void {
4180 const ev: *Evented = @ptrCast(@alignCast(userdata));4181 const ev: *Evented = @ptrCast(@alignCast(userdata));
4181 var path_buffer: [PATH_MAX]u8 = undefined;4182 var path_buffer: [PATH_MAX]u8 = undefined;
4182 const dir_path_posix = try pathToPosix(dir_path, &path_buffer);4183 const dir_path_posix = try pathToPosix(dir_path, &path_buffer);
lib/std/process.zig+29
...@@ -900,6 +900,35 @@ pub fn setCurrentDir(io: Io, dir: Io.Dir) !void {...@@ -900,6 +900,35 @@ pub fn setCurrentDir(io: Io, dir: Io.Dir) !void {
900 return io.vtable.processSetCurrentDir(io.userdata, dir);900 return io.vtable.processSetCurrentDir(io.userdata, dir);
901}901}
902902
903pub const SetCurrentPathError = error{
904 AccessDenied,
905 SymLinkLoop,
906 SystemResources,
907 BadPathName,
908 FileNotFound,
909 FileSystem,
910 NoDevice,
911 NotDir,
912 NameTooLong,
913 OperationUnsupported,
914 /// Windows-only. The path is invalid WTF-8.
915 /// https://wtf-8.codeberg.page/
916 InvalidWtf8,
917} || Io.Cancelable || Io.UnexpectedError;
918
919/// Changes the current working directory to the given path.
920/// Corresponds to "chdir" in libc.
921///
922/// This modifies global process state and can have surprising effects in
923/// multithreaded applications. Most applications and especially libraries
924/// should not call this function as a general rule, however it can have use
925/// cases in, for example, implementing a shell, or child process execution.
926///
927/// Calling this function makes code less portable and less reusable.
928pub fn setCurrentPath(io: Io, path: []const u8) !void {
929 return io.vtable.processSetCurrentPath(io.userdata, path);
930}
931
903pub const LockMemoryError = error{932pub const LockMemoryError = error{
904 UnsupportedOperation,933 UnsupportedOperation,
905 PermissionDenied,934 PermissionDenied,
test/standalone/posix/cwd.zig+3-3
...@@ -37,7 +37,7 @@ fn test_chdir_self(io: Io) !void {...@@ -37,7 +37,7 @@ fn test_chdir_self(io: Io) !void {
37 const old_cwd = old_cwd_buf[0..try std.process.currentPath(io, &old_cwd_buf)];37 const old_cwd = old_cwd_buf[0..try std.process.currentPath(io, &old_cwd_buf)];
3838
39 // Try changing to the current directory39 // Try changing to the current directory
40 try std.Io.Threaded.chdir(old_cwd);40 try std.process.setCurrentPath(io, old_cwd);
41 try expect_cwd(io, old_cwd);41 try expect_cwd(io, old_cwd);
42}42}
4343
...@@ -48,7 +48,7 @@ fn test_chdir_absolute(io: Io) !void {...@@ -48,7 +48,7 @@ fn test_chdir_absolute(io: Io) !void {
48 const parent = std.fs.path.dirname(old_cwd) orelse unreachable; // old_cwd should be absolute48 const parent = std.fs.path.dirname(old_cwd) orelse unreachable; // old_cwd should be absolute
4949
50 // Try changing to the parent via a full path50 // Try changing to the parent via a full path
51 try std.Io.Threaded.chdir(parent);51 try std.process.setCurrentPath(io, parent);
5252
53 try expect_cwd(io, parent);53 try expect_cwd(io, parent);
54}54}
...@@ -68,7 +68,7 @@ fn test_chdir_relative(gpa: Allocator, io: Io, tmp_dir: Io.Dir) !void {...@@ -68,7 +68,7 @@ fn test_chdir_relative(gpa: Allocator, io: Io, tmp_dir: Io.Dir) !void {
68 defer gpa.free(expected_path);68 defer gpa.free(expected_path);
6969
70 // change current working directory to new test directory70 // change current working directory to new test directory
71 try std.Io.Threaded.chdir(subdir_path);71 try std.process.setCurrentPath(io, subdir_path);
7272
73 var new_cwd_buf: [path_max]u8 = undefined;73 var new_cwd_buf: [path_max]u8 = undefined;
74 const new_cwd = new_cwd_buf[0..try std.process.currentPath(io, &new_cwd_buf)];74 const new_cwd = new_cwd_buf[0..try std.process.currentPath(io, &new_cwd_buf)];
test/standalone/windows_spawn/main.zig+1-3
...@@ -9,8 +9,6 @@ pub fn main(init: std.process.Init) !void {...@@ -9,8 +9,6 @@ pub fn main(init: std.process.Init) !void {
9 const gpa = init.gpa;9 const gpa = init.gpa;
10 const io = init.io;10 const io = init.io;
11 const process_cwd_path = try std.process.currentPathAlloc(io, init.arena.allocator());11 const process_cwd_path = try std.process.currentPathAlloc(io, init.arena.allocator());
12 var initial_process_cwd = try Io.Dir.cwd().openDir(io, ".", .{});
13 defer initial_process_cwd.close(io);
1412
15 var it = try init.minimal.args.iterateAllocator(gpa);13 var it = try init.minimal.args.iterateAllocator(gpa);
16 defer it.deinit();14 defer it.deinit();
...@@ -127,7 +125,7 @@ pub fn main(init: std.process.Init) !void {...@@ -127,7 +125,7 @@ pub fn main(init: std.process.Init) !void {
127125
128 // Now let's set the tmp dir as the cwd and set the path only include the "something" sub dir126 // Now let's set the tmp dir as the cwd and set the path only include the "something" sub dir
129 try std.process.setCurrentDir(io, tmp_dir);127 try std.process.setCurrentDir(io, tmp_dir);
130 defer std.process.setCurrentDir(io, initial_process_cwd) catch {};128 defer std.process.setCurrentPath(io, process_cwd_path) catch {};
131 const something_subdir_abs_path = try std.mem.concatWithSentinel(gpa, u16, &.{ tmp_absolute_path_w, utf16Literal("\\something") }, 0);129 const something_subdir_abs_path = try std.mem.concatWithSentinel(gpa, u16, &.{ tmp_absolute_path_w, utf16Literal("\\something") }, 0);
132 defer gpa.free(something_subdir_abs_path);130 defer gpa.free(something_subdir_abs_path);
133131