From 66e76a0209586000a78fe896071e73202a80b81f Mon Sep 17 00:00:00 2001 From: Andrew Kelley Date: Fri, 13 Mar 2020 21:06:07 -0400 Subject: [PATCH] zig build system: correctly handle multiple output artifacts Previously the zig build system incorrectly assumed that the only build artifact was a binary. Now, when you enable the cache, only the output dir is printed to stdout, and the zig build system iterates over the files in that directory, copying them to the output directory. To support this change: * Add `std.os.renameat`, `std.os.renameatZ`, and `std.os.renameatW`. * Fix `std.os.linux.renameat` not compiling due to typos. * Deprecate `std.fs.updateFile` and `std.fs.updateFileMode`. * Add `std.fs.Dir.updateFile`, which supports using open directory handles for both the source and destination paths, as well as an options parameter which allows overriding the mode. * Update `std.fs.AtomicFile` to support operating based on an open directory handle. Instead of `std.fs.AtomicFile.init`, use `std.fs.Dir.atomicFile`. * `std.fs.AtomicFile` deinit() better handles the situation when the rename fails but the temporary file still exists, by still attempting to remove the temporary file. * `std.fs.Dir.openFileWindows` is moved to `std.os.windows.OpenFileW`. * `std.os.RenameError` gains the error codes `NoDevice`, `SharingViolation`, and `PipeBusy` which have been observed from Windows. Closes #4733 --- lib/std/build.zig | 21 +-- lib/std/c.zig | 1 + lib/std/fs.zig | 261 ++++++++++++++++-------------------- lib/std/os.zig | 114 +++++++++++++++- lib/std/os/linux.zig | 8 +- lib/std/os/windows.zig | 76 +++++++++++ lib/std/os/windows/bits.zig | 7 + src/main.cpp | 3 +- 8 files changed, 333 insertions(+), 158 deletions(-) diff --git a/lib/std/build.zig b/lib/std/build.zig index e8484e9d1..8fb5eaeeb 100644 --- a/lib/std/build.zig +++ b/lib/std/build.zig @@ -2144,17 +2144,22 @@ pub const LibExeObjStep = struct { try zig_args.append("--cache"); try zig_args.append("on"); - const output_path_nl = try builder.execFromStep(zig_args.toSliceConst(), &self.step); - const output_path = mem.trimRight(u8, output_path_nl, "\r\n"); + const output_dir_nl = try builder.execFromStep(zig_args.toSliceConst(), &self.step); + const build_output_dir = mem.trimRight(u8, output_dir_nl, "\r\n"); if (self.output_dir) |output_dir| { - const full_dest = try fs.path.join(builder.allocator, &[_][]const u8{ - output_dir, - fs.path.basename(output_path), - }); - try builder.updateFile(output_path, full_dest); + var src_dir = try std.fs.cwd().openDirTraverse(build_output_dir); + defer src_dir.close(); + + var dest_dir = try std.fs.cwd().openDirList(output_dir); + defer dest_dir.close(); + + var it = src_dir.iterate(); + while (try it.next()) |entry| { + _ = try src_dir.updateFile(entry.name, dest_dir, entry.name, .{}); + } } else { - self.output_dir = fs.path.dirname(output_path).?; + self.output_dir = build_output_dir; } } diff --git a/lib/std/c.zig b/lib/std/c.zig index 39a865ebb..43b3d7f31 100644 --- a/lib/std/c.zig +++ b/lib/std/c.zig @@ -106,6 +106,7 @@ pub extern "c" fn mkdir(path: [*:0]const u8, mode: c_uint) c_int; pub extern "c" fn mkdirat(dirfd: fd_t, path: [*:0]const u8, mode: u32) c_int; pub extern "c" fn symlink(existing: [*:0]const u8, new: [*:0]const u8) c_int; pub extern "c" fn rename(old: [*:0]const u8, new: [*:0]const u8) c_int; +pub extern "c" fn renameat(olddirfd: fd_t, old: [*:0]const u8, newdirfd: fd_t, new: [*:0]const u8) c_int; pub extern "c" fn chdir(path: [*:0]const u8) c_int; pub extern "c" fn fchdir(fd: fd_t) c_int; pub extern "c" fn execve(path: [*:0]const u8, argv: [*:null]const ?[*:0]const u8, envp: [*:null]const ?[*:0]const u8) c_int; diff --git a/lib/std/fs.zig b/lib/std/fs.zig index de6be91f7..231235baf 100644 --- a/lib/std/fs.zig +++ b/lib/std/fs.zig @@ -81,60 +81,21 @@ pub fn atomicSymLink(allocator: *Allocator, existing_path: []const u8, new_path: } } -// TODO fix enum literal not casting to error union -const PrevStatus = enum { +pub const PrevStatus = enum { stale, fresh, }; +/// Deprecated; use `Dir.updateFile`. pub fn updateFile(source_path: []const u8, dest_path: []const u8) !PrevStatus { - return updateFileMode(source_path, dest_path, null); + const my_cwd = cwd(); + return Dir.updateFile(my_cwd, source_path, my_cwd, dest_path, .{}); } -/// Check the file size, mtime, and mode of `source_path` and `dest_path`. If they are equal, does nothing. -/// Otherwise, atomically copies `source_path` to `dest_path`. The destination file gains the mtime, -/// atime, and mode of the source file so that the next call to `updateFile` will not need a copy. -/// Returns the previous status of the file before updating. -/// If any of the directories do not exist for dest_path, they are created. -/// TODO rework this to integrate with Dir -pub fn updateFileMode(source_path: []const u8, dest_path: []const u8, mode: ?File.Mode) !PrevStatus { +/// Deprecated; use `Dir.updateFile`. +pub fn updateFileMode(source_path: []const u8, dest_path: []const u8, mode: ?File.Mode) !Dir.PrevStatus { const my_cwd = cwd(); - - var src_file = try my_cwd.openFile(source_path, .{}); - defer src_file.close(); - - const src_stat = try src_file.stat(); - check_dest_stat: { - const dest_stat = blk: { - var dest_file = my_cwd.openFile(dest_path, .{}) catch |err| switch (err) { - error.FileNotFound => break :check_dest_stat, - else => |e| return e, - }; - defer dest_file.close(); - - break :blk try dest_file.stat(); - }; - - if (src_stat.size == dest_stat.size and - src_stat.mtime == dest_stat.mtime and - src_stat.mode == dest_stat.mode) - { - return PrevStatus.fresh; - } - } - const actual_mode = mode orelse src_stat.mode; - - if (path.dirname(dest_path)) |dirname| { - try cwd().makePath(dirname); - } - - var atomic_file = try AtomicFile.init(dest_path, actual_mode); - defer atomic_file.deinit(); - - try atomic_file.file.writeFileAll(src_file, .{ .in_len = src_stat.size }); - try atomic_file.file.updateTimes(src_stat.atime, src_stat.mtime); - try atomic_file.finish(); - return PrevStatus.stale; + return Dir.updateFile(my_cwd, source_path, my_cwd, dest_path, .{ .override_mode = mode }); } /// Guaranteed to be atomic. @@ -172,43 +133,40 @@ pub fn copyFileMode(source_path: []const u8, dest_path: []const u8, mode: File.M return atomic_file.finish(); } -/// TODO update this API to avoid a getrandom syscall for every operation. It -/// should accept a random interface. -/// TODO rework this to integrate with Dir +/// TODO update this API to avoid a getrandom syscall for every operation. pub const AtomicFile = struct { file: File, - tmp_path_buf: [MAX_PATH_BYTES]u8, + tmp_path_buf: [MAX_PATH_BYTES - 1:0]u8, dest_path: []const u8, - finished: bool, + file_open: bool, + file_exists: bool, + dir: Dir, const InitError = File.OpenError; - /// dest_path must remain valid for the lifetime of AtomicFile - /// call finish to atomically replace dest_path with contents - pub fn init(dest_path: []const u8, mode: File.Mode) InitError!AtomicFile { + /// TODO rename this. Callers should go through Dir API + pub fn init2(dest_path: []const u8, mode: File.Mode, dir: Dir) InitError!AtomicFile { const dirname = path.dirname(dest_path); var rand_buf: [12]u8 = undefined; const dirname_component_len = if (dirname) |d| d.len + 1 else 0; const encoded_rand_len = comptime base64.Base64Encoder.calcSize(rand_buf.len); const tmp_path_len = dirname_component_len + encoded_rand_len; - var tmp_path_buf: [MAX_PATH_BYTES]u8 = undefined; - if (tmp_path_len >= tmp_path_buf.len) return error.NameTooLong; + var tmp_path_buf: [MAX_PATH_BYTES - 1:0]u8 = undefined; + if (tmp_path_len > tmp_path_buf.len) return error.NameTooLong; - if (dirname) |dir| { - mem.copy(u8, tmp_path_buf[0..], dir); - tmp_path_buf[dir.len] = path.sep; + if (dirname) |dn| { + mem.copy(u8, tmp_path_buf[0..], dn); + tmp_path_buf[dn.len] = path.sep; } tmp_path_buf[tmp_path_len] = 0; const tmp_path_slice = tmp_path_buf[0..tmp_path_len :0]; - const my_cwd = cwd(); - while (true) { try crypto.randomBytes(rand_buf[0..]); base64_encoder.encode(tmp_path_slice[dirname_component_len..tmp_path_len], &rand_buf); - const file = my_cwd.createFileC( + const file = dir.createFileC( tmp_path_slice, .{ .mode = mode, .exclusive = true }, ) catch |err| switch (err) { @@ -220,33 +178,46 @@ pub const AtomicFile = struct { .file = file, .tmp_path_buf = tmp_path_buf, .dest_path = dest_path, - .finished = false, + .file_open = true, + .file_exists = true, + .dir = dir, }; } } + /// Deprecated. Use `Dir.atomicFile`. + pub fn init(dest_path: []const u8, mode: File.Mode) InitError!AtomicFile { + return init2(dest_path, mode, cwd()); + } + /// always call deinit, even after successful finish() pub fn deinit(self: *AtomicFile) void { - if (!self.finished) { + if (self.file_open) { self.file.close(); - cwd().deleteFileC(@ptrCast([*:0]u8, &self.tmp_path_buf)) catch {}; - self.finished = true; + self.file_open = false; } + if (self.file_exists) { + self.dir.deleteFileC(&self.tmp_path_buf) catch {}; + self.file_exists = false; + } + self.* = undefined; } pub fn finish(self: *AtomicFile) !void { - assert(!self.finished); + assert(self.file_exists); + if (self.file_open) { + self.file.close(); + self.file_open = false; + } if (std.Target.current.os.tag == .windows) { const dest_path_w = try os.windows.sliceToPrefixedFileW(self.dest_path); - const tmp_path_w = try os.windows.cStrToPrefixedFileW(@ptrCast([*:0]u8, &self.tmp_path_buf)); - self.file.close(); - self.finished = true; - return os.renameW(&tmp_path_w, &dest_path_w); + const tmp_path_w = try os.windows.cStrToPrefixedFileW(&self.tmp_path_buf); + try os.renameatW(self.dir.fd, &tmp_path_w, self.dir.fd, &dest_path_w, os.windows.TRUE); + self.file_exists = false; } else { const dest_path_c = try os.toPosixPath(self.dest_path); - self.file.close(); - self.finished = true; - return os.renameC(@ptrCast([*:0]u8, &self.tmp_path_buf), &dest_path_c); + try os.renameatZ(self.dir.fd, &self.tmp_path_buf, self.dir.fd, &dest_path_c); + self.file_exists = false; } } }; @@ -694,7 +665,10 @@ pub const Dir = struct { const access_mask = w.SYNCHRONIZE | (if (flags.read) @as(u32, w.GENERIC_READ) else 0) | (if (flags.write) @as(u32, w.GENERIC_WRITE) else 0); - return self.openFileWindows(sub_path_w, access_mask, w.FILE_OPEN); + return @as(File, .{ + .handle = try os.windows.OpenFileW(self.fd, sub_path_w, null, access_mask, w.FILE_OPEN), + .io_mode = .blocking, + }); } /// Creates, opens, or overwrites a file with write access. @@ -739,7 +713,10 @@ pub const Dir = struct { @as(u32, w.FILE_OVERWRITE_IF) else @as(u32, w.FILE_OPEN_IF); - return self.openFileWindows(sub_path_w, access_mask, creation); + return @as(File, .{ + .handle = try os.windows.OpenFileW(self.fd, sub_path_w, null, access_mask, creation), + .io_mode = .blocking, + }); } /// Deprecated; call `openFile` directly. @@ -757,72 +734,6 @@ pub const Dir = struct { return self.openFileW(sub_path, .{}); } - pub fn openFileWindows( - self: Dir, - sub_path_w: [*:0]const u16, - access_mask: os.windows.ACCESS_MASK, - creation: os.windows.ULONG, - ) File.OpenError!File { - const w = os.windows; - - if (sub_path_w[0] == '.' and sub_path_w[1] == 0) { - return error.IsDir; - } - if (sub_path_w[0] == '.' and sub_path_w[1] == '.' and sub_path_w[2] == 0) { - return error.IsDir; - } - - var result = File{ - .handle = undefined, - .io_mode = .blocking, - }; - - const path_len_bytes = math.cast(u16, mem.toSliceConst(u16, sub_path_w).len * 2) catch |err| switch (err) { - error.Overflow => return error.NameTooLong, - }; - var nt_name = w.UNICODE_STRING{ - .Length = path_len_bytes, - .MaximumLength = path_len_bytes, - .Buffer = @intToPtr([*]u16, @ptrToInt(sub_path_w)), - }; - var attr = w.OBJECT_ATTRIBUTES{ - .Length = @sizeOf(w.OBJECT_ATTRIBUTES), - .RootDirectory = if (path.isAbsoluteWindowsW(sub_path_w)) null else self.fd, - .Attributes = 0, // Note we do not use OBJ_CASE_INSENSITIVE here. - .ObjectName = &nt_name, - .SecurityDescriptor = null, - .SecurityQualityOfService = null, - }; - var io: w.IO_STATUS_BLOCK = undefined; - const rc = w.ntdll.NtCreateFile( - &result.handle, - access_mask, - &attr, - &io, - null, - w.FILE_ATTRIBUTE_NORMAL, - w.FILE_SHARE_WRITE | w.FILE_SHARE_READ | w.FILE_SHARE_DELETE, - creation, - w.FILE_NON_DIRECTORY_FILE | w.FILE_SYNCHRONOUS_IO_NONALERT, - null, - 0, - ); - switch (rc) { - .SUCCESS => return result, - .OBJECT_NAME_INVALID => unreachable, - .OBJECT_NAME_NOT_FOUND => return error.FileNotFound, - .OBJECT_PATH_NOT_FOUND => return error.FileNotFound, - .NO_MEDIA_IN_DEVICE => return error.NoDevice, - .INVALID_PARAMETER => unreachable, - .SHARING_VIOLATION => return error.SharingViolation, - .ACCESS_DENIED => return error.AccessDenied, - .PIPE_BUSY => return error.PipeBusy, - .OBJECT_PATH_SYNTAX_BAD => unreachable, - .OBJECT_NAME_COLLISION => return error.PathAlreadyExists, - else => return w.unexpectedStatus(rc), - } - } - pub fn makeDir(self: Dir, sub_path: []const u8) !void { try os.mkdirat(self.fd, sub_path, default_new_dir_mode); } @@ -898,6 +809,7 @@ pub const Dir = struct { /// Call `close` on the result when done. /// /// Asserts that the path parameter has no null bytes. + /// TODO collapse this and `openDirList` into one function with an options parameter pub fn openDirTraverse(self: Dir, sub_path: []const u8) OpenError!Dir { if (builtin.os.tag == .windows) { const sub_path_w = try os.windows.sliceToPrefixedFileW(sub_path); @@ -915,6 +827,7 @@ pub const Dir = struct { /// Call `close` on the result when done. /// /// Asserts that the path parameter has no null bytes. + /// TODO collapse this and `openDirTraverse` into one function with an options parameter pub fn openDirList(self: Dir, sub_path: []const u8) OpenError!Dir { if (builtin.os.tag == .windows) { const sub_path_w = try os.windows.sliceToPrefixedFileW(sub_path); @@ -1370,6 +1283,70 @@ pub const Dir = struct { pub fn accessW(self: Dir, sub_path_w: [*:0]const u16, flags: File.OpenFlags) AccessError!void { return os.faccessatW(self.fd, sub_path_w, 0, 0); } + + pub const UpdateFileOptions = struct { + override_mode: ?File.Mode = null, + }; + + /// Check the file size, mtime, and mode of `source_path` and `dest_path`. If they are equal, does nothing. + /// Otherwise, atomically copies `source_path` to `dest_path`. The destination file gains the mtime, + /// atime, and mode of the source file so that the next call to `updateFile` will not need a copy. + /// Returns the previous status of the file before updating. + /// If any of the directories do not exist for dest_path, they are created. + /// If `override_mode` is provided, then that value is used rather than the source path's mode. + pub fn updateFile( + source_dir: Dir, + source_path: []const u8, + dest_dir: Dir, + dest_path: []const u8, + options: UpdateFileOptions, + ) !PrevStatus { + var src_file = try source_dir.openFile(source_path, .{}); + defer src_file.close(); + + const src_stat = try src_file.stat(); + const actual_mode = options.override_mode orelse src_stat.mode; + check_dest_stat: { + const dest_stat = blk: { + var dest_file = dest_dir.openFile(dest_path, .{}) catch |err| switch (err) { + error.FileNotFound => break :check_dest_stat, + else => |e| return e, + }; + defer dest_file.close(); + + break :blk try dest_file.stat(); + }; + + if (src_stat.size == dest_stat.size and + src_stat.mtime == dest_stat.mtime and + actual_mode == dest_stat.mode) + { + return PrevStatus.fresh; + } + } + + if (path.dirname(dest_path)) |dirname| { + try dest_dir.makePath(dirname); + } + + var atomic_file = try dest_dir.atomicFile(dest_path, .{ .mode = actual_mode }); + defer atomic_file.deinit(); + + try atomic_file.file.writeFileAll(src_file, .{ .in_len = src_stat.size }); + try atomic_file.file.updateTimes(src_stat.atime, src_stat.mtime); + try atomic_file.finish(); + return PrevStatus.stale; + } + + pub const AtomicFileOptions = struct { + mode: File.Mode = File.default_mode, + }; + + /// `dest_path` must remain valid for the lifetime of `AtomicFile`. + /// Call `AtomicFile.finish` to atomically replace `dest_path` with contents. + pub fn atomicFile(self: Dir, dest_path: []const u8, options: AtomicFileOptions) !AtomicFile { + return AtomicFile.init2(dest_path, options.mode, self); + } }; /// Returns an handle to the current working directory that is open for traversal. diff --git a/lib/std/os.zig b/lib/std/os.zig index ed99c4802..0186e1b0b 100644 --- a/lib/std/os.zig +++ b/lib/std/os.zig @@ -461,13 +461,11 @@ pub fn ftruncate(fd: fd_t, length: u64) TruncateError!void { ); switch (rc) { - .SUCCESS => {}, + .SUCCESS => return, .INVALID_HANDLE => unreachable, // Handle not open for writing .ACCESS_DENIED => return error.CannotTruncate, else => return windows.unexpectedStatus(rc), } - - return; } while (true) { @@ -852,6 +850,7 @@ pub const OpenError = error{ /// Open and possibly create a file. Keeps trying if it gets interrupted. /// See also `openC`. +/// TODO support windows pub fn open(file_path: []const u8, flags: u32, perm: usize) OpenError!fd_t { const file_path_c = try toPosixPath(file_path); return openC(&file_path_c, flags, perm); @@ -859,6 +858,7 @@ pub fn open(file_path: []const u8, flags: u32, perm: usize) OpenError!fd_t { /// Open and possibly create a file. Keeps trying if it gets interrupted. /// See also `open`. +/// TODO support windows pub fn openC(file_path: [*:0]const u8, flags: u32, perm: usize) OpenError!fd_t { while (true) { const rc = system.open(file_path, flags, perm); @@ -892,6 +892,7 @@ pub fn openC(file_path: [*:0]const u8, flags: u32, perm: usize) OpenError!fd_t { /// Open and possibly create a file. Keeps trying if it gets interrupted. /// `file_path` is relative to the open directory handle `dir_fd`. /// See also `openatC`. +/// TODO support windows pub fn openat(dir_fd: fd_t, file_path: []const u8, flags: u32, mode: mode_t) OpenError!fd_t { const file_path_c = try toPosixPath(file_path); return openatC(dir_fd, &file_path_c, flags, mode); @@ -900,6 +901,7 @@ pub fn openat(dir_fd: fd_t, file_path: []const u8, flags: u32, mode: mode_t) Ope /// Open and possibly create a file. Keeps trying if it gets interrupted. /// `file_path` is relative to the open directory handle `dir_fd`. /// See also `openat`. +/// TODO support windows pub fn openatC(dir_fd: fd_t, file_path: [*:0]const u8, flags: u32, mode: mode_t) OpenError!fd_t { while (true) { const rc = system.openat(dir_fd, file_path, flags, mode); @@ -1527,6 +1529,9 @@ const RenameError = error{ RenameAcrossMountPoints, InvalidUtf8, BadPathName, + NoDevice, + SharingViolation, + PipeBusy, } || UnexpectedError; /// Change the name or location of a file. @@ -1580,6 +1585,108 @@ pub fn renameW(old_path: [*:0]const u16, new_path: [*:0]const u16) RenameError!v return windows.MoveFileExW(old_path, new_path, flags); } +/// Change the name or location of a file based on an open directory handle. +pub fn renameat( + old_dir_fd: fd_t, + old_path: []const u8, + new_dir_fd: fd_t, + new_path: []const u8, +) RenameError!void { + if (builtin.os.tag == .windows) { + const old_path_w = try windows.sliceToPrefixedFileW(old_path); + const new_path_w = try windows.sliceToPrefixedFileW(new_path); + return renameatW(old_dir_fd, &old_path_w, new_dir_fd, &new_path_w, windows.TRUE); + } else { + const old_path_c = try toPosixPath(old_path); + const new_path_c = try toPosixPath(new_path); + return renameatZ(old_dir_fd, &old_path_c, new_dir_fd, &new_path_c); + } +} + +/// Same as `renameat` except the parameters are null-terminated byte arrays. +pub fn renameatZ( + old_dir_fd: fd_t, + old_path: [*:0]const u8, + new_dir_fd: fd_t, + new_path: [*:0]const u8, +) RenameError!void { + if (builtin.os.tag == .windows) { + const old_path_w = try windows.cStrToPrefixedFileW(old_path); + const new_path_w = try windows.cStrToPrefixedFileW(new_path); + return renameatW(old_dir_fd, &old_path_w, new_dir_fd, &new_path_w, windows.TRUE); + } + + switch (errno(system.renameat(old_dir_fd, old_path, new_dir_fd, new_path))) { + 0 => return, + EACCES => return error.AccessDenied, + EPERM => return error.AccessDenied, + EBUSY => return error.FileBusy, + EDQUOT => return error.DiskQuota, + EFAULT => unreachable, + EINVAL => unreachable, + EISDIR => return error.IsDir, + ELOOP => return error.SymLinkLoop, + EMLINK => return error.LinkQuotaExceeded, + ENAMETOOLONG => return error.NameTooLong, + ENOENT => return error.FileNotFound, + ENOTDIR => return error.NotDir, + ENOMEM => return error.SystemResources, + ENOSPC => return error.NoSpaceLeft, + EEXIST => return error.PathAlreadyExists, + ENOTEMPTY => return error.PathAlreadyExists, + EROFS => return error.ReadOnlyFileSystem, + EXDEV => return error.RenameAcrossMountPoints, + else => |err| return unexpectedErrno(err), + } +} + +/// Same as `renameat` except the parameters are null-terminated UTF16LE encoded byte arrays. +/// Assumes target is Windows. +/// TODO these args can actually be slices when using ntdll. audit the rest of the W functions too. +pub fn renameatW( + old_dir_fd: fd_t, + old_path: [*:0]const u16, + new_dir_fd: fd_t, + new_path_w: [*:0]const u16, + ReplaceIfExists: windows.BOOLEAN, +) RenameError!void { + const access_mask = windows.SYNCHRONIZE | windows.GENERIC_WRITE; + const src_fd = try windows.OpenFileW(old_dir_fd, old_path, null, access_mask, windows.FILE_OPEN); + defer windows.CloseHandle(src_fd); + + const struct_buf_len = @sizeOf(windows.FILE_RENAME_INFORMATION) + (MAX_PATH_BYTES - 1); + var rename_info_buf: [struct_buf_len]u8 align(@alignOf(windows.FILE_RENAME_INFORMATION)) = undefined; + const new_path = mem.span(new_path_w); + const struct_len = @sizeOf(windows.FILE_RENAME_INFORMATION) - 1 + new_path.len * 2; + if (struct_len > struct_buf_len) return error.NameTooLong; + + const rename_info = @ptrCast(*windows.FILE_RENAME_INFORMATION, &rename_info_buf); + + rename_info.* = .{ + .ReplaceIfExists = ReplaceIfExists, + .RootDirectory = if (std.fs.path.isAbsoluteWindowsW(new_path_w)) null else new_dir_fd, + .FileNameLength = @intCast(u32, new_path.len * 2), // already checked error.NameTooLong + .FileName = undefined, + }; + std.mem.copy(u16, @as([*]u16, &rename_info.FileName)[0..new_path.len], new_path); + + var io_status_block: windows.IO_STATUS_BLOCK = undefined; + + const rc = windows.ntdll.NtSetInformationFile( + src_fd, + &io_status_block, + rename_info, + @intCast(u32, struct_len), // already checked for error.NameTooLong + .FileRenameInformation, + ); + + switch (rc) { + .SUCCESS => return, + .INVALID_HANDLE => unreachable, + else => return windows.unexpectedStatus(rc), + } +} + pub const MakeDirError = error{ AccessDenied, DiskQuota, @@ -3125,6 +3232,7 @@ pub fn realpathC(pathname: [*:0]const u8, out_buffer: *[MAX_PATH_BYTES]u8) RealP } /// Same as `realpath` except `pathname` is null-terminated and UTF16LE-encoded. +/// TODO use ntdll for better semantics pub fn realpathW(pathname: [*:0]const u16, out_buffer: *[MAX_PATH_BYTES]u8) RealPathError![]u8 { const h_file = try windows.CreateFileW( pathname, diff --git a/lib/std/os/linux.zig b/lib/std/os/linux.zig index ba7356d62..f3265eaa6 100644 --- a/lib/std/os/linux.zig +++ b/lib/std/os/linux.zig @@ -465,17 +465,17 @@ pub fn renameat(oldfd: i32, oldpath: [*]const u8, newfd: i32, newpath: [*]const return syscall4( SYS_renameat, @bitCast(usize, @as(isize, oldfd)), - @ptrToInt(old), + @ptrToInt(oldpath), @bitCast(usize, @as(isize, newfd)), - @ptrToInt(new), + @ptrToInt(newpath), ); } else { return syscall5( SYS_renameat2, @bitCast(usize, @as(isize, oldfd)), - @ptrToInt(old), + @ptrToInt(oldpath), @bitCast(usize, @as(isize, newfd)), - @ptrToInt(new), + @ptrToInt(newpath), 0, ); } diff --git a/lib/std/os/windows.zig b/lib/std/os/windows.zig index 6c9a1f24b..f5acb70ad 100644 --- a/lib/std/os/windows.zig +++ b/lib/std/os/windows.zig @@ -88,6 +88,82 @@ pub fn CreateFileW( return result; } +pub const OpenError = error{ + IsDir, + FileNotFound, + NoDevice, + SharingViolation, + AccessDenied, + PipeBusy, + PathAlreadyExists, + Unexpected, + NameTooLong, +}; + +/// TODO rename to CreateFileW +/// TODO actually we don't need the path parameter to be null terminated +pub fn OpenFileW( + dir: ?HANDLE, + sub_path_w: [*:0]const u16, + sa: ?*SECURITY_ATTRIBUTES, + access_mask: ACCESS_MASK, + creation: ULONG, +) OpenError!HANDLE { + if (sub_path_w[0] == '.' and sub_path_w[1] == 0) { + return error.IsDir; + } + if (sub_path_w[0] == '.' and sub_path_w[1] == '.' and sub_path_w[2] == 0) { + return error.IsDir; + } + + var result: HANDLE = undefined; + + const path_len_bytes = math.cast(u16, mem.toSliceConst(u16, sub_path_w).len * 2) catch |err| switch (err) { + error.Overflow => return error.NameTooLong, + }; + var nt_name = UNICODE_STRING{ + .Length = path_len_bytes, + .MaximumLength = path_len_bytes, + .Buffer = @intToPtr([*]u16, @ptrToInt(sub_path_w)), + }; + var attr = OBJECT_ATTRIBUTES{ + .Length = @sizeOf(OBJECT_ATTRIBUTES), + .RootDirectory = if (std.fs.path.isAbsoluteWindowsW(sub_path_w)) null else dir, + .Attributes = 0, // Note we do not use OBJ_CASE_INSENSITIVE here. + .ObjectName = &nt_name, + .SecurityDescriptor = if (sa) |ptr| ptr.lpSecurityDescriptor else null, + .SecurityQualityOfService = null, + }; + var io: IO_STATUS_BLOCK = undefined; + const rc = ntdll.NtCreateFile( + &result, + access_mask, + &attr, + &io, + null, + FILE_ATTRIBUTE_NORMAL, + FILE_SHARE_WRITE | FILE_SHARE_READ | FILE_SHARE_DELETE, + creation, + FILE_NON_DIRECTORY_FILE | FILE_SYNCHRONOUS_IO_NONALERT, + null, + 0, + ); + switch (rc) { + .SUCCESS => return result, + .OBJECT_NAME_INVALID => unreachable, + .OBJECT_NAME_NOT_FOUND => return error.FileNotFound, + .OBJECT_PATH_NOT_FOUND => return error.FileNotFound, + .NO_MEDIA_IN_DEVICE => return error.NoDevice, + .INVALID_PARAMETER => unreachable, + .SHARING_VIOLATION => return error.SharingViolation, + .ACCESS_DENIED => return error.AccessDenied, + .PIPE_BUSY => return error.PipeBusy, + .OBJECT_PATH_SYNTAX_BAD => unreachable, + .OBJECT_NAME_COLLISION => return error.PathAlreadyExists, + else => return unexpectedStatus(rc), + } +} + pub const CreatePipeError = error{Unexpected}; pub fn CreatePipe(rd: *HANDLE, wr: *HANDLE, sattr: *const SECURITY_ATTRIBUTES) CreatePipeError!void { diff --git a/lib/std/os/windows/bits.zig b/lib/std/os/windows/bits.zig index 1e49b903c..16cc6d744 100644 --- a/lib/std/os/windows/bits.zig +++ b/lib/std/os/windows/bits.zig @@ -242,6 +242,13 @@ pub const FILE_NAME_INFORMATION = extern struct { FileName: [1]WCHAR, }; +pub const FILE_RENAME_INFORMATION = extern struct { + ReplaceIfExists: BOOLEAN, + RootDirectory: ?HANDLE, + FileNameLength: ULONG, + FileName: [1]WCHAR, +}; + pub const IO_STATUS_BLOCK = extern struct { // "DUMMYUNIONNAME" expands to "u" u: extern union { diff --git a/src/main.cpp b/src/main.cpp index 66138dd04..06d132da8 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -1290,6 +1290,7 @@ static int main0(int argc, char **argv) { if (g->enable_cache) { #if defined(ZIG_OS_WINDOWS) buf_replace(&g->bin_file_output_path, '/', '\\'); + buf_replace(g->output_dir, '/', '\\'); #endif if (final_output_dir_step != nullptr) { Buf *dest_basename = buf_alloc(); @@ -1303,7 +1304,7 @@ static int main0(int argc, char **argv) { return main_exit(root_progress_node, EXIT_FAILURE); } } else { - if (g->emit_bin && printf("%s\n", buf_ptr(&g->bin_file_output_path)) < 0) + if (printf("%s\n", buf_ptr(g->output_dir)) < 0) return main_exit(root_progress_node, EXIT_FAILURE); } }