Skip to content
Closed
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
214 changes: 211 additions & 3 deletions crates/vite_global_cli/src/commands/global/install.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,16 @@ use vite_path::{AbsolutePath, AbsolutePathBuf, current_dir};
use vite_shared::{format_path_prepended, output};

#[cfg(test)]
use crate::commands::env::package_metadata::{INSTALL_ID_LENGTH, is_install_id};
use crate::commands::env::package_metadata::INSTALL_ID_LENGTH;
use crate::{
commands::{
env::{
bin_config::BinConfig,
config::{get_bin_dir, get_node_modules_dir, resolve_version, resolve_version_alias},
package_metadata::{INSTALL_ID_PREFIX, PackageMetadata},
config::{
get_bin_dir, get_node_modules_dir, get_packages_dir, resolve_version,
resolve_version_alias,
},
package_metadata::{INSTALL_ID_PREFIX, PackageMetadata, is_install_id},
},
global::{CORE_SHIMS, is_local_package_spec, parse_package_spec},
},
Expand All @@ -44,6 +47,8 @@ struct InstalledPackage {
install_dir: AbsolutePathBuf,
}

const STALE_GLOBAL_INSTALL_MIN_AGE: Duration = Duration::from_secs(60 * 60);

fn package_error(package_name: &str, error: impl Into<Error>) -> (Option<String>, Error) {
(Some(package_name.to_string()), error.into())
}
Expand Down Expand Up @@ -691,6 +696,94 @@ async fn cleanup_previous_installation(
}
}

pub(crate) async fn cleanup_stale_installations() -> Result<(), Error> {
let packages_dir = get_packages_dir()?;
if !tokio::fs::try_exists(&packages_dir).await.unwrap_or(false) {
return Ok(());
}

let active_install_dirs = PackageMetadata::list_all()
.await?
.into_iter()
.filter_map(|metadata| metadata.installation_dir().ok())
.map(|path| path.as_path().to_path_buf())
.collect::<HashSet<_>>();

let mut entries = tokio::fs::read_dir(packages_dir).await?;
while let Some(entry) = entries.next_entry().await? {
let file_type = entry.file_type().await?;
if !file_type.is_dir() {
continue;
}

let name = entry.file_name();
let name = name.to_string_lossy();
if name.starts_with('@') {
let mut scoped_entries = tokio::fs::read_dir(entry.path()).await?;
while let Some(scoped_entry) = scoped_entries.next_entry().await? {
if !scoped_entry.file_type().await?.is_dir() {
continue;
}
let scoped_path = scoped_entry.path();
cleanup_stale_installation_dir(
scoped_entry.file_name().to_string_lossy().as_ref(),
&scoped_path,
&active_install_dirs,
)
.await;
}
continue;
}

let path = entry.path();
cleanup_stale_installation_dir(&name, &path, &active_install_dirs).await;
}

Ok(())
}

async fn cleanup_stale_installation_dir(
name: &str,
path: &std::path::Path,
active_install_dirs: &HashSet<std::path::PathBuf>,
) {
let has_reserved_separator = name.contains(INSTALL_ID_PREFIX);
let has_valid_install_id = name
.rfind(INSTALL_ID_PREFIX)
.is_some_and(|index| index > 0 && is_install_id(&name[index..]));
if active_install_dirs.contains(path) || (has_reserved_separator && !has_valid_install_id) {
return;
}

if !is_old_enough_for_stale_cleanup(path).await {
tracing::debug!("Skipping recent global package installation: {}", path.display());
return;
}

tracing::debug!("Cleaning up stale global package installation: {}", path.display());
if let Err(error) = tokio::fs::remove_dir_all(path).await {
if error.kind() != std::io::ErrorKind::NotFound {
tracing::warn!(
"Failed to remove stale global package installation at {}: {}",
path.display(),
error
);
}
}
}

async fn is_old_enough_for_stale_cleanup(path: &std::path::Path) -> bool {
let Ok(metadata) = tokio::fs::metadata(path).await else {
return false;
};
let Ok(modified) = metadata.modified() else {
return false;
};
std::time::SystemTime::now()
.duration_since(modified)
.is_ok_and(|age| age >= STALE_GLOBAL_INSTALL_MIN_AGE)
}

async fn remove_dir_all_if_exists(path: &AbsolutePathBuf) -> Result<(), Error> {
match tokio::fs::remove_dir_all(path).await {
Ok(()) => Ok(()),
Expand Down Expand Up @@ -1304,6 +1397,121 @@ mod tests {
assert!(stale_install.as_path().exists());
}

#[tokio::test]
async fn test_cleanup_stale_installations_removes_inactive_identified_dirs() {
use tempfile::TempDir;
use vite_path::AbsolutePathBuf;

let temp_dir = TempDir::new().unwrap();
let temp_path = temp_dir.path().to_path_buf();
let _env_guard = vite_shared::EnvConfig::test_guard(
vite_shared::EnvConfig::for_test_with_home(&temp_path),
);

let packages_dir = AbsolutePathBuf::new(temp_path.join("packages")).unwrap();
let active_id = "#123e4567-e89b-42d3-a456-426614174000";
let stale_id = "#987e6543-e21b-42d3-a456-426614174000";
let scoped_active_id = "#111e4567-e89b-42d3-a456-426614174000";
let scoped_stale_id = "#222e4567-e89b-42d3-a456-426614174000";
let legacy_stale_id = "#333e4567-e89b-42d3-a456-426614174000";
let in_progress_id = "#444e4567-e89b-42d3-a456-426614174000";

let active_dir = packages_dir.join(format!("typescript{active_id}"));
let stale_dir = packages_dir.join(format!("typescript{stale_id}"));
let legacy_stale_for_active_identified_dir = packages_dir.join("typescript");
let legacy_dir = packages_dir.join("legacy-pkg");
let legacy_stale_dir = packages_dir.join(format!("legacy-pkg{legacy_stale_id}"));
let malformed_dir = packages_dir.join("typescript#not-a-valid-install-id");
let scoped_active_dir = packages_dir.join("@scope").join(format!("pkg{scoped_active_id}"));
let scoped_stale_dir = packages_dir.join("@scope").join(format!("pkg{scoped_stale_id}"));
let scoped_legacy_stale_for_active_identified_dir = packages_dir.join("@scope").join("pkg");
let in_progress_dir = packages_dir.join(format!("in-progress{in_progress_id}"));

for dir in [
&active_dir,
&stale_dir,
&legacy_stale_for_active_identified_dir,
&legacy_dir,
&legacy_stale_dir,
&malformed_dir,
&scoped_active_dir,
&scoped_stale_dir,
&scoped_legacy_stale_for_active_identified_dir,
&in_progress_dir,
] {
tokio::fs::create_dir_all(dir).await.unwrap();
}
tokio::fs::write(active_dir.join("marker").as_path(), "active").await.unwrap();
tokio::fs::write(legacy_dir.join("marker").as_path(), "active").await.unwrap();
tokio::fs::write(scoped_active_dir.join("marker").as_path(), "active").await.unwrap();

let mut metadata = PackageMetadata::new(
"typescript".to_string(),
"5.9.3".to_string(),
"22.0.0".to_string(),
None,
vec![],
HashSet::new(),
"npm".to_string(),
);
metadata.install_id = active_id.to_string();
metadata.save().await.unwrap();

let mut scoped_metadata = PackageMetadata::new(
"@scope/pkg".to_string(),
"1.0.0".to_string(),
"22.0.0".to_string(),
None,
vec![],
HashSet::new(),
"npm".to_string(),
);
scoped_metadata.install_id = scoped_active_id.to_string();
scoped_metadata.save().await.unwrap();

let legacy_metadata = PackageMetadata::new(
"legacy-pkg".to_string(),
"1.0.0".to_string(),
"22.0.0".to_string(),
None,
vec![],
HashSet::new(),
"npm".to_string(),
);
legacy_metadata.save().await.unwrap();

for dir in [
&stale_dir,
&legacy_stale_for_active_identified_dir,
&legacy_stale_dir,
&scoped_stale_dir,
&scoped_legacy_stale_for_active_identified_dir,
] {
mark_install_dir_old(dir);
}

cleanup_stale_installations().await.unwrap();

assert!(active_dir.join("marker").as_path().exists());
assert!(legacy_dir.join("marker").as_path().exists());
assert!(scoped_active_dir.join("marker").as_path().exists());
assert!(in_progress_dir.as_path().exists());
assert!(malformed_dir.as_path().exists());
assert!(!stale_dir.as_path().exists());
assert!(!legacy_stale_for_active_identified_dir.as_path().exists());
assert!(!legacy_stale_dir.as_path().exists());
assert!(!scoped_stale_dir.as_path().exists());
assert!(!scoped_legacy_stale_for_active_identified_dir.as_path().exists());
}

fn mark_install_dir_old(dir: &AbsolutePathBuf) {
let modified =
std::time::SystemTime::now() - STALE_GLOBAL_INSTALL_MIN_AGE - Duration::from_secs(1);
let file = std::fs::File::open(dir.as_path()).unwrap();
let times = std::fs::FileTimes::new().set_modified(modified);
file.set_times(times).unwrap();
}

#[tokio::test]
#[cfg_attr(windows, serial_test::serial)]
async fn test_restore_previous_install_state_removes_partial_new_bins() {
Expand Down
3 changes: 3 additions & 0 deletions crates/vite_global_cli/src/commands/upgrade/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,9 @@ async fn install_platform_and_main(
{
output::warn(&format!("Old version cleanup failed (non-fatal): {e}"));
}
if let Err(e) = crate::commands::global::install::cleanup_stale_installations().await {
output::warn(&format!("Stale global package cleanup failed (non-fatal): {e}"));
}

if !silent {
println!(
Expand Down
Loading