Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

install: Detect cross-FS cache directory and find best one #12126

Draft
wants to merge 25 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
e0a990a
Copy file if not on same FS + detect and select best cache dir if not…
zackradisic Jun 21, 2024
2af98ec
Bump
Jarred-Sumner Jun 21, 2024
5e80852
Fixes #12012 (#12020)
Jarred-Sumner Jun 21, 2024
7fae25d
Deflake weboscket.test.js
Jarred-Sumner Jun 21, 2024
025b123
fix(install): use ssh keys for private git repos (#11917)
Eckhardt-D Jun 21, 2024
a1a7086
Fix minor spelling mistake in bun:test toThrowError() (#12043)
surprisedpika Jun 21, 2024
4844c29
Fix `bun patch` with workspaces and scoped packages (#12022)
zackradisic Jun 21, 2024
ea12702
[FIX]: Made the LICENCE a markdown file to be previewable and minor f…
mohiwalla Jun 21, 2024
1130ff9
fix #4925 (#12049)
dylan-conway Jun 22, 2024
fa7c50a
fix mock function crash (#12023)
paperdave Jun 22, 2024
6271579
remove glibc `memfd_create` from required symbols (#12050)
dylan-conway Jun 22, 2024
f03c78c
Use slow move-based fallback for `renameatConcurrently` (#12048)
zackradisic Jun 22, 2024
7a4a649
fix(install): fix potential flakiness with git dependencies (#12030)
dylan-conway Jun 22, 2024
38ad595
Fix TS experimental decorator crash (#11902)
forcefieldsovereign Jun 22, 2024
5d4eb07
Fixes #12045 (#12051)
Jarred-Sumner Jun 22, 2024
da32326
Implement initial LCOV reporter (no function names support) (#11883)
exoego Jun 22, 2024
cc4dcb0
Remove extraneous `Bun.ArrayBufferSink` mention. (#12065)
bomberstudios Jun 22, 2024
1c1205c
Rename code coverage reporter for `console` -> `text` (#12054)
Jarred-Sumner Jun 22, 2024
baf9278
Commit missing snapshot file
Jarred-Sumner Jun 22, 2024
b9c4566
Fixes #12039 (#12066)
Jarred-Sumner Jun 23, 2024
8cecfda
Bump
Jarred-Sumner Jun 23, 2024
16b6d09
Fixes #12070 (#12071)
Jarred-Sumner Jun 23, 2024
6fe61d1
Remove empty if statement in CMakeLists.txt (#12073)
nmarks413 Jun 23, 2024
3f99640
split out testing same fs code
zackradisic Jun 24, 2024
784fbda
Merge branch 'main' into zack/install-cache-cross-device
zackradisic Jun 24, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/bun.zig
Original file line number Diff line number Diff line change
Expand Up @@ -3265,7 +3265,7 @@ noinline fn assertionFailureWithLocation(src: std.builtin.SourceLocation) noretu
});
}

pub inline fn debugAssert(cheap_value_only_plz: bool) void {
pub fn debugAssert(cheap_value_only_plz: bool) void {
if (comptime !Environment.isDebug) {
return;
}
Expand Down
9 changes: 9 additions & 0 deletions src/env_loader.zig
Original file line number Diff line number Diff line change
Expand Up @@ -276,6 +276,15 @@ pub const Loader = struct {
return this.map.get(_key);
}

pub fn getTruthy(this: *const Loader, key: string) ?string {
if (this.get(key)) |val| {
if (val.len > 0 and !std.mem.eql(u8, val, "0")) {
return val;
}
}
return null;
}

pub fn getAuto(this: *const Loader, key: string) string {
// If it's "" or "$", it's not a variable
if (key.len < 2 or key[0] != '$') {
Expand Down
23 changes: 23 additions & 0 deletions src/fs.zig
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,29 @@ pub const FileSystem = struct {
});
}

pub fn TmpnameBuf(comptime extname: string) type {
if (!@inComptime()) @compileError("Only call this in comptime plz");

const hex_value: u64 = std.math.maxInt(u64);
const tmpname_id_number_: u32 = std.math.maxInt(u32);

var buf: [1024]u8 = undefined;
const str = std.fmt.bufPrintZ(buf[0..], ".{any}-{any}.{s}", .{
bun.fmt.hexIntLower(hex_value),
bun.fmt.hexIntUpper(tmpname_id_number_),
extname,
}) catch @compileError("Too big man");

// +1 for sentinel
const len = str.len + 1;
const sentinel_type = @typeInfo([:0]u8);
return @Type(std.builtin.Type{ .Array = std.builtin.Type.Array{
.child = u8,
.len = len,
.sentinel = sentinel_type.Pointer.sentinel,
} });
}

pub var max_fd: std.posix.fd_t = 0;

pub inline fn setMaxFd(fd: std.posix.fd_t) void {
Expand Down
184 changes: 169 additions & 15 deletions src/install/install.zig
Original file line number Diff line number Diff line change
Expand Up @@ -3367,17 +3367,113 @@ pub const PackageManager = struct {
};
}

/// We try to get a cache directory on the same filesystem as the
/// project's node_modules directory (if the user didn't explicitly
/// set the cache directory).
///
/// This is important for performance, cross-device copying/moving/renaming is slow.
///
/// To test if cache_dir is in the same FS as node_modules we first try to
/// create a dummy file in cache_dir and rename it to node_modules
///
/// If that fails we try to create a new cache_dir at the highest possible directory,
/// to make it more likely that projects in different directories will share the cache
/// folder.
///
/// Let's say the user's project is at `/Volumes/Untitled/myapp`, where `/Volumes/Untitled`
/// is a mounted filesystem or something.
///
/// We'll first start at `/` and try our same FS test. If that fails we try it with
/// `/Volumes/`, then `/Volumes/Untitled/`, which should succeed.
///
/// Our new cache dir will be something like: `/Volumes/Untitled/.bun/install/cache`.
///
/// Note how this ensures that any other projects in `/Volumes/Untitled/` will share
/// the same cache dir.
noinline fn ensureCacheDirectory(this: *PackageManager) std.fs.Dir {
const TmpBuf = bun.fs.FileSystem.TmpnameBuf("hm");

var tmpbuf: TmpBuf = undefined;
const node_modules = std.fs.cwd().makeOpenPath("node_modules", .{}) catch |err| {
Output.prettyErrorln("<r><red>error<r>: bun is unable to create the node_modules folder: {s}", .{@errorName(err)});
Global.crash();
};
const tmpname: [:0]const u8 = bun.span(Fs.FileSystem.instance.tmpname("hm", &tmpbuf, bun.fastRandom()) catch unreachable);
const tmp = node_modules.createFileZ(tmpname, .{ .truncate = true }) catch |err| {
Output.prettyErrorln("<r><red>error<r>: bun is unable to create files in the node_modules folder: {s}", .{@errorName(err)});
Global.crash();
};
defer tmp.close();
defer node_modules.deleteFileZ(tmpname) catch {};

loop: while (true) {
if (this.options.enable.cache) {
const cache_dir = fetchCacheDirectoryPath(this.env);
this.cache_directory_path = this.allocator.dupeZ(u8, cache_dir.path) catch bun.outOfMemory();
var cache_dir_set_kind: CacheDirSetKind = .auto;
const cache_dir = fetchCacheDirectoryPathImpl(this.env, &cache_dir_set_kind);

return std.fs.cwd().makeOpenPath(cache_dir.path, .{}) catch {
var dir = std.fs.cwd().makeOpenPath(cache_dir.path, .{}) catch {
this.options.enable.cache = false;
this.allocator.free(this.cache_directory_path);
continue :loop;
};

if (!bun.sys.testSameFileSystem(node_modules, dir, tmpname)) {
if (cache_dir_set_kind.didExplicitlySet()) {
Output.warn(
"Bun's install cache directory was set to <cyan>{s}<r>, by the environment variable <b>{s}<r>.\n\nHowever, this directory exists <b>outside<r> of the filesystem the current project is located in. Moving files across filesystems is much slower than normal.\n\nIf you want to change this, set the environment variable to a path on the same filesystem as the project, or unset it and Bun will automatically do this for you.",
.{
cache_dir.path,
@tagName(cache_dir_set_kind),
},
);
} else {
dir.close();
var buf: bun.PathBuffer = undefined;
var buf2: bun.PathBuffer = undefined;
const node_modules_folder_path = switch (bun.sys.getFdPath(bun.toFD(node_modules.fd), &buf2)) {
.result => |p| p,
.err => |err| {
Output.prettyErrorln("<r><red>error<r>: bun is unable to get the node_modules path: {}", .{err});
Global.crash();
},
};

if (bun.sys.findBestDirectoryInSameFileSystem(node_modules_folder_path, node_modules, tmpname, &buf)) |result| out: {
const bestdir: std.fs.Dir = result[0];
const bestdir_path: []const u8 = result[1];
const is_node_modules = bun.strings.eql(node_modules_folder_path, bestdir_path);
const is_inside_cwd = brk: {
const cwd_path = node_modules_folder_path[0 .. node_modules_folder_path.len - ("node_modules".len + 1)];
break :brk bun.strings.eql(cwd_path, bestdir_path);
};

const best_cache_dir = (if (is_node_modules)
bestdir.makeOpenPath(".cache", .{})
else if (is_inside_cwd)
bestdir.makeOpenPath("node_modules/.cache", .{})
else
bestdir.makeOpenPath(".bun/install/cache", .{})) catch break :out;

const best_cache_dir_path = if (is_node_modules)
bun.path.joinZBuf(buf2[0..], &[_][]const u8{ bestdir_path, ".cache" }, .auto)
else if (is_inside_cwd)
bun.path.joinZBuf(buf2[0..], &[_][]const u8{ bestdir_path, "node_modules", ".cache" }, .auto)
else
bun.path.joinZBuf(buf2[0..], &[_][]const u8{ bestdir_path, ".bun", "install", "cache" }, .auto);

this.cache_directory_path = this.allocator.dupeZ(u8, best_cache_dir_path) catch bun.outOfMemory();
return best_cache_dir;
}

this.options.enable.cache = false;
continue :loop;
}
}
// make sure to delete the file we just moved
else dir.deleteFileZ(tmpname) catch {};

this.cache_directory_path = this.allocator.dupeZ(u8, cache_dir.path) catch bun.outOfMemory();

return dir;
}

this.cache_directory_path = this.allocator.dupeZ(u8, Path.joinAbsString(
Expand Down Expand Up @@ -3406,6 +3502,7 @@ pub const PackageManager = struct {
// Error RenameAcrossMountPoints moving react-is to cache dir:
noinline fn ensureTemporaryDirectory(this: *PackageManager) std.fs.Dir {
var cache_directory = this.getCacheDirectory();

// The chosen tempdir must be on the same filesystem as the cache directory
// This makes renameat() work
this.temp_dir_name = Fs.FileSystem.RealFS.getDefaultTempDir();
Expand Down Expand Up @@ -3444,6 +3541,7 @@ pub const PackageManager = struct {
};
file.close();

// Make sure tempdir and cachedir are in the same filesystem
std.posix.renameatZ(tempdir.fd, tmpname, cache_directory.fd, tmpname) catch |err| {
if (!tried_dot_tmp) {
tried_dot_tmp = true;
Expand Down Expand Up @@ -6025,22 +6123,40 @@ pub const PackageManager = struct {
}

const CacheDir = struct { path: string, is_node_modules: bool };
const CacheDirSetKind = enum {
BUN_INSTALL_CACHE_DIR,
BUN_INSTALL,
XDG_CACHE_HOME,
auto,

pub fn didExplicitlySet(this: CacheDirSetKind) bool {
return this != .auto;
}
};
pub fn fetchCacheDirectoryPath(env: *DotEnv.Loader) CacheDir {
if (env.get("BUN_INSTALL_CACHE_DIR")) |dir| {
var set_kind: CacheDirSetKind = .auto;
return fetchCacheDirectoryPathImpl(env, &set_kind);
}

fn fetchCacheDirectoryPathImpl(env: *DotEnv.Loader, explicitly_set_install_dir: *CacheDirSetKind) CacheDir {
if (env.getTruthy("BUN_INSTALL_CACHE_DIR")) |dir| {
explicitly_set_install_dir.* = .BUN_INSTALL_CACHE_DIR;
return CacheDir{ .path = Fs.FileSystem.instance.abs(&[_]string{dir}), .is_node_modules = false };
}

if (env.get("BUN_INSTALL")) |dir| {
if (env.getTruthy("BUN_INSTALL")) |dir| {
explicitly_set_install_dir.* = .BUN_INSTALL;
var parts = [_]string{ dir, "install/", "cache/" };
return CacheDir{ .path = Fs.FileSystem.instance.abs(&parts), .is_node_modules = false };
}

if (env.get("XDG_CACHE_HOME")) |dir| {
if (env.getTruthy("XDG_CACHE_HOME")) |dir| {
explicitly_set_install_dir.* = .XDG_CACHE_HOME;
var parts = [_]string{ dir, ".bun/", "install/", "cache/" };
return CacheDir{ .path = Fs.FileSystem.instance.abs(&parts), .is_node_modules = false };
}

if (env.get(bun.DotEnv.home_env)) |dir| {
if (env.getTruthy(bun.DotEnv.home_env)) |dir| {
var parts = [_]string{ dir, ".bun/", "install/", "cache/" };
return CacheDir{ .path = Fs.FileSystem.instance.abs(&parts), .is_node_modules = false };
}
Expand All @@ -6049,6 +6165,11 @@ pub const PackageManager = struct {
return CacheDir{ .is_node_modules = true, .path = Fs.FileSystem.instance.abs(&fallback_parts) };
}

fn fallbackCacheDir() CacheDir {
var fallback_parts = [_]string{"node_modules/.bun-cache"};
return CacheDir{ .is_node_modules = true, .path = Fs.FileSystem.instance.abs(&fallback_parts) };
}

pub fn runTasks(
manager: *PackageManager,
comptime ExtractCompletionContext: type,
Expand Down Expand Up @@ -10937,7 +11058,16 @@ pub const PackageManager = struct {
"node_modules",
.{ .move_fallback = true },
).asErr()) |e| {
Output.warn("failed renaming nested node_modules folder, this may cause issues: {}", .{e});
if (e.getErrno() == .XDEV) {
bun.C.moveFileZSlow(
bun.toFD(root_node_modules.fd),
random_tempdir,
bun.toFD(new_folder_handle.fd),
"node_modules",
) catch |ee| {
Output.warn("failed renaming the bun patch tag, this may cause issues: {s}", .{@errorName(ee)});
};
} else Output.warn("failed renaming nested node_modules folder, this may cause issues: {}", .{e});
}
}

Expand All @@ -10949,7 +11079,16 @@ pub const PackageManager = struct {
patch_tag,
.{ .move_fallback = true },
).asErr()) |e| {
Output.warn("failed renaming the bun patch tag, this may cause issues: {}", .{e});
if (e.getErrno() == .XDEV) {
bun.C.moveFileZSlow(
bun.toFD(root_node_modules.fd),
patch_tag_tmpname,
bun.toFD(new_folder_handle.fd),
patch_tag,
) catch |ee| {
Output.warn("failed renaming the bun patch tag, this may cause issues: {s}", .{@errorName(ee)});
};
} else Output.warn("failed renaming the bun patch tag, this may cause issues: {}", .{e});
}
}
}
Expand Down Expand Up @@ -11106,11 +11245,26 @@ pub const PackageManager = struct {
path_in_patches_dir,
.{ .move_fallback = true },
).asErr()) |e| {
Output.prettyError(
"<r><red>error<r>: failed renaming patch file to patches dir {}<r>\n",
.{e.toSystemError()},
);
Global.crash();
if (e.getErrno() == .XDEV) {
bun.C.moveFileZSlow(
bun.toFD(tmpdir.fd),
tempfile_name,
bun.FD.cwd(),
path_in_patches_dir,
) catch |ee| {
Output.prettyError(
"<r><red>error<r>: failed renaming patch file to patches dir {s}<r>\n",
.{@errorName(ee)},
);
Global.crash();
};
} else {
Output.prettyError(
"<r><red>error<r>: failed renaming patch file to patches dir {}<r>\n",
.{e.toSystemError()},
);
Global.crash();
}
}

const patch_key = std.fmt.allocPrint(manager.allocator, "{s}", .{resolution_label}) catch bun.outOfMemory();
Expand Down
Loading
Loading