diff --git a/Cargo.lock b/Cargo.lock index 7f4affed291a8..bba178f2a599e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4384,6 +4384,7 @@ dependencies = [ "filetime", "flate2", "fs-err", + "futures", "ignore", "indicatif", "indoc", diff --git a/crates/uv-toolchain/src/downloads.rs b/crates/uv-toolchain/src/downloads.rs index b82b2b300fd87..06b2df2eef75a 100644 --- a/crates/uv-toolchain/src/downloads.rs +++ b/crates/uv-toolchain/src/downloads.rs @@ -15,7 +15,7 @@ use uv_client::BetterReqwestError; use futures::TryStreamExt; use tokio_util::compat::FuturesAsyncReadCompatExt; -use tracing::debug; +use tracing::{debug, instrument}; use url::Url; use uv_fs::Simplified; @@ -265,20 +265,30 @@ impl PythonDownloadRequest { impl Display for PythonDownloadRequest { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { let mut parts = Vec::new(); - if let Some(version) = &self.version { - parts.push(version.to_string()); - } if let Some(implementation) = self.implementation { parts.push(implementation.to_string()); + } else { + parts.push("any".to_string()); + } + if let Some(version) = &self.version { + parts.push(version.to_string()); + } else { + parts.push("any".to_string()); } if let Some(os) = &self.os { parts.push(os.to_string()); + } else { + parts.push("any".to_string()); } if let Some(arch) = self.arch { parts.push(arch.to_string()); + } else { + parts.push("any".to_string()); } if let Some(libc) = self.libc { parts.push(libc.to_string()); + } else { + parts.push("any".to_string()); } write!(f, "{}", parts.join("-")) } @@ -371,6 +381,7 @@ impl PythonDownload { } /// Download and extract + #[instrument(skip(client, parent_path), fields(download = %self.key()))] pub async fn fetch( &self, client: &uv_client::BaseClient, diff --git a/crates/uv-toolchain/src/toolchain.rs b/crates/uv-toolchain/src/toolchain.rs index 3c0c4e604c111..652d59869b90a 100644 --- a/crates/uv-toolchain/src/toolchain.rs +++ b/crates/uv-toolchain/src/toolchain.rs @@ -82,7 +82,6 @@ impl Toolchain { ) -> Result { let sources = ToolchainSources::from_settings(system, preview); let toolchain = find_toolchain(request, system, &sources, cache)??; - Ok(toolchain) } diff --git a/crates/uv/Cargo.toml b/crates/uv/Cargo.toml index 30c727c69fb2c..d0882afc40922 100644 --- a/crates/uv/Cargo.toml +++ b/crates/uv/Cargo.toml @@ -46,6 +46,7 @@ clap = { workspace = true, features = ["derive", "string", "wrap_help"] } clap_complete_command = { workspace = true } flate2 = { workspace = true, default-features = false } fs-err = { workspace = true, features = ["tokio"] } +futures = { workspace = true } indicatif = { workspace = true } itertools = { workspace = true } miette = { workspace = true, features = ["fancy"] } diff --git a/crates/uv/src/cli.rs b/crates/uv/src/cli.rs index a5f0819c82df7..d41dd29a09711 100644 --- a/crates/uv/src/cli.rs +++ b/crates/uv/src/cli.rs @@ -1754,10 +1754,10 @@ pub(crate) struct ToolchainListArgs { #[derive(Args)] #[allow(clippy::struct_excessive_bools)] pub(crate) struct ToolchainInstallArgs { - /// The toolchain to install. + /// The toolchains to install. /// /// If not provided, the latest available version will be installed unless a toolchain was previously installed. - pub(crate) target: Option, + pub(crate) targets: Vec, /// Force the installation of the toolchain, even if it is already installed. #[arg(long, short)] diff --git a/crates/uv/src/commands/toolchain/install.rs b/crates/uv/src/commands/toolchain/install.rs index 1bcebf81a54a3..eb9fe02b10040 100644 --- a/crates/uv/src/commands/toolchain/install.rs +++ b/crates/uv/src/commands/toolchain/install.rs @@ -1,4 +1,6 @@ -use anyhow::Result; +use anyhow::{Error, Result}; +use futures::StreamExt; +use itertools::Itertools; use std::fmt::Write; use uv_cache::Cache; use uv_client::Connectivity; @@ -15,7 +17,7 @@ use crate::printer::Printer; /// Download and install a Python toolchain. #[allow(clippy::too_many_arguments)] pub(crate) async fn install( - target: Option, + targets: Vec, force: bool, native_tls: bool, connectivity: Connectivity, @@ -27,63 +29,75 @@ pub(crate) async fn install( warn_user!("`uv toolchain install` is experimental and may change without warning."); } + let start = std::time::Instant::now(); + let toolchains = InstalledToolchains::from_settings()?.init()?; let toolchain_dir = toolchains.root(); - let request = if let Some(target) = target { - let request = ToolchainRequest::parse(&target); - match request { - ToolchainRequest::Any => (), - ToolchainRequest::Directory(_) - | ToolchainRequest::ExecutableName(_) - | ToolchainRequest::File(_) => { - writeln!(printer.stderr(), "Invalid toolchain request '{target}'")?; - return Ok(ExitStatus::Failure); - } - _ => { - writeln!(printer.stderr(), "Looking for {request}")?; - } - } - request + let requests = if targets.is_empty() { + vec![PythonDownloadRequest::default()] } else { - ToolchainRequest::default() + targets + .iter() + .map(|target| parse_target(target, printer)) + .collect::>()? }; - if let Some(toolchain) = toolchains - .find_all()? - .find(|toolchain| toolchain.satisfies(&request)) - { - writeln!( - printer.stderr(), - "Found installed toolchain '{}'", - toolchain.key() - )?; + let installed_toolchains: Vec<_> = toolchains.find_all()?.collect(); + let mut unfilled_requests = Vec::new(); + for request in requests { + if let Some(toolchain) = installed_toolchains + .iter() + .find(|toolchain| request.satisfied_by_key(toolchain.key())) + { + writeln!( + printer.stderr(), + "Found installed toolchain '{}' that satisfies {request}", + toolchain.key() + )?; + if force { + unfilled_requests.push(request); + } + } else { + unfilled_requests.push(request); + } + } - if force { - writeln!(printer.stderr(), "Forcing reinstallation...")?; + if unfilled_requests.is_empty() { + if targets.is_empty() { + writeln!( + printer.stderr(), + "A toolchain is already installed. Use `uv toolchain install ` to install a specific toolchain.", + )?; + } else if targets.len() > 1 { + writeln!( + printer.stderr(), + "All requested toolchains already installed." + )?; } else { - if matches!(request, ToolchainRequest::Any) { - writeln!( - printer.stderr(), - "A toolchain is already installed. Use `uv toolchain install ` to install a specific toolchain.", - )?; - } else { - writeln!( - printer.stderr(), - "Already installed at {}", - toolchain.path().user_display() - )?; - } - return Ok(ExitStatus::Success); + writeln!(printer.stderr(), "Requested toolchain already installed.")?; } + return Ok(ExitStatus::Success); } - // Fill platform information missing from the request - let request = PythonDownloadRequest::from_request(request)?.fill()?; + let s = if unfilled_requests.len() == 1 { + "" + } else { + "s" + }; + writeln!( + printer.stderr(), + "Installing {} toolchain{s}", + unfilled_requests.len() + )?; - // Find the corresponding download - let download = PythonDownload::from_request(&request)?; - let version = download.python_version(); + let downloads = unfilled_requests + .into_iter() + // Populate the download requests with defaults + .map(PythonDownloadRequest::fill) + .map(|request| request.map(|inner| PythonDownload::from_request(&inner))) + .flatten_ok() + .collect::, uv_toolchain::downloads::Error>>()?; // Construct a client let client = uv_client::BaseClientBuilder::new() @@ -91,24 +105,53 @@ pub(crate) async fn install( .native_tls(native_tls) .build(); - writeln!(printer.stderr(), "Downloading {}", download.key())?; - let result = download.fetch(&client, toolchain_dir).await?; + let mut tasks = futures::stream::iter(downloads.iter()) + .map(|download| async { + let _ = writeln!(printer.stderr(), "Downloading {}", download.key()); + let result = download.fetch(&client, toolchain_dir).await; + (download.python_version(), result) + }) + .buffered(4); - let path = match result { - // Note we should only encounter `AlreadyAvailable` if there's a race condition - // TODO(zanieb): We should lock the toolchain directory on fetch - DownloadResult::AlreadyAvailable(path) => path, - DownloadResult::Fetched(path) => path, - }; - - let installed = InstalledToolchain::new(path)?; - installed.ensure_externally_managed()?; + let mut results = Vec::new(); + while let Some(task) = tasks.next().await { + let (version, result) = task; + let path = match result? { + // We should only encounter already-available during concurrent installs + DownloadResult::AlreadyAvailable(path) => path, + DownloadResult::Fetched(path) => { + writeln!( + printer.stderr(), + "Installed Python {version} to {}", + path.user_display() + )?; + path + } + }; + // Ensure the installations have externally managed markers + let installed = InstalledToolchain::new(path.clone())?; + installed.ensure_externally_managed()?; + results.push((version, path)); + } + let s = if downloads.len() == 1 { "" } else { "s" }; writeln!( printer.stderr(), - "Installed Python {version} to {}", - installed.path().user_display() + "Installed {} toolchain{s} in {}s", + downloads.len(), + start.elapsed().as_secs() )?; Ok(ExitStatus::Success) } + +fn parse_target(target: &str, printer: Printer) -> Result { + let request = ToolchainRequest::parse(target); + let download_request = PythonDownloadRequest::from_request(request.clone())?; + // TODO(zanieb): Can we improve the `PythonDownloadRequest` display? + writeln!( + printer.stderr(), + "Looking for toolchain {request} ({download_request})" + )?; + Ok(download_request) +} diff --git a/crates/uv/src/main.rs b/crates/uv/src/main.rs index fe778e523b6a3..1b6c87b94f706 100644 --- a/crates/uv/src/main.rs +++ b/crates/uv/src/main.rs @@ -796,7 +796,7 @@ async fn run() -> Result { let cache = cache.init()?; commands::toolchain_install( - args.target, + args.targets, args.force, globals.native_tls, globals.connectivity, diff --git a/crates/uv/src/settings.rs b/crates/uv/src/settings.rs index fc90904fb5acd..24a015b7640b9 100644 --- a/crates/uv/src/settings.rs +++ b/crates/uv/src/settings.rs @@ -257,7 +257,7 @@ impl ToolchainListSettings { #[allow(clippy::struct_excessive_bools)] #[derive(Debug, Clone)] pub(crate) struct ToolchainInstallSettings { - pub(crate) target: Option, + pub(crate) targets: Vec, pub(crate) force: bool, } @@ -268,9 +268,9 @@ impl ToolchainInstallSettings { args: ToolchainInstallArgs, _filesystem: Option, ) -> Self { - let ToolchainInstallArgs { target, force } = args; + let ToolchainInstallArgs { targets, force } = args; - Self { target, force } + Self { targets, force } } }