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
This commit is contained in:
Andrew Kelley 2026-01-06 05:32:16 +01:00
commit c906f7d2e7
13 changed files with 449 additions and 206 deletions

View file

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

View file

@ -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, .{});
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;
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);
}
/// 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;

View file

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

View file

@ -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,
dest_sub_path: []const u8,
pub const InitError = File.OpenError;
/// 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;
/// 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();
}

View file

@ -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.?,
};
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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 af = try Io.Dir.cwd().createFileAtomic(io, dep_file, .{ .replace = true });
defer af.deinit(io);
var buf: [4096]u8 = undefined;
var af = try Io.Dir.cwd().atomicFile(io, depfile, .{ .write_buffer = &buf });
defer af.deinit();
var file_writer = af.file.writer(io, &buf);
comp.writeDepFile(binfile, &af.file_writer.interface) catch return af.file_writer.err.?;
try af.finish();
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);

View file

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