1
0
mirror of https://github.com/j178/prek.git synced 2026-04-25 02:11:36 +02:00

Download files to scratch directory to avoid cross-filesystem rename (#889)

This commit is contained in:
Jo
2025-10-15 16:28:58 +08:00
committed by GitHub
parent 6d8579fdf4
commit a6598cf08a
8 changed files with 63 additions and 38 deletions
+1 -1
View File
@@ -38,7 +38,7 @@ impl LanguageImpl for Golang {
_ => unreachable!(),
};
let go = installer
.install(version)
.install(store, version)
.await
.context("Failed to install go")?;
+5 -4
View File
@@ -16,6 +16,7 @@ use crate::languages::golang::GoRequest;
use crate::languages::golang::golang::bin_dir;
use crate::languages::golang::version::GoVersion;
use crate::process::Cmd;
use crate::store::Store;
pub(crate) struct GoResult {
path: PathBuf,
@@ -99,7 +100,7 @@ impl GoInstaller {
}
}
pub(crate) async fn install(&self, request: &GoRequest) -> Result<GoResult> {
pub(crate) async fn install(&self, store: &Store, request: &GoRequest) -> Result<GoResult> {
fs_err::tokio::create_dir_all(&self.root).await?;
let _lock = LockedFile::acquire(self.root.join(".lock"), "go").await?;
@@ -120,7 +121,7 @@ impl GoInstaller {
.context("Failed to resolve Go version")?;
trace!(version = %resolved_version, "Installing go");
self.download(&resolved_version).await
self.download(store, &resolved_version).await
}
fn find_installed(&self, request: &GoRequest) -> Result<GoResult> {
@@ -183,7 +184,7 @@ impl GoInstaller {
Ok(version)
}
async fn download(&self, version: &GoVersion) -> Result<GoResult> {
async fn download(&self, store: &Store, version: &GoVersion) -> Result<GoResult> {
let arch = match HOST.architecture {
Architecture::X86_32(_) => "386",
Architecture::X86_64 => "amd64",
@@ -211,7 +212,7 @@ impl GoInstaller {
let url = format!("https://go.dev/dl/{filename}");
let target = self.root.join(version.to_string());
download_and_extract(&self.client, &url, &filename, async |extracted| {
download_and_extract(&self.client, &url, &filename, store, async |extracted| {
if target.exists() {
debug!(target = %target.display(), "Removing existing go");
fs_err::tokio::remove_dir_all(&target).await?;
+6 -2
View File
@@ -263,6 +263,7 @@ async fn download_and_extract(
client: &reqwest::Client,
url: &str,
filename: &str,
store: &Store,
callback: impl AsyncFn(&Path) -> Result<()>,
) -> Result<()> {
let response = client
@@ -285,7 +286,10 @@ async fn download_and_extract(
.into_async_read()
.compat();
let temp_dir = tempfile::tempdir()?;
let scratch_dir = store.scratch_path();
fs_err::tokio::create_dir_all(&scratch_dir).await?;
let temp_dir = tempfile::tempdir_in(&scratch_dir)?;
debug!(url = %url, temp_dir = ?temp_dir.path(), "Downloading");
let ext = ArchiveExtension::from_path(filename)?;
@@ -299,7 +303,7 @@ async fn download_and_extract(
callback(&extracted).await?;
fs_err::tokio::remove_dir_all(temp_dir.path()).await?;
drop(temp_dir);
Ok(())
}
+5 -4
View File
@@ -15,6 +15,7 @@ use crate::languages::download_and_extract;
use crate::languages::node::NodeRequest;
use crate::languages::node::version::NodeVersion;
use crate::process::Cmd;
use crate::store::Store;
#[derive(Debug)]
pub(crate) struct NodeResult {
@@ -96,7 +97,7 @@ impl NodeInstaller {
}
/// Install a version of Node.js.
pub(crate) async fn install(&self, request: &NodeRequest) -> Result<NodeResult> {
pub(crate) async fn install(&self, store: &Store, request: &NodeRequest) -> Result<NodeResult> {
fs_err::tokio::create_dir_all(&self.root).await?;
let _lock = LockedFile::acquire(self.root.join(".lock"), "node").await?;
@@ -115,7 +116,7 @@ impl NodeInstaller {
let resolved_version = self.resolve_version(request).await?;
trace!(version = %resolved_version, "Downloading node");
self.download(&resolved_version).await
self.download(store, &resolved_version).await
}
/// Get the installed version of Node.js.
@@ -173,7 +174,7 @@ impl NodeInstaller {
// TODO: support mirror?
/// Install a specific version of Node.js.
async fn download(&self, version: &NodeVersion) -> Result<NodeResult> {
async fn download(&self, store: &Store, version: &NodeVersion) -> Result<NodeResult> {
let mut arch = match HOST.architecture {
Architecture::X86_32(_) => "x86",
Architecture::X86_64 => "x64",
@@ -201,7 +202,7 @@ impl NodeInstaller {
let url = format!("https://nodejs.org/dist/v{}/{filename}", version.version());
let target = self.root.join(version.to_string());
download_and_extract(&self.client, &url, &filename, async |extracted| {
download_and_extract(&self.client, &url, &filename, store, async |extracted| {
if target.exists() {
debug!(target = %target.display(), "Removing existing node");
fs_err::tokio::remove_dir_all(&target).await?;
+1 -1
View File
@@ -49,7 +49,7 @@ impl LanguageImpl for Node {
_ => unreachable!(),
};
let node = installer
.install(node_request)
.install(store, node_request)
.await
.context("Failed to install node")?;
+1 -1
View File
@@ -90,7 +90,7 @@ impl LanguageImpl for Pygrep {
let progress = reporter.on_install_start(&hook);
let uv_dir = store.tools_path(ToolBucket::Uv);
let uv = Uv::install(&uv_dir).await?;
let uv = Uv::install(store, &uv_dir).await?;
let python_dir = store.tools_path(ToolBucket::Python);
// Find or download a Python interpreter.
+3 -1
View File
@@ -90,7 +90,9 @@ impl LanguageImpl for Python {
let progress = reporter.on_install_start(&hook);
let uv_dir = store.tools_path(ToolBucket::Uv);
let uv = Uv::install(&uv_dir).await.context("Failed to install uv")?;
let uv = Uv::install(store, &uv_dir)
.await
.context("Failed to install uv")?;
let mut info = InstallInfo::new(
hook.language,
+41 -24
View File
@@ -142,15 +142,15 @@ enum InstallSource {
}
impl InstallSource {
async fn install(&self, target: &Path) -> Result<()> {
async fn install(&self, store: &Store, target: &Path) -> Result<()> {
match self {
Self::GitHub => self.install_from_github(target).await,
Self::PyPi(source) => self.install_from_pypi(target, source).await,
Self::GitHub => self.install_from_github(store, target).await,
Self::PyPi(source) => self.install_from_pypi(store, target, source).await,
Self::Pip => self.install_from_pip(target).await,
}
}
async fn install_from_github(&self, target: &Path) -> Result<()> {
async fn install_from_github(&self, store: &Store, target: &Path) -> Result<()> {
let ext = if cfg!(windows) { "zip" } else { "tar.gz" };
let archive_name = format!("uv-{HOST}.{ext}");
let download_url = format!(
@@ -158,28 +158,39 @@ impl InstallSource {
);
let client = reqwest::Client::new();
download_and_extract(&client, &download_url, &archive_name, async |extracted| {
let source = extracted.join("uv").with_extension(EXE_EXTENSION);
let target_path = target.join("uv").with_extension(EXE_EXTENSION);
download_and_extract(
&client,
&download_url,
&archive_name,
store,
async |extracted| {
let source = extracted.join("uv").with_extension(EXE_EXTENSION);
let target_path = target.join("uv").with_extension(EXE_EXTENSION);
if target_path.exists() {
debug!(target = %target.display(), "Removing existing uv");
fs_err::tokio::remove_dir_all(&target).await?;
}
if target_path.exists() {
debug!(target = %target.display(), "Removing existing uv");
fs_err::tokio::remove_dir_all(&target).await?;
}
debug!(?source, target = %target_path.display(), "Moving uv to target");
// TODO: retry on Windows
fs_err::tokio::rename(source, target_path).await?;
debug!(?source, target = %target_path.display(), "Moving uv to target");
// TODO: retry on Windows
fs_err::tokio::rename(source, target_path).await?;
anyhow::Ok(())
})
anyhow::Ok(())
},
)
.await
.context("Failed to download and extra uv")?;
Ok(())
}
async fn install_from_pypi(&self, target: &Path, source: &PyPiMirror) -> Result<()> {
async fn install_from_pypi(
&self,
store: &Store,
target: &Path,
source: &PyPiMirror,
) -> Result<()> {
let platform_tag = get_wheel_platform_tag()?;
let wheel_name = format!("uv-{CUR_UV_VERSION}-py3-none-{platform_tag}.whl");
@@ -188,7 +199,7 @@ impl InstallSource {
let api_url = match source {
PyPiMirror::Pypi => format!("https://pypi.org/pypi/uv/{CUR_UV_VERSION}/json"),
// For mirrors, we'll fall back to simple API approach
_ => return self.install_from_simple_api(target, source).await,
_ => return self.install_from_simple_api(store, target, source).await,
};
debug!("Fetching uv metadata from: {}", api_url);
@@ -226,11 +237,16 @@ impl InstallSource {
.as_str()
.ok_or_else(|| anyhow::anyhow!("Missing download URL in PyPI response"))?;
self.download_and_extract_wheel(&client, target, &wheel_name, download_url)
self.download_and_extract_wheel(store, &client, target, &wheel_name, download_url)
.await
}
async fn install_from_simple_api(&self, target: &Path, source: &PyPiMirror) -> Result<()> {
async fn install_from_simple_api(
&self,
store: &Store,
target: &Path,
source: &PyPiMirror,
) -> Result<()> {
// Fallback for mirrors that don't support JSON API
let platform_tag = get_wheel_platform_tag()?;
let wheel_name = format!("uv-{CUR_UV_VERSION}-py3-none-{platform_tag}.whl");
@@ -275,18 +291,19 @@ impl InstallSource {
format!("{simple_url}{download_path}")
};
self.download_and_extract_wheel(&client, target, &wheel_name, &download_url)
self.download_and_extract_wheel(store, &client, target, &wheel_name, &download_url)
.await
}
async fn download_and_extract_wheel(
&self,
store: &Store,
client: &reqwest::Client,
target: &Path,
filename: &str,
download_url: &str,
) -> Result<()> {
download_and_extract(client, download_url, filename, async |extracted| {
download_and_extract(client, download_url, filename, store, async |extracted| {
// Find the uv binary in the extracted contents
let data_dir = format!("uv-{CUR_UV_VERSION}.data");
let extracted_uv = extracted
@@ -439,7 +456,7 @@ impl Uv {
Ok(source)
}
pub(crate) async fn install(uv_dir: &Path) -> Result<Self> {
pub(crate) async fn install(store: &Store, uv_dir: &Path) -> Result<Self> {
// 1) Check `uv` alongside `prek` binary (e.g. `uv tool install prek --with uv`)
let prek_exe = std::env::current_exe()?.canonicalize()?;
if let Some(prek_dir) = prek_exe.parent() {
@@ -490,7 +507,7 @@ impl Uv {
} else {
Self::select_source().await?
};
source.install(uv_dir).await?;
source.install(store, uv_dir).await?;
Ok(Self::new(uv_path))
}