commit c906f7d2e7d66bdc07fe8815d1fb2bb3d6664478 (tree)
parent 6ab1159e815fdc7ce11fa49235509a43da74f899
Author: Andrew Kelley <andrew@ziglang.org>
Date: Tue, 6 Jan 2026 05:32:16 +0100
Merge pull request 'std: rework atomic file / temp file API' (#30686) from std.Io.File.Atomic into master
Reviewed-on: https://codeberg.org/ziglang/zig/pulls/30686
Diffstat:
13 files changed, 450 insertions(+), 207 deletions(-)
diff --git a/lib/std/Io.zig b/lib/std/Io.zig
@@ -667,6 +667,7 @@ pub const VTable = struct {
dirStatFile: *const fn (?*anyopaque, Dir, []const u8, Dir.StatFileOptions) Dir.StatFileError!File.Stat,
dirAccess: *const fn (?*anyopaque, Dir, []const u8, Dir.AccessOptions) Dir.AccessError!void,
dirCreateFile: *const fn (?*anyopaque, Dir, []const u8, File.CreateFlags) File.OpenError!File,
+ dirCreateFileAtomic: *const fn (?*anyopaque, Dir, []const u8, Dir.CreateFileAtomicOptions) Dir.CreateFileAtomicError!File.Atomic,
dirOpenFile: *const fn (?*anyopaque, Dir, []const u8, File.OpenFlags) File.OpenError!File,
dirClose: *const fn (?*anyopaque, []const Dir) void,
dirRead: *const fn (?*anyopaque, *Dir.Reader, []Dir.Entry) Dir.Reader.Error!usize,
@@ -710,6 +711,7 @@ pub const VTable = struct {
fileUnlock: *const fn (?*anyopaque, File) void,
fileDowngradeLock: *const fn (?*anyopaque, File) File.DowngradeLockError!void,
fileRealPath: *const fn (?*anyopaque, File, out_buffer: []u8) File.RealPathError!usize,
+ fileHardLink: *const fn (?*anyopaque, File, Dir, []const u8, File.HardLinkOptions) File.HardLinkError!void,
processExecutableOpen: *const fn (?*anyopaque, File.OpenFlags) std.process.OpenExecutableError!File,
processExecutablePath: *const fn (?*anyopaque, buffer: []u8) std.process.ExecutablePathError!usize,
diff --git a/lib/std/Io/Dir.zig b/lib/std/Io/Dir.zig
@@ -454,7 +454,6 @@ pub const OpenError = error{
SystemFdQuotaExceeded,
NoDevice,
SystemResources,
- DeviceBusy,
/// On Windows, `\\server` or `\\server\share` was not found.
NetworkNotFound,
} || PathNameError || Io.Cancelable || Io.UnexpectedError;
@@ -598,30 +597,29 @@ pub fn updateFile(
}
}
- if (path.dirname(dest_path)) |dirname| {
- try dest_dir.createDirPath(io, dirname);
- }
-
- var buffer: [1000]u8 = undefined; // Used only when direct fd-to-fd is not available.
- var atomic_file = try dest_dir.atomicFile(io, dest_path, .{
+ var atomic_file = try dest_dir.createFileAtomic(io, dest_path, .{
.permissions = actual_permissions,
- .write_buffer = &buffer,
+ .make_path = true,
+ .replace = true,
});
- defer atomic_file.deinit();
+ defer atomic_file.deinit(io);
+
+ var buffer: [1024]u8 = undefined; // Used only when direct fd-to-fd is not available.
+ var file_writer = atomic_file.file.writer(io, &buffer);
var src_reader: File.Reader = .initSize(src_file, io, &.{}, src_stat.size);
- const dest_writer = &atomic_file.file_writer.interface;
+ const dest_writer = &file_writer.interface;
_ = dest_writer.sendFileAll(&src_reader, .unlimited) catch |err| switch (err) {
error.ReadFailed => return src_reader.err.?,
- error.WriteFailed => return atomic_file.file_writer.err.?,
+ error.WriteFailed => return file_writer.err.?,
};
- try atomic_file.flush();
- try atomic_file.file_writer.file.setTimestamps(io, .{
+ try file_writer.flush();
+ try file_writer.file.setTimestamps(io, .{
.access_timestamp = .init(src_stat.atime),
.modify_timestamp = .init(src_stat.mtime),
});
- try atomic_file.renameIntoPlace();
+ try atomic_file.replace(io);
return .stale;
}
@@ -995,27 +993,9 @@ pub fn renameAbsolute(old_path: []const u8, new_path: []const u8, io: Io) Rename
return io.vtable.dirRename(io.userdata, my_cwd, old_path, my_cwd, new_path);
}
-pub const HardLinkOptions = struct {
- follow_symlinks: bool = true,
-};
+pub const HardLinkOptions = File.HardLinkOptions;
-pub const HardLinkError = error{
- AccessDenied,
- PermissionDenied,
- DiskQuota,
- PathAlreadyExists,
- HardwareFailure,
- /// Either the OS or the filesystem does not support hard links.
- OperationUnsupported,
- SymLinkLoop,
- LinkQuotaExceeded,
- FileNotFound,
- SystemResources,
- NoSpaceLeft,
- ReadOnlyFileSystem,
- NotSameFileSystem,
- NotDir,
-} || Io.Cancelable || PathNameError || Io.UnexpectedError;
+pub const HardLinkError = File.HardLinkError;
pub fn hardLink(
old_dir: Dir,
@@ -1251,7 +1231,6 @@ pub const DeleteTreeError = error{
ReadOnlyFileSystem,
FileSystem,
FileBusy,
- DeviceBusy,
/// One of the path components was not a directory.
/// This error is unreachable if `sub_path` does not contain a path separator.
NotDir,
@@ -1322,7 +1301,6 @@ pub fn deleteTree(dir: Dir, io: Io, sub_path: []const u8) DeleteTreeError!void {
error.Unexpected,
error.BadPathName,
error.NetworkNotFound,
- error.DeviceBusy,
error.Canceled,
=> |e| return e,
};
@@ -1417,7 +1395,6 @@ pub fn deleteTree(dir: Dir, io: Io, sub_path: []const u8) DeleteTreeError!void {
error.Unexpected,
error.BadPathName,
error.NetworkNotFound,
- error.DeviceBusy,
error.Canceled,
=> |e| return e,
};
@@ -1522,7 +1499,6 @@ fn deleteTreeMinStackSizeWithKindHint(parent: Dir, io: Io, sub_path: []const u8,
error.Unexpected,
error.BadPathName,
error.NetworkNotFound,
- error.DeviceBusy,
error.Canceled,
=> |e| return e,
};
@@ -1619,7 +1595,6 @@ fn deleteTreeOpenInitialSubpath(dir: Dir, io: Io, sub_path: []const u8, kind_hin
error.SystemResources,
error.Unexpected,
error.BadPathName,
- error.DeviceBusy,
error.NetworkNotFound,
error.Canceled,
=> |e| return e,
@@ -1658,15 +1633,18 @@ fn deleteTreeOpenInitialSubpath(dir: Dir, io: Io, sub_path: []const u8, kind_hin
pub const CopyFileOptions = struct {
/// When this is `null` the permissions are copied from the source file.
permissions: ?File.Permissions = null,
+ make_path: bool = false,
+ replace: bool = true,
};
pub const CopyFileError = File.OpenError || File.StatError ||
- File.Atomic.InitError || File.Atomic.FinishError ||
+ CreateFileAtomicError || File.Atomic.ReplaceError || File.Atomic.LinkError ||
File.Reader.Error || File.Writer.Error || error{InvalidFileName};
/// Atomically creates a new file at `dest_path` within `dest_dir` with the
-/// same contents as `source_path` within `source_dir`, overwriting any already
-/// existing file.
+/// same contents as `source_path` within `source_dir`.
+///
+/// Whether to overwrite the existing file is determined by `options`.
///
/// On Linux, until https://patchwork.kernel.org/patch/9636735/ is merged and
/// readily available, there is a possibility of power loss or application
@@ -1695,19 +1673,27 @@ pub fn copyFile(
break :blk st.permissions;
};
- var buffer: [1024]u8 = undefined; // Used only when direct fd-to-fd is not available.
- var atomic_file = try dest_dir.atomicFile(io, dest_path, .{
+ var atomic_file = try dest_dir.createFileAtomic(io, dest_path, .{
.permissions = permissions,
- .write_buffer = &buffer,
+ .make_path = options.make_path,
+ .replace = options.replace,
});
- defer atomic_file.deinit();
+ defer atomic_file.deinit(io);
- _ = atomic_file.file_writer.interface.sendFileAll(&file_reader, .unlimited) catch |err| switch (err) {
+ var buffer: [1024]u8 = undefined; // Used only when direct fd-to-fd is not available.
+ var file_writer = atomic_file.file.writer(io, &buffer);
+
+ _ = file_writer.interface.sendFileAll(&file_reader, .unlimited) catch |err| switch (err) {
error.ReadFailed => return file_reader.err.?,
- error.WriteFailed => return atomic_file.file_writer.err.?,
+ error.WriteFailed => return file_writer.err.?,
};
- try atomic_file.finish();
+ try file_writer.flush();
+
+ switch (options.replace) {
+ true => try atomic_file.replace(io),
+ false => try atomic_file.link(io),
+ }
}
/// Same as `copyFile`, except asserts that both `source_path` and `dest_path`
@@ -1730,33 +1716,65 @@ pub fn copyFileAbsolute(
test copyFileAbsolute {}
-pub const AtomicFileOptions = struct {
+pub const CreateFileAtomicOptions = struct {
permissions: File.Permissions = .default_file,
make_path: bool = false,
- write_buffer: []u8,
+ /// Tells whether the unnamed file will be ultimately created with
+ /// `File.Atomic.link` or `File.Atomic.replace`.
+ ///
+ /// If this value is incorrect it will cause an assertion failure in
+ /// `File.Atomic.replace`.
+ replace: bool = false,
};
-/// Directly access the `.file` field, and then call `File.Atomic.finish` to
-/// atomically replace `dest_path` with contents.
-///
-/// Always call `File.Atomic.deinit` to clean up, regardless of whether
-/// `File.Atomic.finish` succeeded. `dest_path` must remain valid until
-/// `File.Atomic.deinit` is called.
-///
-/// On Windows, `dest_path` should be encoded as [WTF-8](https://wtf-8.codeberg.page/).
-/// On WASI, `dest_path` should be encoded as valid UTF-8.
-/// On other platforms, `dest_path` is an opaque sequence of bytes with no particular encoding.
-pub fn atomicFile(parent: Dir, io: Io, dest_path: []const u8, options: AtomicFileOptions) !File.Atomic {
- if (path.dirname(dest_path)) |dirname| {
- const dir = if (options.make_path)
- try parent.createDirPathOpen(io, dirname, .{})
- else
- try parent.openDir(io, dirname, .{});
-
- return .init(io, path.basename(dest_path), options.permissions, dir, true, options.write_buffer);
- } else {
- return .init(io, dest_path, options.permissions, parent, false, options.write_buffer);
- }
+pub const CreateFileAtomicError = error{
+ NoDevice,
+ /// On Windows, `\\server` or `\\server\share` was not found.
+ NetworkNotFound,
+ /// On Windows, antivirus software is enabled by default. It can be
+ /// disabled, but Windows Update sometimes ignores the user's preference
+ /// and re-enables it. When enabled, antivirus software on Windows
+ /// intercepts file system operations and makes them significantly slower
+ /// in addition to possibly failing with this error code.
+ AntivirusInterference,
+ /// In WASI, this error may occur when the file descriptor does
+ /// not hold the required rights to open a new resource relative to it.
+ AccessDenied,
+ PermissionDenied,
+ SymLinkLoop,
+ ProcessFdQuotaExceeded,
+ SystemFdQuotaExceeded,
+ /// Either:
+ /// * One of the path components does not exist.
+ /// * Cwd was used, but cwd has been deleted.
+ /// * The path associated with the open directory handle has been deleted.
+ FileNotFound,
+ /// Insufficient kernel memory was available.
+ SystemResources,
+ /// A new path cannot be created because the device has no room for the new file.
+ NoSpaceLeft,
+ /// A component used as a directory in the path was not, in fact, a directory.
+ NotDir,
+ WouldBlock,
+ ReadOnlyFileSystem,
+} || Io.Dir.PathNameError || Io.Cancelable || Io.UnexpectedError;
+
+/// Create an unnamed ephemeral file that can eventually be atomically
+/// materialized into `sub_path`.
+///
+/// The returned `File.Atomic` provides API to emulate the behavior in case it
+/// is not directly supported by the underlying operating system.
+///
+/// * On Windows, `sub_path` should be encoded as [WTF-8](https://wtf-8.codeberg.page/).
+/// * On WASI, `sub_path` should be encoded as valid UTF-8.
+/// * On other platforms, `sub_path` is an opaque sequence of bytes with no particular encoding.
+pub fn createFileAtomic(
+ dir: Dir,
+ io: Io,
+ sub_path: []const u8,
+ options: CreateFileAtomicOptions,
+) CreateFileAtomicError!File.Atomic {
+ return io.vtable.dirCreateFileAtomic(io.userdata, dir, sub_path, options);
}
pub const SetPermissionsError = File.SetPermissionsError;
diff --git a/lib/std/Io/File.zig b/lib/std/Io/File.zig
@@ -278,7 +278,7 @@ pub const OpenError = error{
FileBusy,
/// Non-blocking was requested and the operation cannot return immediately.
WouldBlock,
-} || Io.Dir.PathNameError || Io.Cancelable || Io.UnexpectedError;
+} || Dir.PathNameError || Io.Cancelable || Io.UnexpectedError;
pub fn close(file: File, io: Io) void {
return io.vtable.fileClose(io.userdata, (&file)[0..1]);
@@ -708,6 +708,38 @@ pub fn realPath(file: File, io: Io, out_buffer: []u8) RealPathError!usize {
return io.vtable.fileRealPath(io.userdata, file, out_buffer);
}
+pub const HardLinkOptions = struct {
+ follow_symlinks: bool = true,
+};
+
+pub const HardLinkError = error{
+ AccessDenied,
+ PermissionDenied,
+ DiskQuota,
+ PathAlreadyExists,
+ HardwareFailure,
+ /// Either the OS or the filesystem does not support hard links.
+ OperationUnsupported,
+ SymLinkLoop,
+ LinkQuotaExceeded,
+ FileNotFound,
+ SystemResources,
+ NoSpaceLeft,
+ ReadOnlyFileSystem,
+ NotSameFileSystem,
+ NotDir,
+} || Io.Cancelable || Dir.PathNameError || Io.UnexpectedError;
+
+pub fn hardLink(
+ file: File,
+ io: Io,
+ new_dir: Dir,
+ new_sub_path: []const u8,
+ options: HardLinkOptions,
+) HardLinkError!void {
+ return io.vtable.fileHardLink(io.userdata, file, new_dir, new_sub_path, options);
+}
+
test {
_ = Reader;
_ = Writer;
diff --git a/lib/std/Io/File/Atomic.zig b/lib/std/Io/File/Atomic.zig
@@ -6,97 +6,78 @@ const File = std.Io.File;
const Dir = std.Io.Dir;
const assert = std.debug.assert;
-file_writer: File.Writer,
-random_integer: u64,
-dest_basename: []const u8,
+file: File,
+file_basename_hex: u64,
file_open: bool,
file_exists: bool,
-close_dir_on_deinit: bool,
+
dir: Dir,
+close_dir_on_deinit: bool,
-pub const InitError = File.OpenError;
+dest_sub_path: []const u8,
-/// Note that the `Dir.atomicFile` API may be more handy than this lower-level function.
-pub fn init(
- io: Io,
- dest_basename: []const u8,
- permissions: File.Permissions,
- dir: Dir,
- close_dir_on_deinit: bool,
- write_buffer: []u8,
-) InitError!Atomic {
- while (true) {
- const random_integer = std.crypto.random.int(u64);
- const tmp_sub_path = std.fmt.hex(random_integer);
- const file = dir.createFile(io, &tmp_sub_path, .{
- .permissions = permissions,
- .exclusive = true,
- }) catch |err| switch (err) {
- error.PathAlreadyExists => continue,
- else => |e| return e,
- };
- return .{
- .file_writer = file.writer(io, write_buffer),
- .random_integer = random_integer,
- .dest_basename = dest_basename,
- .file_open = true,
- .file_exists = true,
- .close_dir_on_deinit = close_dir_on_deinit,
- .dir = dir,
- };
- }
-}
-
-/// Always call deinit, even after a successful finish().
-pub fn deinit(af: *Atomic) void {
- const io = af.file_writer.io;
+pub const InitError = File.OpenError;
+/// To release all resources, always call `deinit`, even after a successful
+/// `finish`.
+pub fn deinit(af: *Atomic, io: Io) void {
if (af.file_open) {
- af.file_writer.file.close(io);
+ af.file.close(io);
af.file_open = false;
}
if (af.file_exists) {
- const tmp_sub_path = std.fmt.hex(af.random_integer);
+ const tmp_sub_path = std.fmt.hex(af.file_basename_hex);
af.dir.deleteFile(io, &tmp_sub_path) catch {};
af.file_exists = false;
}
if (af.close_dir_on_deinit) {
af.dir.close(io);
+ af.close_dir_on_deinit = false;
}
af.* = undefined;
}
-pub const FlushError = File.Writer.Error;
+pub const LinkError = Dir.HardLinkError;
-pub fn flush(af: *Atomic) FlushError!void {
- af.file_writer.interface.flush() catch |err| switch (err) {
- error.WriteFailed => return af.file_writer.err.?,
- };
+/// Atomically materializes the file into place, failing with
+/// `error.PathAlreadyExists` if something already exists there.
+pub fn link(af: *Atomic, io: Io) LinkError!void {
+ if (af.file_exists) {
+ if (af.file_open) {
+ af.file.close(io);
+ af.file_open = false;
+ }
+ const tmp_sub_path = std.fmt.hex(af.file_basename_hex);
+ try af.dir.hardLink(&tmp_sub_path, af.dir, af.dest_sub_path, io, .{});
+ af.dir.deleteFile(io, &tmp_sub_path) catch {};
+ af.file_exists = false;
+ } else {
+ assert(af.file_open);
+ try af.file.hardLink(io, af.dir, af.dest_sub_path, .{});
+ af.file.close(io);
+ af.file_open = false;
+ }
}
-pub const RenameIntoPlaceError = Dir.RenameError;
+pub const ReplaceError = Dir.RenameError;
+/// Atomically materializes the file into place, replacing any file that
+/// already exists there.
+///
+/// Calling this function requires setting `CreateFileAtomicOptions.replace` to
+/// `true`.
+///
/// On Windows, this function introduces a period of time where some file
/// system operations on the destination file will result in
/// `error.AccessDenied`, including rename operations (such as the one used in
/// this function).
-pub fn renameIntoPlace(af: *Atomic) RenameIntoPlaceError!void {
- const io = af.file_writer.io;
-
- assert(af.file_exists);
+pub fn replace(af: *Atomic, io: Io) ReplaceError!void {
+ assert(af.file_exists); // Wrong value for `CreateFileAtomicOptions.replace`.
if (af.file_open) {
- af.file_writer.file.close(io);
+ af.file.close(io);
af.file_open = false;
}
- const tmp_sub_path = std.fmt.hex(af.random_integer);
- try af.dir.rename(&tmp_sub_path, af.dir, af.dest_basename, io);
+ const tmp_sub_path = std.fmt.hex(af.file_basename_hex);
+ try af.dir.rename(&tmp_sub_path, af.dir, af.dest_sub_path, io);
af.file_exists = false;
}
-
-pub const FinishError = FlushError || RenameIntoPlaceError;
-
-/// Combination of `flush` followed by `renameIntoPlace`.
-pub fn finish(af: *Atomic) FinishError!void {
- try af.flush();
- try af.renameIntoPlace();
-}
diff --git a/lib/std/Io/File/Writer.zig b/lib/std/Io/File/Writer.zig
@@ -272,3 +272,11 @@ pub fn end(w: *Writer) EndError!void {
=> {},
}
}
+
+/// Convenience method for calling `Io.Writer.flush` and returning the
+/// underlying error.
+pub fn flush(w: *Writer) Error!void {
+ w.interface.flush() catch |err| switch (err) {
+ error.WriteFailed => return w.err.?,
+ };
+}
diff --git a/lib/std/Io/Threaded.zig b/lib/std/Io/Threaded.zig
@@ -1403,6 +1403,7 @@ pub fn io(t: *Threaded) Io {
.dirStatFile = dirStatFile,
.dirAccess = dirAccess,
.dirCreateFile = dirCreateFile,
+ .dirCreateFileAtomic = dirCreateFileAtomic,
.dirOpenFile = dirOpenFile,
.dirOpenDir = dirOpenDir,
.dirClose = dirClose,
@@ -1445,6 +1446,7 @@ pub fn io(t: *Threaded) Io {
.fileUnlock = fileUnlock,
.fileDowngradeLock = fileDowngradeLock,
.fileRealPath = fileRealPath,
+ .fileHardLink = fileHardLink,
.processExecutableOpen = processExecutableOpen,
.processExecutablePath = processExecutablePath,
@@ -1549,6 +1551,7 @@ pub fn ioBasic(t: *Threaded) Io {
.dirStatFile = dirStatFile,
.dirAccess = dirAccess,
.dirCreateFile = dirCreateFile,
+ .dirCreateFileAtomic = dirCreateFileAtomic,
.dirOpenFile = dirOpenFile,
.dirOpenDir = dirOpenDir,
.dirClose = dirClose,
@@ -1591,6 +1594,7 @@ pub fn ioBasic(t: *Threaded) Io {
.fileUnlock = fileUnlock,
.fileDowngradeLock = fileDowngradeLock,
.fileRealPath = fileRealPath,
+ .fileHardLink = fileHardLink,
.processExecutableOpen = processExecutableOpen,
.processExecutablePath = processExecutablePath,
@@ -3413,6 +3417,170 @@ fn dirCreateFileWasi(
}
}
+fn dirCreateFileAtomic(
+ userdata: ?*anyopaque,
+ dir: Dir,
+ dest_path: []const u8,
+ options: Dir.CreateFileAtomicOptions,
+) Dir.CreateFileAtomicError!File.Atomic {
+ const t: *Threaded = @ptrCast(@alignCast(userdata));
+ const t_io = ioBasic(t);
+
+ // Linux has O_TMPFILE, but linkat() does not support AT_REPLACE, so it's
+ // useless when we have to make up a bogus path name to do the rename()
+ // anyway.
+ if (native_os == .linux and !options.replace) tmpfile: {
+ const flags: posix.O = if (@hasField(posix.O, "TMPFILE")) .{
+ .ACCMODE = .RDWR,
+ .TMPFILE = true,
+ .DIRECTORY = true,
+ .CLOEXEC = true,
+ } else if (@hasField(posix.O, "TMPFILE0") and !@hasField(posix.O, "TMPFILE2")) .{
+ .ACCMODE = .RDWR,
+ .TMPFILE0 = true,
+ .TMPFILE1 = true,
+ .DIRECTORY = true,
+ .CLOEXEC = true,
+ } else break :tmpfile;
+
+ const dest_dirname = Dir.path.dirname(dest_path);
+ if (dest_dirname) |dirname| {
+ // This has a nice side effect of preemptively triggering EISDIR or
+ // ENOENT, avoiding the ambiguity below.
+ dir.createDirPath(t_io, dirname) catch |err| switch (err) {
+ // None of these make sense in this context.
+ error.IsDir,
+ error.Streaming,
+ error.DiskQuota,
+ error.PathAlreadyExists,
+ error.LinkQuotaExceeded,
+ error.SharingViolation,
+ error.PipeBusy,
+ error.FileTooBig,
+ error.DeviceBusy,
+ error.FileLocksUnsupported,
+ error.FileBusy,
+ => return error.Unexpected,
+
+ else => |e| return e,
+ };
+ }
+
+ var path_buffer: [posix.PATH_MAX]u8 = undefined;
+ const sub_path_posix = try pathToPosix(dest_dirname orelse ".", &path_buffer);
+
+ const syscall: Syscall = try .start();
+ while (true) {
+ const rc = openat_sym(dir.handle, sub_path_posix, flags, options.permissions.toMode());
+ switch (posix.errno(rc)) {
+ .SUCCESS => {
+ syscall.finish();
+ return .{
+ .file = .{ .handle = @intCast(rc) },
+ .file_basename_hex = 0,
+ .dest_sub_path = dest_path,
+ .file_open = true,
+ .file_exists = false,
+ .close_dir_on_deinit = false,
+ .dir = dir,
+ };
+ },
+ .INTR => {
+ try syscall.checkCancel();
+ continue;
+ },
+ .ISDIR, .NOENT => {
+ // Ambiguous error code. It might mean the file system
+ // does not support O_TMPFILE. Therefore, we must fall
+ // back to not using O_TMPFILE.
+ syscall.finish();
+ break :tmpfile;
+ },
+ .INVAL => return syscall.fail(error.BadPathName),
+ .ACCES => return syscall.fail(error.AccessDenied),
+ .LOOP => return syscall.fail(error.SymLinkLoop),
+ .MFILE => return syscall.fail(error.ProcessFdQuotaExceeded),
+ .NAMETOOLONG => return syscall.fail(error.NameTooLong),
+ .NFILE => return syscall.fail(error.SystemFdQuotaExceeded),
+ .NODEV => return syscall.fail(error.NoDevice),
+ .NOMEM => return syscall.fail(error.SystemResources),
+ .NOSPC => return syscall.fail(error.NoSpaceLeft),
+ .NOTDIR => return syscall.fail(error.NotDir),
+ .PERM => return syscall.fail(error.PermissionDenied),
+ .AGAIN => return syscall.fail(error.WouldBlock),
+ .NXIO => return syscall.fail(error.NoDevice),
+ .ILSEQ => return syscall.fail(error.BadPathName),
+ else => |err| return syscall.unexpectedErrno(err),
+ }
+ }
+ }
+
+ if (Dir.path.dirname(dest_path)) |dirname| {
+ const new_dir = if (options.make_path)
+ dir.createDirPathOpen(t_io, dirname, .{}) catch |err| switch (err) {
+ // None of these make sense in this context.
+ error.IsDir,
+ error.Streaming,
+ error.DiskQuota,
+ error.PathAlreadyExists,
+ error.LinkQuotaExceeded,
+ error.SharingViolation,
+ error.PipeBusy,
+ error.FileTooBig,
+ error.FileLocksUnsupported,
+ error.FileBusy,
+ error.DeviceBusy,
+ => return error.Unexpected,
+
+ else => |e| return e,
+ }
+ else
+ try dir.openDir(t_io, dirname, .{});
+
+ return atomicFileInit(t_io, Dir.path.basename(dest_path), options.permissions, new_dir, true);
+ }
+
+ return atomicFileInit(t_io, dest_path, options.permissions, dir, false);
+}
+
+fn atomicFileInit(
+ t_io: Io,
+ dest_basename: []const u8,
+ permissions: File.Permissions,
+ dir: Dir,
+ close_dir_on_deinit: bool,
+) Dir.CreateFileAtomicError!File.Atomic {
+ while (true) {
+ const random_integer = std.crypto.random.int(u64);
+ const tmp_sub_path = std.fmt.hex(random_integer);
+ const file = dir.createFile(t_io, &tmp_sub_path, .{
+ .permissions = permissions,
+ .exclusive = true,
+ }) catch |err| switch (err) {
+ error.PathAlreadyExists => continue,
+ error.DeviceBusy => continue,
+ error.FileBusy => continue,
+ error.SharingViolation => continue,
+
+ error.IsDir => return error.Unexpected, // No path components.
+ error.FileTooBig => return error.Unexpected, // Creating, not opening.
+ error.FileLocksUnsupported => return error.Unexpected, // Not asking for locks.
+ error.PipeBusy => return error.Unexpected, // Not opening a pipe.
+
+ else => |e| return e,
+ };
+ return .{
+ .file = file,
+ .file_basename_hex = random_integer,
+ .dest_sub_path = dest_basename,
+ .file_open = true,
+ .file_exists = true,
+ .close_dir_on_deinit = close_dir_on_deinit,
+ .dir = dir,
+ };
+ }
+}
+
const dirOpenFile = switch (native_os) {
.windows => dirOpenFileWindows,
.wasi => dirOpenFileWasi,
@@ -3925,7 +4093,7 @@ fn dirOpenDirPosix(
.NOMEM => return error.SystemResources,
.NOTDIR => return error.NotDir,
.PERM => return error.PermissionDenied,
- .BUSY => return error.DeviceBusy,
+ .BUSY => |err| return errnoBug(err), // O_EXCL not passed
.NXIO => return error.NoDevice,
.ILSEQ => return error.BadPathName,
else => |err| return posix.unexpectedErrno(err),
@@ -4985,6 +5153,64 @@ fn realPathPosix(fd: posix.fd_t, out_buffer: []u8) File.RealPathError!usize {
comptime unreachable;
}
+fn fileHardLink(
+ userdata: ?*anyopaque,
+ file: File,
+ new_dir: Dir,
+ new_sub_path: []const u8,
+ options: File.HardLinkOptions,
+) File.HardLinkError!void {
+ _ = userdata;
+ if (native_os != .linux) return error.OperationUnsupported;
+
+ var new_path_buffer: [posix.PATH_MAX]u8 = undefined;
+ const new_sub_path_posix = try pathToPosix(new_sub_path, &new_path_buffer);
+
+ const flags: u32 = if (!options.follow_symlinks)
+ posix.AT.SYMLINK_NOFOLLOW | posix.AT.EMPTY_PATH
+ else
+ posix.AT.EMPTY_PATH;
+
+ return linkat(file.handle, "", new_dir.handle, new_sub_path_posix, flags);
+}
+
+fn linkat(
+ old_dir: posix.fd_t,
+ old_path: [*:0]const u8,
+ new_dir: posix.fd_t,
+ new_path: [*:0]const u8,
+ flags: u32,
+) File.HardLinkError!void {
+ const syscall: Syscall = try .start();
+ while (true) {
+ switch (posix.errno(posix.system.linkat(old_dir, old_path, new_dir, new_path, flags))) {
+ .SUCCESS => return syscall.finish(),
+ .INTR => {
+ try syscall.checkCancel();
+ continue;
+ },
+ .ACCES => return syscall.fail(error.AccessDenied),
+ .DQUOT => return syscall.fail(error.DiskQuota),
+ .EXIST => return syscall.fail(error.PathAlreadyExists),
+ .IO => return syscall.fail(error.HardwareFailure),
+ .LOOP => return syscall.fail(error.SymLinkLoop),
+ .MLINK => return syscall.fail(error.LinkQuotaExceeded),
+ .NAMETOOLONG => return syscall.fail(error.NameTooLong),
+ .NOENT => return syscall.fail(error.FileNotFound),
+ .NOMEM => return syscall.fail(error.SystemResources),
+ .NOSPC => return syscall.fail(error.NoSpaceLeft),
+ .NOTDIR => return syscall.fail(error.NotDir),
+ .PERM => return syscall.fail(error.PermissionDenied),
+ .ROFS => return syscall.fail(error.ReadOnlyFileSystem),
+ .XDEV => return syscall.fail(error.NotSameFileSystem),
+ .ILSEQ => return syscall.fail(error.BadPathName),
+ .FAULT => |err| return syscall.errnoBug(err),
+ .INVAL => |err| return syscall.errnoBug(err),
+ else => |err| return syscall.unexpectedErrno(err),
+ }
+ }
+}
+
const dirDeleteFile = switch (native_os) {
.windows => dirDeleteFileWindows,
.wasi => dirDeleteFileWasi,
@@ -7325,7 +7551,6 @@ fn dirOpenDirWasi(
.NOMEM => return error.SystemResources,
.NOTDIR => return error.NotDir,
.PERM => return error.PermissionDenied,
- .BUSY => return error.DeviceBusy,
.NOTCAPABLE => return error.AccessDenied,
.ILSEQ => return error.BadPathName,
else => |err| return posix.unexpectedErrno(err),
@@ -7401,46 +7626,7 @@ fn dirHardLink(
const new_sub_path_posix = try pathToPosix(new_sub_path, &new_path_buffer);
const flags: u32 = if (!options.follow_symlinks) posix.AT.SYMLINK_NOFOLLOW else 0;
-
- const syscall: Syscall = try .start();
- while (true) {
- switch (posix.errno(posix.system.linkat(
- old_dir.handle,
- old_sub_path_posix,
- new_dir.handle,
- new_sub_path_posix,
- flags,
- ))) {
- .SUCCESS => return syscall.finish(),
- .INTR => {
- try syscall.checkCancel();
- continue;
- },
- else => |e| {
- syscall.finish();
- switch (e) {
- .ACCES => return error.AccessDenied,
- .DQUOT => return error.DiskQuota,
- .EXIST => return error.PathAlreadyExists,
- .FAULT => |err| return errnoBug(err),
- .IO => return error.HardwareFailure,
- .LOOP => return error.SymLinkLoop,
- .MLINK => return error.LinkQuotaExceeded,
- .NAMETOOLONG => return error.NameTooLong,
- .NOENT => return error.FileNotFound,
- .NOMEM => return error.SystemResources,
- .NOSPC => return error.NoSpaceLeft,
- .NOTDIR => return error.NotDir,
- .PERM => return error.PermissionDenied,
- .ROFS => return error.ReadOnlyFileSystem,
- .XDEV => return error.NotSameFileSystem,
- .INVAL => |err| return errnoBug(err),
- .ILSEQ => return error.BadPathName,
- else => |err| return posix.unexpectedErrno(err),
- }
- },
- }
- }
+ return linkat(old_dir.handle, old_sub_path_posix, new_dir.handle, new_sub_path_posix, flags);
}
fn fileClose(userdata: ?*anyopaque, files: []const File) void {
@@ -13831,7 +14017,6 @@ fn windowsCreateProcessPathExt(
error.NetworkNotFound,
error.NameTooLong,
error.BadPathName,
- error.DeviceBusy,
=> return error.FileNotFound,
};
};
diff --git a/lib/std/c.zig b/lib/std/c.zig
@@ -8427,6 +8427,7 @@ pub const O = switch (native_os) {
CLOEXEC: bool = false,
SYNC: bool = false,
PATH: bool = false,
+ /// This is typically invalid without also setting `DIRECTORY`.
TMPFILE: bool = false,
_: u9 = 0,
},
@@ -8615,6 +8616,7 @@ pub const O = switch (native_os) {
_19: u1 = 0,
CLOEXEC: bool = false,
PATH: bool = false,
+ /// This is typically invalid without also setting `DIRECTORY`.
TMPFILE: bool = false,
_: u9 = 0,
},
diff --git a/lib/std/fs/test.zig b/lib/std/fs/test.zig
@@ -1652,11 +1652,10 @@ test "AtomicFile" {
;
{
- var buffer: [100]u8 = undefined;
- var af = try ctx.dir.atomicFile(io, test_out_file, .{ .write_buffer = &buffer });
- defer af.deinit();
- try af.file_writer.interface.writeAll(test_content);
- try af.finish();
+ var af = try ctx.dir.createFileAtomic(io, test_out_file, .{ .replace = true });
+ defer af.deinit(io);
+ try af.file.writeStreamingAll(io, test_content);
+ try af.replace(io);
}
const content = try ctx.dir.readFileAlloc(io, test_out_file, allocator, .limited(9999));
try expectEqualStrings(test_content, content);
diff --git a/lib/std/os/linux.zig b/lib/std/os/linux.zig
@@ -324,6 +324,7 @@ pub const O = switch (native_arch) {
CLOEXEC: bool = false,
SYNC: bool = false,
PATH: bool = false,
+ /// This is typically invalid without also setting `DIRECTORY`.
TMPFILE: bool = false,
_23: u9 = 0,
},
@@ -346,6 +347,7 @@ pub const O = switch (native_arch) {
CLOEXEC: bool = false,
SYNC: bool = false,
PATH: bool = false,
+ /// This is typically invalid without also setting `DIRECTORY`.
TMPFILE: bool = false,
_23: u9 = 0,
},
@@ -368,6 +370,7 @@ pub const O = switch (native_arch) {
CLOEXEC: bool = false,
SYNC: bool = false,
PATH: bool = false,
+ /// This is typically invalid without also setting `DIRECTORY`.
TMPFILE: bool = false,
_23: u9 = 0,
},
@@ -393,6 +396,7 @@ pub const O = switch (native_arch) {
CLOEXEC: bool = false,
SYNC: bool = false,
PATH: bool = false,
+ /// This is typically invalid without also setting `DIRECTORY`.
TMPFILE: bool = false,
_27: u6 = 0,
},
@@ -417,6 +421,7 @@ pub const O = switch (native_arch) {
CLOEXEC: bool = false,
_20: u1 = 0,
PATH: bool = false,
+ /// This is typically invalid without also setting `DIRECTORY`.
TMPFILE: bool = false,
_23: u9 = 0,
},
@@ -439,6 +444,7 @@ pub const O = switch (native_arch) {
CLOEXEC: bool = false,
SYNC: bool = false,
PATH: bool = false,
+ /// This is typically invalid without also setting `DIRECTORY`.
TMPFILE: bool = false,
_23: u9 = 0,
},
@@ -459,13 +465,16 @@ pub const O = switch (native_arch) {
NOFOLLOW: bool = false,
NOATIME: bool = false,
CLOEXEC: bool = false,
- _20: u1 = 0,
+ /// This is typically invalid without also setting `TMPFILE1` and `DIRECTORY`.
+ TMPFILE0: bool = false,
PATH: bool = false,
- _22: u10 = 0,
+ _22: u4 = 0,
+ /// This is typically invalid without also setting `TMPFILE0` and `DIRECTORY`.
+ TMPFILE1: bool = false,
+ _27: u5 = 0,
// #define O_RSYNC 04010000
// #define O_SYNC 04010000
- // #define O_TMPFILE 020200000
// #define O_NDELAY O_NONBLOCK
},
.m68k => packed struct(u32) {
diff --git a/lib/std/zig/system.zig b/lib/std/zig/system.zig
@@ -793,7 +793,6 @@ fn glibcVerFromRPath(io: Io, rpath: []const u8) !std.SemanticVersion {
var dir = cwd.openDir(io, rpath, .{}) catch |err| switch (err) {
error.NameTooLong => return error.Unexpected,
error.BadPathName => return error.Unexpected,
- error.DeviceBusy => return error.Unexpected,
error.NetworkNotFound => return error.Unexpected, // Windows-only
error.FileNotFound => return error.GLibCNotFound,
diff --git a/src/Builtin.zig b/src/Builtin.zig
@@ -343,10 +343,10 @@ pub fn updateFileOnDisk(file: *File, comp: *Compilation) !void {
}
// `make_path` matters because the dir hasn't actually been created yet.
- var af = try root_dir.atomicFile(io, sub_path, .{ .make_path = true, .write_buffer = &.{} });
- defer af.deinit();
- try af.file_writer.interface.writeAll(file.source.?);
- af.finish() catch |err| switch (err) {
+ var af = try root_dir.createFileAtomic(io, sub_path, .{ .make_path = true, .replace = true });
+ defer af.deinit(io);
+ try af.file.writeStreamingAll(io, file.source.?);
+ af.replace(io) catch |err| switch (err) {
error.AccessDenied => switch (builtin.os.tag) {
.windows => {
// Very likely happened due to another process or thread
diff --git a/src/Compilation.zig b/src/Compilation.zig
@@ -3916,11 +3916,14 @@ pub fn saveState(comp: *Compilation) !void {
// Using an atomic file prevents a crash or power failure from corrupting
// the previous incremental compilation state.
+ var af = try lf.emit.root_dir.handle.createFileAtomic(io, basename, .{ .replace = true });
+ defer af.deinit(io);
+
var write_buffer: [1024]u8 = undefined;
- var af = try lf.emit.root_dir.handle.atomicFile(io, basename, .{ .write_buffer = &write_buffer });
- defer af.deinit();
- try af.file_writer.interface.writeVecAll(bufs.items);
- try af.finish();
+ var file_writer = af.file.writer(io, &write_buffer);
+ try file_writer.interface.writeVecAll(bufs.items);
+ try file_writer.interface.flush();
+ try af.replace(io);
}
fn addBuf(list: *std.array_list.Managed([]const u8), buf: []const u8) void {
@@ -5244,26 +5247,31 @@ fn processOneJob(
}
}
-fn createDepFile(comp: *Compilation, depfile: []const u8, binfile: Cache.Path) anyerror!void {
+fn createDepFile(comp: *Compilation, dep_file: []const u8, bin_file: Cache.Path) anyerror!void {
const io = comp.io;
- var buf: [4096]u8 = undefined;
- var af = try Io.Dir.cwd().atomicFile(io, depfile, .{ .write_buffer = &buf });
- defer af.deinit();
- comp.writeDepFile(binfile, &af.file_writer.interface) catch return af.file_writer.err.?;
+ var af = try Io.Dir.cwd().createFileAtomic(io, dep_file, .{ .replace = true });
+ defer af.deinit(io);
- try af.finish();
+ var buf: [4096]u8 = undefined;
+ var file_writer = af.file.writer(io, &buf);
+
+ comp.writeDepFile(bin_file, &file_writer.interface) catch |err| switch (err) {
+ error.WriteFailed => return file_writer.err.?,
+ };
+ try file_writer.flush();
+ try af.replace(io);
}
fn writeDepFile(
comp: *Compilation,
- binfile: Cache.Path,
+ bin_file: Cache.Path,
w: *std.Io.Writer,
) std.Io.Writer.Error!void {
const prefixes = comp.cache_parent.prefixes();
const fsi = comp.file_system_inputs.?.items;
- try w.print("{f}:", .{binfile});
+ try w.print("{f}:", .{bin_file});
{
var it = std.mem.splitScalar(u8, fsi, 0);
diff --git a/src/fmt.zig b/src/fmt.zig
@@ -355,11 +355,11 @@ fn fmtPathFile(
try fmt.stdout_writer.interface.print("{s}\n", .{file_path});
fmt.any_error = true;
} else {
- var af = try dir.atomicFile(io, sub_path, .{ .permissions = stat.permissions, .write_buffer = &.{} });
- defer af.deinit();
+ var af = try dir.createFileAtomic(io, sub_path, .{ .permissions = stat.permissions, .replace = true });
+ defer af.deinit(io);
- try af.file_writer.interface.writeAll(fmt.out_buffer.written());
- try af.finish();
+ try af.file.writeStreamingAll(io, fmt.out_buffer.written());
+ try af.replace(io);
try fmt.stdout_writer.interface.print("{s}\n", .{file_path});
}
}