master
  1const std = @import("std");
  2const builtin = @import("builtin");
  3const mem = std.mem;
  4const Allocator = mem.Allocator;
  5const os = std.os;
  6const fs = std.fs;
  7const Cache = std.Build.Cache;
  8const Compilation = @import("Compilation.zig");
  9const Package = @import("Package.zig");
 10const build_options = @import("build_options");
 11
 12/// Returns the sub_path that worked, or `null` if none did.
 13/// The path of the returned Directory is relative to `base`.
 14/// The handle of the returned Directory is open.
 15fn testZigInstallPrefix(base_dir: fs.Dir) ?Cache.Directory {
 16    const test_index_file = "std" ++ fs.path.sep_str ++ "std.zig";
 17
 18    zig_dir: {
 19        // Try lib/zig/std/std.zig
 20        const lib_zig = "lib" ++ fs.path.sep_str ++ "zig";
 21        var test_zig_dir = base_dir.openDir(lib_zig, .{}) catch break :zig_dir;
 22        const file = test_zig_dir.openFile(test_index_file, .{}) catch {
 23            test_zig_dir.close();
 24            break :zig_dir;
 25        };
 26        file.close();
 27        return .{ .handle = test_zig_dir, .path = lib_zig };
 28    }
 29
 30    // Try lib/std/std.zig
 31    var test_zig_dir = base_dir.openDir("lib", .{}) catch return null;
 32    const file = test_zig_dir.openFile(test_index_file, .{}) catch {
 33        test_zig_dir.close();
 34        return null;
 35    };
 36    file.close();
 37    return .{ .handle = test_zig_dir, .path = "lib" };
 38}
 39
 40/// Both the directory handle and the path are newly allocated resources which the caller now owns.
 41pub fn findZigLibDir(gpa: Allocator) !Cache.Directory {
 42    const cwd_path = try getResolvedCwd(gpa);
 43    defer gpa.free(cwd_path);
 44    const self_exe_path = try fs.selfExePathAlloc(gpa);
 45    defer gpa.free(self_exe_path);
 46
 47    return findZigLibDirFromSelfExe(gpa, cwd_path, self_exe_path);
 48}
 49
 50/// Like `std.process.getCwdAlloc`, but also resolves the path with `std.fs.path.resolve`. This
 51/// means the path has no repeated separators, no "." or ".." components, and no trailing separator.
 52/// On WASI, "" is returned instead of ".".
 53pub fn getResolvedCwd(gpa: Allocator) error{
 54    OutOfMemory,
 55    CurrentWorkingDirectoryUnlinked,
 56    Unexpected,
 57}![]u8 {
 58    if (builtin.target.os.tag == .wasi) {
 59        if (std.debug.runtime_safety) {
 60            const cwd = try std.process.getCwdAlloc(gpa);
 61            defer gpa.free(cwd);
 62            std.debug.assert(mem.eql(u8, cwd, "."));
 63        }
 64        return "";
 65    }
 66    const cwd = try std.process.getCwdAlloc(gpa);
 67    defer gpa.free(cwd);
 68    const resolved = try fs.path.resolve(gpa, &.{cwd});
 69    std.debug.assert(fs.path.isAbsolute(resolved));
 70    return resolved;
 71}
 72
 73/// Both the directory handle and the path are newly allocated resources which the caller now owns.
 74pub fn findZigLibDirFromSelfExe(
 75    allocator: Allocator,
 76    /// The return value of `getResolvedCwd`.
 77    /// Passed as an argument to avoid pointlessly repeating the call.
 78    cwd_path: []const u8,
 79    self_exe_path: []const u8,
 80) error{ OutOfMemory, FileNotFound }!Cache.Directory {
 81    const cwd = fs.cwd();
 82    var cur_path: []const u8 = self_exe_path;
 83    while (fs.path.dirname(cur_path)) |dirname| : (cur_path = dirname) {
 84        var base_dir = cwd.openDir(dirname, .{}) catch continue;
 85        defer base_dir.close();
 86
 87        const sub_directory = testZigInstallPrefix(base_dir) orelse continue;
 88        const p = try fs.path.join(allocator, &.{ dirname, sub_directory.path.? });
 89        defer allocator.free(p);
 90
 91        const resolved = try resolvePath(allocator, cwd_path, &.{p});
 92        return .{
 93            .handle = sub_directory.handle,
 94            .path = if (resolved.len == 0) null else resolved,
 95        };
 96    }
 97    return error.FileNotFound;
 98}
 99
100/// Caller owns returned memory.
101pub fn resolveGlobalCacheDir(allocator: Allocator) ![]u8 {
102    if (builtin.os.tag == .wasi)
103        @compileError("on WASI the global cache dir must be resolved with preopens");
104
105    if (try std.zig.EnvVar.ZIG_GLOBAL_CACHE_DIR.get(allocator)) |value| return value;
106
107    const appname = "zig";
108
109    if (builtin.os.tag != .windows) {
110        if (std.zig.EnvVar.XDG_CACHE_HOME.getPosix()) |cache_root| {
111            if (cache_root.len > 0) {
112                return fs.path.join(allocator, &.{ cache_root, appname });
113            }
114        }
115        if (std.zig.EnvVar.HOME.getPosix()) |home| {
116            return fs.path.join(allocator, &.{ home, ".cache", appname });
117        }
118    }
119
120    return fs.getAppDataDir(allocator, appname);
121}
122
123/// Similar to `fs.path.resolve`, but converts to a cwd-relative path, or, if that would
124/// start with a relative up-dir (".."), an absolute path based on the cwd. Also, the cwd
125/// returns the empty string ("") instead of ".".
126pub fn resolvePath(
127    gpa: Allocator,
128    /// The return value of `getResolvedCwd`.
129    /// Passed as an argument to avoid pointlessly repeating the call.
130    cwd_resolved: []const u8,
131    paths: []const []const u8,
132) Allocator.Error![]u8 {
133    if (builtin.target.os.tag == .wasi) {
134        std.debug.assert(mem.eql(u8, cwd_resolved, ""));
135        const res = try fs.path.resolve(gpa, paths);
136        if (mem.eql(u8, res, ".")) {
137            gpa.free(res);
138            return "";
139        }
140        return res;
141    }
142
143    // Heuristic for a fast path: if no component is absolute and ".." never appears, we just need to resolve `paths`.
144    for (paths) |p| {
145        if (fs.path.isAbsolute(p)) break; // absolute path
146        if (mem.indexOf(u8, p, "..") != null) break; // may contain up-dir
147    } else {
148        // no absolute path, no "..".
149        const res = try fs.path.resolve(gpa, paths);
150        if (mem.eql(u8, res, ".")) {
151            gpa.free(res);
152            return "";
153        }
154        std.debug.assert(!fs.path.isAbsolute(res));
155        std.debug.assert(!isUpDir(res));
156        return res;
157    }
158
159    // The fast path failed; resolve the whole thing.
160    // Optimization: `paths` often has just one element.
161    const path_resolved = switch (paths.len) {
162        0 => unreachable,
163        1 => try fs.path.resolve(gpa, &.{ cwd_resolved, paths[0] }),
164        else => r: {
165            const all_paths = try gpa.alloc([]const u8, paths.len + 1);
166            defer gpa.free(all_paths);
167            all_paths[0] = cwd_resolved;
168            @memcpy(all_paths[1..], paths);
169            break :r try fs.path.resolve(gpa, all_paths);
170        },
171    };
172    errdefer gpa.free(path_resolved);
173
174    std.debug.assert(fs.path.isAbsolute(path_resolved));
175    std.debug.assert(fs.path.isAbsolute(cwd_resolved));
176
177    if (!std.mem.startsWith(u8, path_resolved, cwd_resolved)) return path_resolved; // not in cwd
178    if (path_resolved.len == cwd_resolved.len) {
179        // equal to cwd
180        gpa.free(path_resolved);
181        return "";
182    }
183    if (path_resolved[cwd_resolved.len] != std.fs.path.sep) return path_resolved; // not in cwd (last component differs)
184
185    // in cwd; extract sub path
186    const sub_path = try gpa.dupe(u8, path_resolved[cwd_resolved.len + 1 ..]);
187    gpa.free(path_resolved);
188    return sub_path;
189}
190
191/// TODO move this to std.fs.path
192pub fn isUpDir(p: []const u8) bool {
193    return mem.startsWith(u8, p, "..") and (p.len == 2 or p[2] == fs.path.sep);
194}
195
196pub const default_local_zig_cache_basename = ".zig-cache";
197
198/// Searches upwards from `cwd` for a directory containing a `build.zig` file.
199/// If such a directory is found, returns the path to it joined to the `.zig_cache` name.
200/// Otherwise, returns `null`, indicating no suitable local cache location.
201pub fn resolveSuitableLocalCacheDir(arena: Allocator, cwd: []const u8) Allocator.Error!?[]u8 {
202    var cur_dir = cwd;
203    while (true) {
204        const joined = try fs.path.join(arena, &.{ cur_dir, Package.build_zig_basename });
205        if (fs.cwd().access(joined, .{})) |_| {
206            return try fs.path.join(arena, &.{ cur_dir, default_local_zig_cache_basename });
207        } else |err| switch (err) {
208            error.FileNotFound => {
209                cur_dir = fs.path.dirname(cur_dir) orelse return null;
210                continue;
211            },
212            else => return null,
213        }
214    }
215}