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:
@@ -38,7 +38,7 @@ impl LanguageImpl for Golang {
|
||||
_ => unreachable!(),
|
||||
};
|
||||
let go = installer
|
||||
.install(version)
|
||||
.install(store, version)
|
||||
.await
|
||||
.context("Failed to install go")?;
|
||||
|
||||
|
||||
@@ -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?;
|
||||
|
||||
@@ -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(())
|
||||
}
|
||||
|
||||
@@ -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?;
|
||||
|
||||
@@ -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")?;
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
@@ -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))
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user