From d5312d53a066092ba9efd687e25b29a87eb6290c Mon Sep 17 00:00:00 2001 From: Andrew Kelley Date: Thu, 10 Nov 2022 13:50:44 -0700 Subject: [PATCH] WASI: remove absolute path emulation from std lib Instead of checking for absolute paths and current working directories in various file system operations, there is one simple solution: allow overriding `std.fs.cwd` on WASI. os.realpath is back to causing a compile error when used on WASI. This caused a compile error in the Sema handling of `@src()`. The compiler should never call realpath, so the commit that made this change is reverted (95ab942184427e7c9b840d71f4d093931e3e48fb). If this breaks debug info, a different strategy is needed to solve it other than using realpath. I also removed the preopens code and replaced it with something much simpler. There is no longer any global state in the standard library. Additionally- * os.openat no longer does an unnecessary fstat on WASI when O.WRONLY is not provided. * os.chdir is back to causing a compile error on WASI. --- lib/std/fs.zig | 35 ++--- lib/std/fs/wasi.zig | 305 +++++--------------------------------------- lib/std/os.zig | 212 +++++------------------------- src/Sema.zig | 11 +- src/introspect.zig | 44 +------ src/main.zig | 85 ++++++++---- 6 files changed, 145 insertions(+), 547 deletions(-) diff --git a/lib/std/fs.zig b/lib/std/fs.zig index 12ee39a7bf..2640abff1e 100644 --- a/lib/std/fs.zig +++ b/lib/std/fs.zig @@ -1130,13 +1130,6 @@ pub const Dir = struct { w.RIGHT.FD_FILESTAT_SET_TIMES | w.RIGHT.FD_FILESTAT_SET_SIZE; } - if (self.fd == os.wasi.AT.FDCWD or path.isAbsolute(sub_path)) { - // Resolve absolute or CWD-relative paths to a path within a Preopen - var resolved_path_buf: [MAX_PATH_BYTES]u8 = undefined; - const resolved_path = try os.resolvePathWasi(sub_path, &resolved_path_buf); - const fd = try os.openatWasi(resolved_path.dir_fd, resolved_path.relative_path, 0x0, 0x0, fdflags, base, 0x0); - return File{ .handle = fd }; - } const fd = try os.openatWasi(self.fd, sub_path, 0x0, 0x0, fdflags, base, 0x0); return File{ .handle = fd }; } @@ -1301,13 +1294,6 @@ pub const Dir = struct { if (flags.exclusive) { oflags |= w.O.EXCL; } - if (self.fd == os.wasi.AT.FDCWD or path.isAbsolute(sub_path)) { - // Resolve absolute or CWD-relative paths to a path within a Preopen - var resolved_path_buf: [MAX_PATH_BYTES]u8 = undefined; - const resolved_path = try os.resolvePathWasi(sub_path, &resolved_path_buf); - const fd = try os.openatWasi(resolved_path.dir_fd, resolved_path.relative_path, 0x0, oflags, 0x0, base, 0x0); - return File{ .handle = fd }; - } const fd = try os.openatWasi(self.fd, sub_path, 0x0, oflags, 0x0, base, 0x0); return File{ .handle = fd }; } @@ -1711,16 +1697,15 @@ pub const Dir = struct { // TODO do we really need all the rights here? const inheriting: w.rights_t = w.RIGHT.ALL ^ w.RIGHT.SOCK_SHUTDOWN; - const result = blk: { - if (self.fd == os.wasi.AT.FDCWD or path.isAbsolute(sub_path)) { - // Resolve absolute or CWD-relative paths to a path within a Preopen - var resolved_path_buf: [MAX_PATH_BYTES]u8 = undefined; - const resolved_path = try os.resolvePathWasi(sub_path, &resolved_path_buf); - break :blk os.openatWasi(resolved_path.dir_fd, resolved_path.relative_path, symlink_flags, w.O.DIRECTORY, 0x0, base, inheriting); - } else { - break :blk os.openatWasi(self.fd, sub_path, symlink_flags, w.O.DIRECTORY, 0x0, base, inheriting); - } - }; + const result = os.openatWasi( + self.fd, + sub_path, + symlink_flags, + w.O.DIRECTORY, + 0x0, + base, + inheriting, + ); const fd = result catch |err| switch (err) { error.FileTooBig => unreachable, // can't happen for directories error.IsDir => unreachable, // we're providing O.DIRECTORY @@ -2667,6 +2652,8 @@ pub const Dir = struct { pub fn cwd() Dir { if (builtin.os.tag == .windows) { return Dir{ .fd = os.windows.peb().ProcessParameters.CurrentDirectory.Handle }; + } else if (builtin.os.tag == .wasi and @hasDecl(root, "wasi_cwd")) { + return root.wasi_cwd(); } else { return Dir{ .fd = os.AT.FDCWD }; } diff --git a/lib/std/fs/wasi.zig b/lib/std/fs/wasi.zig index 706cec905e..d15f37dc0f 100644 --- a/lib/std/fs/wasi.zig +++ b/lib/std/fs/wasi.zig @@ -10,284 +10,47 @@ const wasi = std.os.wasi; const fd_t = wasi.fd_t; const prestat_t = wasi.prestat_t; -/// Type-tag of WASI preopen. -/// -/// WASI currently offers only `Dir` as a valid preopen resource. -pub const PreopenTypeTag = enum { - Dir, -}; +pub const Preopens = struct { + // Indexed by file descriptor number. + names: []const []const u8, -/// Type of WASI preopen. -/// -/// WASI currently offers only `Dir` as a valid preopen resource. -pub const PreopenType = union(PreopenTypeTag) { - /// Preopened directory type. - Dir: []const u8, - - const Self = @This(); - - pub fn eql(self: Self, other: PreopenType) bool { - if (std.meta.activeTag(self) != std.meta.activeTag(other)) return false; - - switch (self) { - PreopenTypeTag.Dir => |this_path| return mem.eql(u8, this_path, other.Dir), - } - } - - // Checks whether `other` refers to a subdirectory of `self` and, if so, - // returns the relative path to `other` from `self` - // - // Expects `other` to be a canonical path, not containing "." or ".." - pub fn getRelativePath(self: Self, other: PreopenType) ?[]const u8 { - if (std.meta.activeTag(self) != std.meta.activeTag(other)) return null; - - switch (self) { - PreopenTypeTag.Dir => |self_path| { - const other_path = other.Dir; - if (mem.indexOfDiff(u8, self_path, other_path)) |index| { - if (index < self_path.len) return null; - } - - const rel_path = other_path[self_path.len..]; - if (rel_path.len == 0) { - return rel_path; - } else if (rel_path[0] == '/') { - return rel_path[1..]; - } else { - if (self_path[self_path.len - 1] != '/') return null; - return rel_path; - } - }, - } - } - - pub fn format(self: Self, comptime fmt: []const u8, options: std.fmt.FormatOptions, out_stream: anytype) !void { - if (fmt.len != 0) std.fmt.invalidFmtError(fmt, self); - _ = options; - try out_stream.print("PreopenType{{ ", .{}); - switch (self) { - PreopenType.Dir => |path| try out_stream.print(".Dir = '{}'", .{std.zig.fmtId(path)}), - } - return out_stream.print(" }}", .{}); - } -}; - -/// WASI preopen struct. This struct consists of a WASI file descriptor -/// and type of WASI preopen. It can be obtained directly from the WASI -/// runtime using `PreopenList.populate()` method. -pub const Preopen = struct { - /// WASI file descriptor. - fd: fd_t, - - /// Type of the preopen. - type: PreopenType, - - /// Construct new `Preopen` instance. - pub fn new(fd: fd_t, preopen_type: PreopenType) Preopen { - return Preopen{ - .fd = fd, - .type = preopen_type, - }; - } -}; - -/// WASI resource identifier struct. This is effectively a path within -/// a WASI Preopen. -pub const PreopenUri = struct { - /// WASI Preopen containing the resource. - base: Preopen, - /// Path to resource within `base`. - relative_path: []const u8, -}; - -/// Dynamically-sized array list of WASI preopens. This struct is a -/// convenience wrapper for issuing `std.os.wasi.fd_prestat_get` and -/// `std.os.wasi.fd_prestat_dir_name` syscalls to the WASI runtime, and -/// collecting the returned preopens. -/// -/// This struct is intended to be used in any WASI program which intends -/// to use the capabilities as passed on by the user of the runtime. -pub const PreopenList = struct { - const InnerList = std.ArrayList(Preopen); - - /// Internal dynamically-sized buffer for storing the gathered preopens. - buffer: InnerList, - - const Self = @This(); - - pub const Error = error{ OutOfMemory, Overflow } || os.UnexpectedError; - - /// Deinitialize with `deinit`. - pub fn init(allocator: Allocator) Self { - return Self{ .buffer = InnerList.init(allocator) }; - } - - /// Release all allocated memory. - pub fn deinit(pm: Self) void { - for (pm.buffer.items) |preopen| { - switch (preopen.type) { - PreopenType.Dir => |path| pm.buffer.allocator.free(path), - } - } - pm.buffer.deinit(); - } - - /// Populate the list with the preopens by issuing `std.os.wasi.fd_prestat_get` - /// and `std.os.wasi.fd_prestat_dir_name` syscalls to the runtime. - /// - /// If called more than once, it will clear its contents every time before - /// issuing the syscalls. - /// - /// In the unlinkely event of overflowing the number of available file descriptors, - /// returns `error.Overflow`. In this case, even though an error condition was reached - /// the preopen list still contains all valid preopened file descriptors that are valid - /// for use. Therefore, it is fine to call `find`, `asSlice`, or `toOwnedSlice`. Finally, - /// `deinit` still must be called! - /// - /// Usage of `cwd_root`: - /// If provided, `cwd_root` is inserted as prefix for any Preopens that - /// begin with "." and all paths are normalized as POSIX-style absolute - /// paths. `cwd_root` must be an absolute path. - /// - /// For example: - /// "./foo/bar" -> "{cwd_root}/foo/bar" - /// "foo/bar" -> "/foo/bar" - /// "/foo/bar" -> "/foo/bar" - /// - /// If `cwd_root` is not provided, all preopen directories are unmodified. - /// - pub fn populate(self: *Self, cwd_root: ?[]const u8) Error!void { - if (cwd_root) |root| assert(fs.path.isAbsolute(root)); - - // Clear contents if we're being called again - for (try self.toOwnedSlice()) |preopen| { - switch (preopen.type) { - PreopenType.Dir => |path| self.buffer.allocator.free(path), - } - } - errdefer self.deinit(); - var fd: fd_t = 3; // start fd has to be beyond stdio fds - - var path_buf: [fs.MAX_PATH_BYTES]u8 = undefined; - while (true) { - var buf: prestat_t = undefined; - switch (wasi.fd_prestat_get(fd, &buf)) { - .SUCCESS => {}, - .OPNOTSUPP => { - // not a preopen, so keep going - fd = try math.add(fd_t, fd, 1); - continue; - }, - .BADF => { - // OK, no more fds available - break; - }, - else => |err| return os.unexpectedErrno(err), - } - const preopen_len = buf.u.dir.pr_name_len; - - mem.set(u8, path_buf[0..preopen_len], 0); - switch (wasi.fd_prestat_dir_name(fd, &path_buf, preopen_len)) { - .SUCCESS => {}, - else => |err| return os.unexpectedErrno(err), - } - - // Unfortunately, WASI runtimes (e.g. wasmer) are not consistent about whether the - // NULL sentinel is included in the reported Preopen name_len - const raw_path = if (path_buf[preopen_len - 1] == 0) blk: { - break :blk path_buf[0 .. preopen_len - 1]; - } else path_buf[0..preopen_len]; - - // If we were provided a CWD root to resolve against, we try to treat Preopen dirs as - // POSIX paths, relative to "/" or `cwd_root` depending on whether they start with "." - const path = if (cwd_root) |cwd| blk: { - const resolve_paths: []const []const u8 = if (raw_path[0] == '.') &.{ cwd, raw_path } else &.{ "/", raw_path }; - break :blk try fs.path.resolve(self.buffer.allocator, resolve_paths); - } else blk: { - // If we were provided no CWD root, we preserve the preopen dir without resolving - break :blk try self.buffer.allocator.dupe(u8, raw_path); - }; - errdefer self.buffer.allocator.free(path); - const preopen = Preopen.new(fd, .{ .Dir = path }); - - try self.buffer.append(preopen); - fd = try math.add(fd_t, fd, 1); - } - } - - /// Find a preopen which includes access to `preopen_type`. - /// - /// If multiple preopens match the provided resource, the most specific - /// match is returned. More recent preopens take priority, as well. - pub fn findContaining(self: Self, preopen_type: PreopenType) ?PreopenUri { - var best_match: ?PreopenUri = null; - - for (self.buffer.items) |preopen| { - if (preopen.type.getRelativePath(preopen_type)) |rel_path| { - if (best_match == null or rel_path.len <= best_match.?.relative_path.len) { - best_match = PreopenUri{ - .base = preopen, - .relative_path = if (rel_path.len == 0) "." else rel_path, - }; - } - } - } - return best_match; - } - - /// Find preopen by fd. If the preopen exists, return it. - /// Otherwise, return `null`. - pub fn findByFd(self: Self, fd: fd_t) ?Preopen { - for (self.buffer.items) |preopen| { - if (preopen.fd == fd) { - return preopen; + pub fn find(p: Preopens, name: []const u8) ?os.fd_t { + for (p.names) |elem_name, i| { + if (mem.eql(u8, elem_name, name)) { + return @intCast(os.fd_t, i); } } return null; } - - /// Find preopen by type. If the preopen exists, return it. - /// Otherwise, return `null`. - pub fn find(self: Self, preopen_type: PreopenType) ?*const Preopen { - for (self.buffer.items) |*preopen| { - if (preopen.type.eql(preopen_type)) { - return preopen; - } - } - return null; - } - - /// Return the inner buffer as read-only slice. - pub fn asSlice(self: Self) []const Preopen { - return self.buffer.items; - } - - /// The caller owns the returned memory. ArrayList becomes empty. - pub fn toOwnedSlice(self: *Self) ![]Preopen { - return try self.buffer.toOwnedSlice(); - } }; -test "extracting WASI preopens" { - if (builtin.os.tag != .wasi or builtin.link_libc) return error.SkipZigTest; +pub fn preopensAlloc(gpa: Allocator) Allocator.Error!Preopens { + var names: std.ArrayListUnmanaged([]const u8) = .{}; + defer names.deinit(gpa); - var preopens = PreopenList.init(std.testing.allocator); - defer preopens.deinit(); + try names.ensureUnusedCapacity(gpa, 3); - try preopens.populate(null); - - const preopen = preopens.find(PreopenType{ .Dir = "." }) orelse unreachable; - try std.testing.expect(preopen.type.eql(PreopenType{ .Dir = "." })); - - const po_type1 = PreopenType{ .Dir = "/" }; - try std.testing.expect(std.mem.eql(u8, po_type1.getRelativePath(.{ .Dir = "/" }).?, "")); - try std.testing.expect(std.mem.eql(u8, po_type1.getRelativePath(.{ .Dir = "/test/foobar" }).?, "test/foobar")); - - const po_type2 = PreopenType{ .Dir = "/test/foo" }; - try std.testing.expect(po_type2.getRelativePath(.{ .Dir = "/test/foobar" }) == null); - - const po_type3 = PreopenType{ .Dir = "/test" }; - try std.testing.expect(std.mem.eql(u8, po_type3.getRelativePath(.{ .Dir = "/test" }).?, "")); - try std.testing.expect(std.mem.eql(u8, po_type3.getRelativePath(.{ .Dir = "/test/" }).?, "")); - try std.testing.expect(std.mem.eql(u8, po_type3.getRelativePath(.{ .Dir = "/test/foo/bar" }).?, "foo/bar")); + names.appendAssumeCapacity("stdin"); // 0 + names.appendAssumeCapacity("stdout"); // 1 + names.appendAssumeCapacity("stderr"); // 2 + while (true) { + const fd = @intCast(wasi.fd_t, names.items.len); + var prestat: prestat_t = undefined; + switch (wasi.fd_prestat_get(fd, &prestat)) { + .SUCCESS => {}, + .OPNOTSUPP, .BADF => return .{ .names = names.toOwnedSlice(gpa) }, + else => @panic("fd_prestat_get: unexpected error"), + } + try names.ensureUnusedCapacity(gpa, 1); + // This length does not include a null byte. Let's keep it this way to + // gently encourage WASI implementations to behave properly. + const name_len = prestat.u.dir.pr_name_len; + const name = try gpa.alloc(u8, name_len); + errdefer gpa.free(name); + switch (wasi.fd_prestat_dir_name(fd, name.ptr, name.len)) { + .SUCCESS => {}, + else => @panic("fd_prestat_dir_name: unexpected error"), + } + names.appendAssumeCapacity(name); + } } diff --git a/lib/std/os.zig b/lib/std/os.zig index 7ebe415026..ab43b3a0a1 100644 --- a/lib/std/os.zig +++ b/lib/std/os.zig @@ -1521,76 +1521,6 @@ pub fn openW(file_path_w: []const u16, flags: u32, perm: mode_t) OpenError!fd_t }; } -var wasi_cwd = if (builtin.os.tag == .wasi and !builtin.link_libc) struct { - // List of available Preopens - preopens: ?PreopenList = null, - // Memory buffer for storing the relative portion of the CWD - path_buffer: [MAX_PATH_BYTES]u8 = undefined, - // The absolute path associated with the current working directory - cwd: []const u8 = "/", -}{} else undefined; - -/// Initialize the available Preopen list on WASI and set the CWD to `cwd_init`. -/// Note that `cwd_init` corresponds to a Preopen directory, not necessarily -/// a POSIX path. For example, "." matches a Preopen provided with `--dir=.` -/// -/// This must be called before using any relative or absolute paths with `std.os` -/// functions, if you are on WASI without linking libc. -/// -/// The current working directory is initialized to `cwd_root`, and `cwd_root` -/// is inserted as a prefix for any Preopens whose dir begins with "." -/// For example: -/// "./foo/bar" - canonicalizes to -> "{cwd_root}/foo/bar" -/// "foo/bar" - canonicalizes to -> "/foo/bar" -/// "/foo/bar" - canonicalizes to -> "/foo/bar" -/// -/// `cwd_root` must be an absolute path. For initialization behavior similar to -/// wasi-libc, use "/" as the `cwd_root` -/// -/// `alloc` must not be a temporary or leak-detecting allocator, since `std.os` -/// retains ownership of allocations internally and may never call free(). -pub fn initPreopensWasi(alloc: Allocator, cwd_root: []const u8) !void { - if (builtin.os.tag == .wasi) { - if (!builtin.link_libc) { - var preopen_list = PreopenList.init(alloc); - errdefer preopen_list.deinit(); - try preopen_list.populate(cwd_root); - - var path_alloc = std.heap.FixedBufferAllocator.init(&wasi_cwd.path_buffer); - wasi_cwd.cwd = try path_alloc.allocator().dupe(u8, cwd_root); - - if (wasi_cwd.preopens) |preopens| preopens.deinit(); - wasi_cwd.preopens = preopen_list; - } else { - // wasi-libc defaults to an effective CWD root of "/" - if (!mem.eql(u8, cwd_root, "/")) return error.UnsupportedDirectory; - } - } -} - -/// Resolve a relative or absolute path to an handle (`fd_t`) and a relative subpath. -/// -/// For absolute paths, this automatically searches among available Preopens to find -/// a match. For relative paths, it uses the "emulated" CWD. -/// Automatically looks up the correct Preopen corresponding to the provided path. -pub fn resolvePathWasi(path: []const u8, out_buffer: *[MAX_PATH_BYTES]u8) !RelativePathWasi { - var allocator = std.heap.FixedBufferAllocator.init(out_buffer); - var alloc = allocator.allocator(); - - const abs_path = fs.path.resolve(alloc, &.{ wasi_cwd.cwd, path }) catch return error.NameTooLong; - const preopen_uri = wasi_cwd.preopens.?.findContaining(.{ .Dir = abs_path }); - - if (preopen_uri) |po| { - return RelativePathWasi{ - .dir_fd = po.base.fd, - .relative_path = po.relative_path, - }; - } else { - // No matching preopen found - return error.AccessDenied; - } -} - /// 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 `openatZ`. @@ -1600,22 +1530,23 @@ pub fn openat(dir_fd: fd_t, file_path: []const u8, flags: u32, mode: mode_t) Ope return openatW(dir_fd, file_path_w.span(), flags, mode); } else if (builtin.os.tag == .wasi and !builtin.link_libc) { // `mode` is ignored on WASI, which does not support unix-style file permissions - const fd = if (dir_fd == wasi.AT.FDCWD or fs.path.isAbsolute(file_path)) blk: { - // Resolve absolute or CWD-relative paths to a path within a Preopen - var path_buf: [MAX_PATH_BYTES]u8 = undefined; - const path = try resolvePathWasi(file_path, &path_buf); - - const opts = try openOptionsFromFlagsWasi(path.dir_fd, flags); - break :blk try openatWasi(path.dir_fd, path.relative_path, opts.lookup_flags, opts.oflags, opts.fs_flags, opts.fs_rights_base, opts.fs_rights_inheriting); - } else blk: { - const opts = try openOptionsFromFlagsWasi(dir_fd, flags); - break :blk try openatWasi(dir_fd, file_path, opts.lookup_flags, opts.oflags, opts.fs_flags, opts.fs_rights_base, opts.fs_rights_inheriting); - }; + const opts = try openOptionsFromFlagsWasi(dir_fd, flags); + const fd = try openatWasi( + dir_fd, + file_path, + opts.lookup_flags, + opts.oflags, + opts.fs_flags, + opts.fs_rights_base, + opts.fs_rights_inheriting, + ); errdefer close(fd); - const info = try fstat(fd); - if (flags & O.WRONLY != 0 and info.filetype == .DIRECTORY) - return error.IsDir; + if (flags & O.WRONLY != 0) { + const info = try fstat(fd); + if (info.filetype == .DIRECTORY) + return error.IsDir; + } return fd; } @@ -1673,7 +1604,15 @@ fn openOptionsFromFlagsWasi(fd: fd_t, oflag: u32) OpenError!WasiOpenOptions { } /// Open and possibly create a file in WASI. -pub fn openatWasi(dir_fd: fd_t, file_path: []const u8, lookup_flags: lookupflags_t, oflags: oflags_t, fdflags: fdflags_t, base: rights_t, inheriting: rights_t) OpenError!fd_t { +pub fn openatWasi( + dir_fd: fd_t, + file_path: []const u8, + lookup_flags: lookupflags_t, + oflags: oflags_t, + fdflags: fdflags_t, + base: rights_t, + inheriting: rights_t, +) OpenError!fd_t { while (true) { var fd: fd_t = undefined; switch (wasi.path_open(dir_fd, lookup_flags, file_path.ptr, file_path.len, oflags, base, inheriting, fdflags, &fd)) { @@ -2031,7 +1970,7 @@ pub fn getcwd(out_buffer: []u8) GetCwdError![]u8 { if (builtin.os.tag == .windows) { return windows.GetCurrentDirectory(out_buffer); } else if (builtin.os.tag == .wasi and !builtin.link_libc) { - const path = wasi_cwd.cwd; + const path = "."; if (out_buffer.len < path.len) return error.NameTooLong; std.mem.copy(u8, out_buffer, path); return out_buffer[0..path.len]; @@ -2125,12 +2064,6 @@ pub fn symlinkat(target_path: []const u8, newdirfd: fd_t, sym_link_path: []const if (builtin.os.tag == .windows) { @compileError("symlinkat is not supported on Windows; use std.os.windows.CreateSymbolicLink instead"); } else if (builtin.os.tag == .wasi and !builtin.link_libc) { - if (newdirfd == wasi.AT.FDCWD or fs.path.isAbsolute(target_path)) { - // Resolve absolute or CWD-relative paths to a path within a Preopen - var path_buf: [MAX_PATH_BYTES]u8 = undefined; - const path = try resolvePathWasi(sym_link_path, &path_buf); - return symlinkatWasi(target_path, path.dir_fd, path.relative_path); - } return symlinkatWasi(target_path, newdirfd, sym_link_path); } const target_path_c = try toPosixPath(target_path); @@ -2284,25 +2217,8 @@ pub fn linkat( flags: i32, ) LinkatError!void { if (builtin.os.tag == .wasi and !builtin.link_libc) { - var resolve_olddir: bool = (olddir == wasi.AT.FDCWD or fs.path.isAbsolute(oldpath)); - var resolve_newdir: bool = (newdir == wasi.AT.FDCWD or fs.path.isAbsolute(newpath)); - - var old: RelativePathWasi = .{ .dir_fd = olddir, .relative_path = oldpath }; - var new: RelativePathWasi = .{ .dir_fd = newdir, .relative_path = newpath }; - - // Resolve absolute or CWD-relative paths to a path within a Preopen - if (resolve_olddir or resolve_newdir) { - var buf_old: [MAX_PATH_BYTES]u8 = undefined; - var buf_new: [MAX_PATH_BYTES]u8 = undefined; - - if (resolve_olddir) - old = try resolvePathWasi(oldpath, &buf_old); - - if (resolve_newdir) - new = try resolvePathWasi(newpath, &buf_new); - - return linkatWasi(old, new, flags); - } + const old: RelativePathWasi = .{ .dir_fd = olddir, .relative_path = oldpath }; + const new: RelativePathWasi = .{ .dir_fd = newdir, .relative_path = newpath }; return linkatWasi(old, new, flags); } const old = try toPosixPath(oldpath); @@ -2423,12 +2339,6 @@ pub fn unlinkat(dirfd: fd_t, file_path: []const u8, flags: u32) UnlinkatError!vo const file_path_w = try windows.sliceToPrefixedFileW(file_path); return unlinkatW(dirfd, file_path_w.span(), flags); } else if (builtin.os.tag == .wasi and !builtin.link_libc) { - if (dirfd == wasi.AT.FDCWD or fs.path.isAbsolute(file_path)) { - // Resolve absolute or CWD-relative paths to a path within a Preopen - var path_buf: [MAX_PATH_BYTES]u8 = undefined; - const path = try resolvePathWasi(file_path, &path_buf); - return unlinkatWasi(path.dir_fd, path.relative_path, flags); - } return unlinkatWasi(dirfd, file_path, flags); } else { const file_path_c = try toPosixPath(file_path); @@ -2597,24 +2507,8 @@ pub fn renameat( const new_path_w = try windows.sliceToPrefixedFileW(new_path); return renameatW(old_dir_fd, old_path_w.span(), new_dir_fd, new_path_w.span(), windows.TRUE); } else if (builtin.os.tag == .wasi and !builtin.link_libc) { - var resolve_old: bool = (old_dir_fd == wasi.AT.FDCWD or fs.path.isAbsolute(old_path)); - var resolve_new: bool = (new_dir_fd == wasi.AT.FDCWD or fs.path.isAbsolute(new_path)); - - var old: RelativePathWasi = .{ .dir_fd = old_dir_fd, .relative_path = old_path }; - var new: RelativePathWasi = .{ .dir_fd = new_dir_fd, .relative_path = new_path }; - - // Resolve absolute or CWD-relative paths to a path within a Preopen - if (resolve_old or resolve_new) { - var buf_old: [MAX_PATH_BYTES]u8 = undefined; - var buf_new: [MAX_PATH_BYTES]u8 = undefined; - - if (resolve_old) - old = try resolvePathWasi(old_path, &buf_old); - if (resolve_new) - new = try resolvePathWasi(new_path, &buf_new); - - return renameatWasi(old, new); - } + const old: RelativePathWasi = .{ .dir_fd = old_dir_fd, .relative_path = old_path }; + const new: RelativePathWasi = .{ .dir_fd = new_dir_fd, .relative_path = new_path }; return renameatWasi(old, new); } else { const old_path_c = try toPosixPath(old_path); @@ -2755,12 +2649,6 @@ pub fn mkdirat(dir_fd: fd_t, sub_dir_path: []const u8, mode: u32) MakeDirError!v const sub_dir_path_w = try windows.sliceToPrefixedFileW(sub_dir_path); return mkdiratW(dir_fd, sub_dir_path_w.span(), mode); } else if (builtin.os.tag == .wasi and !builtin.link_libc) { - if (dir_fd == wasi.AT.FDCWD or fs.path.isAbsolute(sub_dir_path)) { - // Resolve absolute or CWD-relative paths to a path within a Preopen - var path_buf: [MAX_PATH_BYTES]u8 = undefined; - const path = try resolvePathWasi(sub_dir_path, &path_buf); - return mkdiratWasi(path.dir_fd, path.relative_path, mode); - } return mkdiratWasi(dir_fd, sub_dir_path, mode); } else { const sub_dir_path_c = try toPosixPath(sub_dir_path); @@ -2997,22 +2885,7 @@ pub const ChangeCurDirError = error{ /// `dir_path` is recommended to be a UTF-8 encoded string. pub fn chdir(dir_path: []const u8) ChangeCurDirError!void { if (builtin.os.tag == .wasi and !builtin.link_libc) { - var buf: [MAX_PATH_BYTES]u8 = undefined; - var alloc = std.heap.FixedBufferAllocator.init(&buf); - const path = fs.path.resolve(alloc.allocator(), &.{ wasi_cwd.cwd, dir_path }) catch |err| switch (err) { - error.OutOfMemory => return error.NameTooLong, - else => |e| return e, - }; - - const dirinfo = try fstatat(AT.FDCWD, path, 0); - if (dirinfo.filetype != .DIRECTORY) { - return error.NotDir; - } - - // This copy is guaranteed to succeed, since buf and path_buffer are the same size. - var cwd_alloc = std.heap.FixedBufferAllocator.init(&wasi_cwd.path_buffer); - wasi_cwd.cwd = cwd_alloc.allocator().dupe(u8, path) catch unreachable; - return; + @compileError("WASI does not support os.chdir"); } else if (builtin.os.tag == .windows) { var utf16_dir_path: [windows.PATH_MAX_WIDE]u16 = undefined; const len = try std.unicode.utf8ToUtf16Le(utf16_dir_path[0..], dir_path); @@ -3143,12 +3016,6 @@ pub fn readlinkZ(file_path: [*:0]const u8, out_buffer: []u8) ReadLinkError![]u8 /// See also `readlinkatWasi`, `realinkatZ` and `realinkatW`. pub fn readlinkat(dirfd: fd_t, file_path: []const u8, out_buffer: []u8) ReadLinkError![]u8 { if (builtin.os.tag == .wasi and !builtin.link_libc) { - if (dirfd == wasi.AT.FDCWD or fs.path.isAbsolute(file_path)) { - // Resolve absolute or CWD-relative paths to a path within a Preopen - var path_buf: [MAX_PATH_BYTES]u8 = undefined; - var path = try resolvePathWasi(file_path, &path_buf); - return readlinkatWasi(path.dir_fd, path.relative_path, out_buffer); - } return readlinkatWasi(dirfd, file_path, out_buffer); } if (builtin.os.tag == .windows) { @@ -4155,12 +4022,6 @@ pub const FStatAtError = FStatError || error{ NameTooLong, FileNotFound, SymLink pub fn fstatat(dirfd: fd_t, pathname: []const u8, flags: u32) FStatAtError!Stat { if (builtin.os.tag == .wasi and !builtin.link_libc) { const wasi_flags = if (flags & linux.AT.SYMLINK_NOFOLLOW == 0) wasi.LOOKUP_SYMLINK_FOLLOW else 0; - if (dirfd == wasi.AT.FDCWD or fs.path.isAbsolute(pathname)) { - // Resolve absolute or CWD-relative paths to a path within a Preopen - var path_buf: [MAX_PATH_BYTES]u8 = undefined; - const path = try resolvePathWasi(pathname, &path_buf); - return fstatatWasi(path.dir_fd, path.relative_path, wasi_flags); - } return fstatatWasi(dirfd, pathname, wasi_flags); } else if (builtin.os.tag == .windows) { @compileError("fstatat is not yet implemented on Windows"); @@ -4556,12 +4417,6 @@ pub fn faccessat(dirfd: fd_t, path: []const u8, mode: u32, flags: u32) AccessErr var resolved = RelativePathWasi{ .dir_fd = dirfd, .relative_path = path }; const file = blk: { - if (dirfd == wasi.AT.FDCWD or fs.path.isAbsolute(path)) { - // Resolve absolute or CWD-relative paths to a path within a Preopen - var path_buf: [MAX_PATH_BYTES]u8 = undefined; - resolved = resolvePathWasi(path, &path_buf) catch |err| break :blk @as(FStatAtError!Stat, err); - break :blk fstatat(resolved.dir_fd, resolved.relative_path, flags); - } break :blk fstatat(dirfd, path, flags); } catch |err| switch (err) { error.AccessDenied => return error.PermissionDenied, @@ -5147,12 +5002,7 @@ pub fn realpath(pathname: []const u8, out_buffer: *[MAX_PATH_BYTES]u8) RealPathE const pathname_w = try windows.sliceToPrefixedFileW(pathname); return realpathW(pathname_w.span(), out_buffer); } else if (builtin.os.tag == .wasi and !builtin.link_libc) { - var alloc = std.heap.FixedBufferAllocator.init(out_buffer); - - // NOTE: This emulation is incomplete. Symbolic links are not - // currently expanded during path canonicalization. - const paths = &.{ wasi_cwd.cwd, pathname }; - return fs.path.resolve(alloc.allocator(), paths) catch error.NameTooLong; + @compileError("WASI does not support os.realpath"); } const pathname_c = try toPosixPath(pathname); return realpathZ(&pathname_c, out_buffer); diff --git a/src/Sema.zig b/src/Sema.zig index a85c80826e..cb6165f2f6 100644 --- a/src/Sema.zig +++ b/src/Sema.zig @@ -15082,14 +15082,11 @@ fn zirBuiltinSrc( const file_name_val = blk: { var anon_decl = try block.startAnonDecl(); defer anon_decl.deinit(); - const relative_path = try fn_owner_decl.getFileScope().fullPath(sema.arena); - const absolute_path = std.fs.realpathAlloc(sema.arena, relative_path) catch |err| { - return sema.fail(block, src, "failed to get absolute path of file '{s}': {s}", .{ relative_path, @errorName(err) }); - }; - const aboslute_duped = try anon_decl.arena().dupeZ(u8, absolute_path); + // The compiler must not call realpath anywhere. + const name = try fn_owner_decl.getFileScope().fullPathZ(anon_decl.arena()); const new_decl = try anon_decl.finish( - try Type.Tag.array_u8_sentinel_0.create(anon_decl.arena(), aboslute_duped.len), - try Value.Tag.bytes.create(anon_decl.arena(), aboslute_duped[0 .. aboslute_duped.len + 1]), + try Type.Tag.array_u8_sentinel_0.create(anon_decl.arena(), name.len), + try Value.Tag.bytes.create(anon_decl.arena(), name[0 .. name.len + 1]), 0, // default alignment ); break :blk try Value.Tag.decl_ref.create(sema.arena, new_decl); diff --git a/src/introspect.zig b/src/introspect.zig index 27925ab667..2eeae956ec 100644 --- a/src/introspect.zig +++ b/src/introspect.zig @@ -37,34 +37,7 @@ fn testZigInstallPrefix(base_dir: fs.Dir) ?Compilation.Directory { /// based on a hard-coded Preopen directory ("/zig") pub fn findZigExePath(allocator: mem.Allocator) ![]u8 { if (builtin.os.tag == .wasi) { - var args = try std.process.argsWithAllocator(allocator); - defer args.deinit(); - // On WASI, argv[0] is always just the basename of the current executable - const argv0 = args.next() orelse return error.FileNotFound; - - // Check these paths: - // 1. "/zig/{exe_name}" - // 2. "/zig/bin/{exe_name}" - const base_paths_to_check = &[_][]const u8{ "/zig", "/zig/bin" }; - const exe_names_to_check = &[_][]const u8{ fs.path.basename(argv0), "zig.wasm" }; - - for (base_paths_to_check) |base_path| { - for (exe_names_to_check) |exe_name| { - const test_path = fs.path.join(allocator, &.{ base_path, exe_name }) catch continue; - defer allocator.free(test_path); - - // Make sure it's a file we're pointing to - const file = os.fstatat(os.wasi.AT.FDCWD, test_path, 0) catch continue; - if (file.filetype != .REGULAR_FILE) continue; - - // Path seems to be valid, let's try to turn it into an absolute path - var real_path_buf: [fs.MAX_PATH_BYTES]u8 = undefined; - if (os.realpath(test_path, &real_path_buf)) |real_path| { - return allocator.dupe(u8, real_path); // Success: return absolute path - } else |_| continue; - } - } - return error.FileNotFound; + @compileError("this function is unsupported on WASI"); } return fs.selfExePathAlloc(allocator); @@ -107,6 +80,9 @@ pub fn findZigLibDirFromSelfExe( /// Caller owns returned memory. pub fn resolveGlobalCacheDir(allocator: mem.Allocator) ![]u8 { + if (builtin.os.tag == .wasi) { + @compileError("on WASI the global cache dir must be resolved with preopens"); + } if (std.process.getEnvVarOwned(allocator, "ZIG_GLOBAL_CACHE_DIR")) |value| { if (value.len > 0) { return value; @@ -125,17 +101,7 @@ pub fn resolveGlobalCacheDir(allocator: mem.Allocator) ![]u8 { } } - if (builtin.os.tag == .wasi) { - // On WASI, we have no way to get an App data dir, so we try to use a fixed - // Preopen path "/cache" as a last resort - const path = "/cache"; - - const file = os.fstatat(os.wasi.AT.FDCWD, path, 0) catch return error.CacheDirUnavailable; - if (file.filetype != .DIRECTORY) return error.CacheDirUnavailable; - return allocator.dupe(u8, path); - } else { - return fs.getAppDataDir(allocator, appname); - } + return fs.getAppDataDir(allocator, appname); } /// Similar to std.fs.path.resolve, with a few important differences: diff --git a/src/main.zig b/src/main.zig index 5947ff773a..11eaff9058 100644 --- a/src/main.zig +++ b/src/main.zig @@ -27,6 +27,23 @@ const crash_report = @import("crash_report.zig"); // Crash report needs to override the panic handler and other root decls pub usingnamespace crash_report.root_decls; +var wasi_preopens: fs.wasi.Preopens = undefined; +pub inline fn wasi_cwd() fs.Dir { + // Expect the first preopen to be current working directory. + const cwd_fd: std.os.fd_t = 3; + assert(mem.eql(u8, wasi_preopens.names[cwd_fd], ".")); + return .{ .fd = cwd_fd }; +} + +pub fn getWasiPreopen(name: []const u8) Compilation.Directory { + return .{ + .path = name, + .handle = .{ + .fd = wasi_preopens.find(name) orelse fatal("WASI preopen not found: '{s}'", .{name}), + }, + }; +} + pub fn fatal(comptime format: []const u8, args: anytype) noreturn { std.log.err(format, args); process.exit(1); @@ -161,20 +178,14 @@ pub fn main() anyerror!void { return mainArgs(gpa_tracy.allocator(), arena, args); } - // WASI: `--dir` instructs the WASM runtime to "preopen" a directory, making - // it available to the us, the guest program. This is the only way for us to - // access files/dirs on the host filesystem if (builtin.os.tag == .wasi) { - // This sets our CWD to "/preopens/cwd" - // Dot-prefixed preopens like `--dir=.` are "mounted" at "/preopens/cwd" - // Other preopens like `--dir=lib` are "mounted" at "/" - try std.os.initPreopensWasi(arena, "/preopens/cwd"); + wasi_preopens = try fs.wasi.preopensAlloc(arena); } // Short circuit some of the other logic for bootstrapping. if (build_options.only_c) { - assert(mem.eql(u8, args[1], "build-obj")); - return buildOutputType(gpa, arena, args, .{ .build = .Obj }); + assert(mem.eql(u8, args[1], "build-exe")); + return buildOutputType(gpa, arena, args, .{ .build = .Exe }); } return mainArgs(gpa, arena, args); @@ -2300,7 +2311,7 @@ fn buildOutputType( }, } - if (std.fs.path.isAbsolute(lib_name)) { + if (fs.path.isAbsolute(lib_name)) { fatal("cannot use absolute path as a system library: {s}", .{lib_name}); } @@ -2763,18 +2774,33 @@ fn buildOutputType( } } - const self_exe_path = try introspect.findZigExePath(arena); - var zig_lib_directory: Compilation.Directory = if (override_lib_dir) |unresolved_lib_dir| l: { - const lib_dir = try introspect.resolvePath(arena, unresolved_lib_dir); - break :l .{ - .path = lib_dir, - .handle = fs.cwd().openDir(lib_dir, .{}) catch |err| { - fatal("unable to open zig lib directory '{s}': {s}", .{ lib_dir, @errorName(err) }); - }, + const self_exe_path: ?[]const u8 = if (!process.can_spawn) + null + else + introspect.findZigExePath(arena) catch |err| { + fatal("unable to find zig self exe path: {s}", .{@errorName(err)}); }; - } else introspect.findZigLibDirFromSelfExe(arena, self_exe_path) catch |err| { - fatal("unable to find zig installation directory: {s}\n", .{@errorName(err)}); + + var zig_lib_directory: Compilation.Directory = d: { + if (override_lib_dir) |unresolved_lib_dir| { + const lib_dir = try introspect.resolvePath(arena, unresolved_lib_dir); + break :d .{ + .path = lib_dir, + .handle = fs.cwd().openDir(lib_dir, .{}) catch |err| { + fatal("unable to open zig lib directory '{s}': {s}", .{ lib_dir, @errorName(err) }); + }, + }; + } else if (builtin.os.tag == .wasi) { + break :d getWasiPreopen("/lib"); + } else if (self_exe_path) |p| { + break :d introspect.findZigLibDirFromSelfExe(arena, p) catch |err| { + fatal("unable to find zig installation directory: {s}", .{@errorName(err)}); + }; + } else { + unreachable; + } }; + defer zig_lib_directory.handle.close(); var thread_pool: ThreadPool = undefined; @@ -2791,7 +2817,16 @@ fn buildOutputType( } var global_cache_directory: Compilation.Directory = l: { - const p = override_global_cache_dir orelse try introspect.resolveGlobalCacheDir(arena); + if (override_global_cache_dir) |p| { + break :l .{ + .handle = try fs.cwd().makeOpenPath(p, .{}), + .path = p, + }; + } + if (builtin.os.tag == .wasi) { + break :l getWasiPreopen("/cache"); + } + const p = try introspect.resolveGlobalCacheDir(arena); break :l .{ .handle = try fs.cwd().makeOpenPath(p, .{}), .path = p, @@ -3082,7 +3117,7 @@ fn buildOutputType( gpa, arena, test_exec_args.items, - self_exe_path, + self_exe_path.?, arg_mode, target_info, watch, @@ -3154,7 +3189,7 @@ fn buildOutputType( gpa, arena, test_exec_args.items, - self_exe_path, + self_exe_path.?, arg_mode, target_info, watch, @@ -3179,7 +3214,7 @@ fn buildOutputType( gpa, arena, test_exec_args.items, - self_exe_path, + self_exe_path.?, arg_mode, target_info, watch, @@ -3523,7 +3558,7 @@ fn cmdTranslateC(comp: *Compilation, arena: Allocator, enable_cache: bool) !void defer tree.deinit(comp.gpa); if (out_dep_path) |dep_file_path| { - const dep_basename = std.fs.path.basename(dep_file_path); + const dep_basename = fs.path.basename(dep_file_path); // Add the files depended on to the cache system. try man.addDepFilePost(zig_cache_tmp_dir, dep_basename); // Just to save disk space, we delete the file because it is never needed again.