Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
11 changes: 6 additions & 5 deletions src/install/PackageManager.rs
Original file line number Diff line number Diff line change
Expand Up @@ -203,11 +203,12 @@ pub use super::package_installer::PackageInstaller;
pub use self::package_manager_directories as directories;
use directories::attempt_to_create_package_json_and_open;
pub use directories::{
attempt_to_create_package_json, cached_git_folder_name, cached_git_folder_name_print,
cached_git_folder_name_print_auto, cached_github_folder_name, cached_github_folder_name_print,
cached_github_folder_name_print_auto, cached_npm_package_folder_name,
cached_npm_package_folder_name_print, cached_npm_package_folder_print_basename,
cached_tarball_folder_name, cached_tarball_folder_name_print, compute_cache_dir_and_subpath,
ClearCacheDirectoryError, attempt_to_create_package_json, cached_git_folder_name,
cached_git_folder_name_print, cached_git_folder_name_print_auto, cached_github_folder_name,
cached_github_folder_name_print, cached_github_folder_name_print_auto,
cached_npm_package_folder_name, cached_npm_package_folder_name_print,
cached_npm_package_folder_print_basename, cached_tarball_folder_name,
cached_tarball_folder_name_print, clear_cache_directory, compute_cache_dir_and_subpath,
fetch_cache_directory_path, get_cache_directory, get_cache_directory_and_abs_path,
get_temporary_directory, global_link_dir, global_link_dir_path, is_folder_in_cache,
path_for_cached_npm_path, path_for_resolution, save_lockfile, setup_global_dir,
Expand Down
130 changes: 126 additions & 4 deletions src/install/PackageManager/PackageManagerDirectories.rs
Original file line number Diff line number Diff line change
Expand Up @@ -375,8 +375,12 @@ pub struct CacheDir {
pub path: Vec<u8>,
}

/// An empty variable counts as unset: `abs(&[b""])` is the project directory.
pub fn fetch_cache_directory_path(env: &mut DotEnvLoader, options: Option<&Options>) -> CacheDir {
if let Some(dir) = env.get(b"BUN_INSTALL_CACHE_DIR") {
if let Some(dir) = env
.get(b"BUN_INSTALL_CACHE_DIR")
.filter(|dir| !dir.is_empty())
{
return CacheDir {
path: FileSystem::instance().abs(&[dir]).to_vec(),
};
Expand All @@ -390,21 +394,21 @@ pub fn fetch_cache_directory_path(env: &mut DotEnvLoader, options: Option<&Optio
}
}

if let Some(dir) = env.get(b"BUN_INSTALL") {
if let Some(dir) = env.get(b"BUN_INSTALL").filter(|dir| !dir.is_empty()) {
let parts: [&[u8]; 3] = [dir, b"install/", b"cache/"];
return CacheDir {
path: FileSystem::instance().abs(&parts).to_vec(),
};
}

if let Some(dir) = env_var::XDG_CACHE_HOME.get() {
if let Some(dir) = env_var::XDG_CACHE_HOME.get_not_empty() {
let parts: [&[u8]; 4] = [dir, b".bun/", b"install/", b"cache/"];
return CacheDir {
path: FileSystem::instance().abs(&parts).to_vec(),
};
}

if let Some(dir) = env_var::HOME.get() {
if let Some(dir) = env_var::HOME.get_not_empty() {
let parts: [&[u8]; 4] = [dir, b".bun/", b"install/", b"cache/"];
return CacheDir {
path: FileSystem::instance().abs(&parts).to_vec(),
Expand All @@ -417,6 +421,124 @@ pub fn fetch_cache_directory_path(env: &mut DotEnvLoader, options: Option<&Optio
}
}

// ───────────────────────────── bun pm cache rm ────────────────────────────────

pub enum ClearCacheDirectoryError {
FilesystemRoot {
cache_dir: Box<[u8]>,
},
/// `cache_dir` is (`is_same_dir`) or contains `protected`, which `what` names.
Protected {
cache_dir: Box<[u8]>,
what: &'static str,
protected: Box<[u8]>,
is_same_dir: bool,
},
Io {
cache_dir: Box<[u8]>,
err: sys::Error,
},
}

/// `bun pm cache rm`. Empties rather than removes: a symlinked or mounted cache must survive.
pub fn clear_cache_directory(
env: &mut DotEnvLoader,
original_cwd: &[u8],
) -> Result<(), ClearCacheDirectoryError> {
let configured = fetch_cache_directory_path(env, None).path;
let dir = match Dir::open(&configured) {
Ok(dir) => dir,
Err(err) if err.get_errno() == sys::E::ENOENT => return Ok(()),
Err(err) => {
return Err(ClearCacheDirectoryError::Io {
cache_dir: configured.into_boxed_slice(),
err,
});
}
};

// The opened directory, not the setting: a symlink to `$HOME` must compare as `$HOME`.
let mut cache_dir_buf = path::path_buffer_pool::get();
let cache_dir: &[u8] = match dir.get_fd_path(&mut cache_dir_buf) {
Ok(real) => real,
Err(err) => {
return Err(ClearCacheDirectoryError::Io {
cache_dir: configured.into_boxed_slice(),
err,
});
}
};

if path::basename(cache_dir).is_empty() {
return Err(ClearCacheDirectoryError::FilesystemRoot {
cache_dir: cache_dir.into(),
});
}

// The setting says where the cache is, not that the directory holds only a cache.
let protected_dirs: [(&'static str, Option<&[u8]>); 5] = [
("the home directory", env_var::HOME.get_not_empty()),
(
"the bun executable",
bun_core::self_exe_path().ok().map(ZStr::as_bytes),
),
(
"$BUN_INSTALL",
env.get(b"BUN_INSTALL").filter(|dir| !dir.is_empty()),
),
(
"the project directory",
Some(FileSystem::instance().top_level_dir()),
),
("the current directory", Some(original_cwd)),
];
for (what, protected) in protected_dirs {
let Some(protected) = protected else {
continue;
};
let mut protected_buf = path::path_buffer_pool::get();
let protected = canonical_dir_path(protected, &mut protected_buf);
let is_same_dir = match path::resolve_path::is_parent_or_equal(cache_dir, protected) {
path::resolve_path::ParentEqual::Unrelated => continue,
path::resolve_path::ParentEqual::Equal => true,
path::resolve_path::ParentEqual::Parent => false,
};
return Err(ClearCacheDirectoryError::Protected {
cache_dir: cache_dir.into(),
what,
protected: protected.into(),
is_same_dir,
});
}

let io = |err: sys::Error| ClearCacheDirectoryError::Io {
cache_dir: cache_dir.into(),
err,
};

// Collect first: readdir may skip entries while the directory is being emptied.
let mut entries: Vec<Box<[u8]>> = Vec::new();
let mut iter = sys::iterate_dir(dir.fd());
while let Some(entry) = iter.next().map_err(io)? {
entries.push(Box::from(entry.name.slice_u8()));
}
for entry in &entries {
dir.delete_tree(entry).map_err(io)?;
}
Ok(())
}

/// The real path of the directory at `path`, or `path` itself when it cannot be opened.
fn canonical_dir_path<'a>(path: &'a [u8], buf: &'a mut PathBuffer) -> &'a [u8] {
match Dir::open(path) {
Ok(dir) => match dir.get_fd_path(buf) {
Ok(real) => real,
Err(_) => path,
},
Err(_) => path,
}
}

// ─────────────────────── cached folder name printers ──────────────────────────
//
// PERF: an earlier version used `core::fmt::write` over a `format_args!` of
Expand Down
63 changes: 37 additions & 26 deletions src/runtime/cli/package_manager_command.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@ use bun_install::dependency::Dependency;
use bun_install::lockfile::{LoadResult, LoadStep, Lockfile, package::PackageColumns as _, tree};
use bun_install::npm as Npm;
use bun_install::package_manager_real::{
CommandLineArguments, Subcommand, fetch_cache_directory_path, get_cache_directory,
package_manager_options::LogLevel, setup_global_dir,
ClearCacheDirectoryError, CommandLineArguments, Subcommand, clear_cache_directory,
get_cache_directory, package_manager_options::LogLevel, setup_global_dir,
};
use bun_install::{DependencyID, PackageID, PackageManager, migration};
use bun_paths::{self as Path, PathBuffer};
Expand Down Expand Up @@ -141,6 +141,12 @@ impl PackageManagerCommand {
Global::exit(0);
}

fn note_cache_directory_setting() {
bun_core::note!(
"the cache directory comes from <b>$BUN_INSTALL_CACHE_DIR<r> or <b>$BUN_INSTALL<r>. Point it at a directory that holds nothing but the bun install cache."
);
}

fn get_subcommand(args_ptr: &mut &'static [&'static [u8]]) -> &'static [u8] {
// Mutates through `args_ptr`
// directly so the reslice persists into `pm.options.positionals`.
Expand Down Expand Up @@ -437,37 +443,42 @@ Learn more about these at <magenta>https://bun.com/docs/cli/pm<r>.\n";
{
let mut had_err = false;

// Not `pm.env`: a committed .env or .npmrc must not pick what gets deleted.
let mut process_env = bun_dotenv::Loader::init();
process_env.load_process()?;
let cache_dir = fetch_cache_directory_path(&mut process_env, None);
let mut rm_buf = PathBuffer::uninit();
let rm_dir = match Dir::cwd().make_open_path(&cache_dir.path, Default::default()) {
Ok(d) => d,
Err(err) => {
bun_core::pretty_errorln!(
"{} getting cache directory",
crate::Error::from(err).name(),
match clear_cache_directory(&mut process_env, &cwd) {
Ok(()) => bun_core::prettyln!("Cleared 'bun install' cache"),
Err(ClearCacheDirectoryError::FilesystemRoot { cache_dir }) => {
Output::err_generic(
"refusing to clear {}: it is the root of a filesystem",
(bun_fmt::quote(&cache_dir),),
);
Global::crash();
Self::note_cache_directory_setting();
had_err = true;
}
};
let rm_path = match rm_dir.get_fd_path(&mut rm_buf) {
Ok(p) => &p[..],
Err(err) => {
bun_core::pretty_errorln!(
"{} getting cache directory",
crate::Error::from(err).name(),
Err(ClearCacheDirectoryError::Protected {
cache_dir,
what,
protected,
is_same_dir,
}) => {
Output::err_generic(
"refusing to clear {}: it {} {} ({})",
(
bun_fmt::quote(&cache_dir),
if is_same_dir { "is" } else { "contains" },
what,
bun_fmt::quote(&protected),
),
);
Global::crash();
Self::note_cache_directory_setting();
had_err = true;
}
Err(ClearCacheDirectoryError::Io { cache_dir, err }) => {
Output::err(err, "could not clear {}", (bun_fmt::quote(&cache_dir),));
had_err = true;
}
};
rm_dir.close();

if let Err(err) = bun_sys::delete_tree_absolute(rm_path) {
Output::err(err, "Could not delete {s}", (bstr::BStr::new(rm_path),));
had_err = true;
}
bun_core::prettyln!("Cleared 'bun install' cache");

'bunx: {
let tmp = Fs::RealFS::platform_temp_dir();
Expand Down
Loading
Loading