From c1f89ddf0b80149304c34339856303e023ead111 Mon Sep 17 00:00:00 2001 From: shulaoda <165626830+shulaoda@users.noreply.github.com> Date: Fri, 26 Jun 2026 07:18:09 +0800 Subject: [PATCH] fix(install): clean up the temp dir when a package-manager install fails --- crates/vite_install/src/package_manager.rs | 51 +++++++++++++++++++++- 1 file changed, 49 insertions(+), 2 deletions(-) diff --git a/crates/vite_install/src/package_manager.rs b/crates/vite_install/src/package_manager.rs index 5a9d1eecb0..e7fcb975f1 100644 --- a/crates/vite_install/src/package_manager.rs +++ b/crates/vite_install/src/package_manager.rs @@ -871,7 +871,9 @@ pub async fn download_package_manager( // Use tempfile::TempDir for robust temporary directory creation let parent_dir = target_dir.parent().unwrap(); tokio::fs::create_dir_all(parent_dir).await?; - let target_dir_tmp = tempfile::tempdir_in(parent_dir)?.path().to_path_buf(); + // Keep the TempDir guard alive so a failure path cleans up the temp dir. + let tmp_dir = tempfile::tempdir_in(parent_dir)?; + let target_dir_tmp = tmp_dir.path().to_path_buf(); download_and_extract_tgz_with_hash(&tgz_url, &target_dir_tmp, expected_hash).await.map_err( |err| { @@ -987,7 +989,9 @@ async fn download_bun_package_manager( // Download the platform-specific package directly let platform_tgz_url = get_npm_package_tgz_url(platform_package_name, version); - let target_dir_tmp = tempfile::tempdir_in(parent_dir)?.path().to_path_buf(); + // Keep the TempDir guard alive so a failure path cleans up the temp dir. + let tmp_dir = tempfile::tempdir_in(parent_dir)?; + let target_dir_tmp = tmp_dir.path().to_path_buf(); download_and_extract_tgz_with_hash(&platform_tgz_url, &target_dir_tmp, None).await.map_err( |err| { @@ -3480,4 +3484,47 @@ mod tests { // Release lock lock_file1.unlock().expect("Failed to unlock file 1"); } + + /// A failed install must not leak its temporary directory into + /// `package_manager//`. The temp dir is created via a `TempDir` guard, + /// so an early return on failure drops it and cleans up. + #[tokio::test] + async fn test_download_package_manager_cleans_temp_dir_on_failure() { + use httpmock::prelude::*; + + let vp_home = create_temp_dir(); + let server = MockServer::start(); + // A 200 body that is not a valid gzip: the download succeeds but + // extraction fails, so the install errors out after the temp dir exists. + server.mock(|when, then| { + when.method(GET).path("/pnpm/-/pnpm-10.0.0.tgz"); + then.status(200) + .header("content-type", "application/octet-stream") + .body("this is not a valid gzip archive"); + }); + + let _guard = EnvConfig::test_guard(EnvConfig { + npm_registry: server.base_url().into(), + vite_plus_home: Some(vp_home.path().to_path_buf()), + ..EnvConfig::for_test() + }); + + let result = download_package_manager(PackageManagerType::Pnpm, "10.0.0", None).await; + assert!(result.is_err(), "corrupt tarball should fail the install, got {result:?}"); + + // The per-install temp dir must be gone after the failure. + let pnpm_dir = vp_home.path().join("package_manager").join("pnpm"); + let leftovers: Vec<_> = fs::read_dir(&pnpm_dir) + .map(|rd| { + rd.filter_map(Result::ok) + .filter(|e| e.path().is_dir()) + .map(|e| e.file_name()) + .collect() + }) + .unwrap_or_default(); + assert!( + leftovers.is_empty(), + "failed install leaked temp dir(s) in {pnpm_dir:?}: {leftovers:?}" + ); + } }