mirror of
https://github.com/astral-sh/uv.git
synced 2025-10-02 06:51:14 +00:00
Allow multiple toolchains to be requested in uv toolchain install
(#4334)
Allows installation of multiple toolchains in a single invocation because I don't want to be limited to one! Most of the implementation for concurrent downloads ported from `cargo dev fetch-python`.
This commit is contained in:
parent
4db6b902eb
commit
fdcdc2cbe6
8 changed files with 127 additions and 72 deletions
1
Cargo.lock
generated
1
Cargo.lock
generated
|
@ -4434,6 +4434,7 @@ dependencies = [
|
||||||
"filetime",
|
"filetime",
|
||||||
"flate2",
|
"flate2",
|
||||||
"fs-err",
|
"fs-err",
|
||||||
|
"futures",
|
||||||
"ignore",
|
"ignore",
|
||||||
"indicatif",
|
"indicatif",
|
||||||
"indoc",
|
"indoc",
|
||||||
|
|
|
@ -15,7 +15,7 @@ use uv_client::BetterReqwestError;
|
||||||
use futures::TryStreamExt;
|
use futures::TryStreamExt;
|
||||||
|
|
||||||
use tokio_util::compat::FuturesAsyncReadCompatExt;
|
use tokio_util::compat::FuturesAsyncReadCompatExt;
|
||||||
use tracing::debug;
|
use tracing::{debug, instrument};
|
||||||
use url::Url;
|
use url::Url;
|
||||||
use uv_fs::Simplified;
|
use uv_fs::Simplified;
|
||||||
|
|
||||||
|
@ -265,20 +265,30 @@ impl PythonDownloadRequest {
|
||||||
impl Display for PythonDownloadRequest {
|
impl Display for PythonDownloadRequest {
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
let mut parts = Vec::new();
|
let mut parts = Vec::new();
|
||||||
if let Some(version) = &self.version {
|
|
||||||
parts.push(version.to_string());
|
|
||||||
}
|
|
||||||
if let Some(implementation) = self.implementation {
|
if let Some(implementation) = self.implementation {
|
||||||
parts.push(implementation.to_string());
|
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 {
|
if let Some(os) = &self.os {
|
||||||
parts.push(os.to_string());
|
parts.push(os.to_string());
|
||||||
|
} else {
|
||||||
|
parts.push("any".to_string());
|
||||||
}
|
}
|
||||||
if let Some(arch) = self.arch {
|
if let Some(arch) = self.arch {
|
||||||
parts.push(arch.to_string());
|
parts.push(arch.to_string());
|
||||||
|
} else {
|
||||||
|
parts.push("any".to_string());
|
||||||
}
|
}
|
||||||
if let Some(libc) = self.libc {
|
if let Some(libc) = self.libc {
|
||||||
parts.push(libc.to_string());
|
parts.push(libc.to_string());
|
||||||
|
} else {
|
||||||
|
parts.push("any".to_string());
|
||||||
}
|
}
|
||||||
write!(f, "{}", parts.join("-"))
|
write!(f, "{}", parts.join("-"))
|
||||||
}
|
}
|
||||||
|
@ -371,6 +381,7 @@ impl PythonDownload {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Download and extract
|
/// Download and extract
|
||||||
|
#[instrument(skip(client, parent_path), fields(download = %self.key()))]
|
||||||
pub async fn fetch(
|
pub async fn fetch(
|
||||||
&self,
|
&self,
|
||||||
client: &uv_client::BaseClient,
|
client: &uv_client::BaseClient,
|
||||||
|
|
|
@ -82,7 +82,6 @@ impl Toolchain {
|
||||||
) -> Result<Self, Error> {
|
) -> Result<Self, Error> {
|
||||||
let sources = ToolchainSources::from_settings(system, preview);
|
let sources = ToolchainSources::from_settings(system, preview);
|
||||||
let toolchain = find_toolchain(request, system, &sources, cache)??;
|
let toolchain = find_toolchain(request, system, &sources, cache)??;
|
||||||
|
|
||||||
Ok(toolchain)
|
Ok(toolchain)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -46,6 +46,7 @@ clap = { workspace = true, features = ["derive", "string", "wrap_help"] }
|
||||||
clap_complete_command = { workspace = true }
|
clap_complete_command = { workspace = true }
|
||||||
flate2 = { workspace = true, default-features = false }
|
flate2 = { workspace = true, default-features = false }
|
||||||
fs-err = { workspace = true, features = ["tokio"] }
|
fs-err = { workspace = true, features = ["tokio"] }
|
||||||
|
futures = { workspace = true }
|
||||||
indicatif = { workspace = true }
|
indicatif = { workspace = true }
|
||||||
itertools = { workspace = true }
|
itertools = { workspace = true }
|
||||||
miette = { workspace = true, features = ["fancy"] }
|
miette = { workspace = true, features = ["fancy"] }
|
||||||
|
|
|
@ -1754,10 +1754,10 @@ pub(crate) struct ToolchainListArgs {
|
||||||
#[derive(Args)]
|
#[derive(Args)]
|
||||||
#[allow(clippy::struct_excessive_bools)]
|
#[allow(clippy::struct_excessive_bools)]
|
||||||
pub(crate) struct ToolchainInstallArgs {
|
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.
|
/// If not provided, the latest available version will be installed unless a toolchain was previously installed.
|
||||||
pub(crate) target: Option<String>,
|
pub(crate) targets: Vec<String>,
|
||||||
|
|
||||||
/// Force the installation of the toolchain, even if it is already installed.
|
/// Force the installation of the toolchain, even if it is already installed.
|
||||||
#[arg(long, short)]
|
#[arg(long, short)]
|
||||||
|
|
|
@ -1,4 +1,6 @@
|
||||||
use anyhow::Result;
|
use anyhow::{Error, Result};
|
||||||
|
use futures::StreamExt;
|
||||||
|
use itertools::Itertools;
|
||||||
use std::fmt::Write;
|
use std::fmt::Write;
|
||||||
use uv_cache::Cache;
|
use uv_cache::Cache;
|
||||||
use uv_client::Connectivity;
|
use uv_client::Connectivity;
|
||||||
|
@ -15,7 +17,7 @@ use crate::printer::Printer;
|
||||||
/// Download and install a Python toolchain.
|
/// Download and install a Python toolchain.
|
||||||
#[allow(clippy::too_many_arguments)]
|
#[allow(clippy::too_many_arguments)]
|
||||||
pub(crate) async fn install(
|
pub(crate) async fn install(
|
||||||
target: Option<String>,
|
targets: Vec<String>,
|
||||||
force: bool,
|
force: bool,
|
||||||
native_tls: bool,
|
native_tls: bool,
|
||||||
connectivity: Connectivity,
|
connectivity: Connectivity,
|
||||||
|
@ -27,63 +29,75 @@ pub(crate) async fn install(
|
||||||
warn_user!("`uv toolchain install` is experimental and may change without warning.");
|
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 toolchains = InstalledToolchains::from_settings()?.init()?;
|
||||||
let toolchain_dir = toolchains.root();
|
let toolchain_dir = toolchains.root();
|
||||||
|
|
||||||
let request = if let Some(target) = target {
|
let requests = if targets.is_empty() {
|
||||||
let request = ToolchainRequest::parse(&target);
|
vec![PythonDownloadRequest::default()]
|
||||||
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
|
|
||||||
} else {
|
} else {
|
||||||
ToolchainRequest::default()
|
targets
|
||||||
|
.iter()
|
||||||
|
.map(|target| parse_target(target, printer))
|
||||||
|
.collect::<Result<_>>()?
|
||||||
};
|
};
|
||||||
|
|
||||||
if let Some(toolchain) = toolchains
|
let installed_toolchains: Vec<_> = toolchains.find_all()?.collect();
|
||||||
.find_all()?
|
let mut unfilled_requests = Vec::new();
|
||||||
.find(|toolchain| toolchain.satisfies(&request))
|
for request in requests {
|
||||||
{
|
if let Some(toolchain) = installed_toolchains
|
||||||
writeln!(
|
.iter()
|
||||||
printer.stderr(),
|
.find(|toolchain| request.satisfied_by_key(toolchain.key()))
|
||||||
"Found installed toolchain '{}'",
|
{
|
||||||
toolchain.key()
|
writeln!(
|
||||||
)?;
|
printer.stderr(),
|
||||||
|
"Found installed toolchain '{}' that satisfies {request}",
|
||||||
if force {
|
toolchain.key()
|
||||||
writeln!(printer.stderr(), "Forcing reinstallation...")?;
|
)?;
|
||||||
} else {
|
if force {
|
||||||
if matches!(request, ToolchainRequest::Any) {
|
unfilled_requests.push(request);
|
||||||
writeln!(
|
|
||||||
printer.stderr(),
|
|
||||||
"A toolchain is already installed. Use `uv toolchain install <request>` to install a specific toolchain.",
|
|
||||||
)?;
|
|
||||||
} else {
|
|
||||||
writeln!(
|
|
||||||
printer.stderr(),
|
|
||||||
"Already installed at {}",
|
|
||||||
toolchain.path().user_display()
|
|
||||||
)?;
|
|
||||||
}
|
}
|
||||||
return Ok(ExitStatus::Success);
|
} else {
|
||||||
|
unfilled_requests.push(request);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fill platform information missing from the request
|
if unfilled_requests.is_empty() {
|
||||||
let request = PythonDownloadRequest::from_request(request)?.fill()?;
|
if targets.is_empty() {
|
||||||
|
writeln!(
|
||||||
|
printer.stderr(),
|
||||||
|
"A toolchain is already installed. Use `uv toolchain install <request>` to install a specific toolchain.",
|
||||||
|
)?;
|
||||||
|
} else if targets.len() > 1 {
|
||||||
|
writeln!(
|
||||||
|
printer.stderr(),
|
||||||
|
"All requested toolchains already installed."
|
||||||
|
)?;
|
||||||
|
} else {
|
||||||
|
writeln!(printer.stderr(), "Requested toolchain already installed.")?;
|
||||||
|
}
|
||||||
|
return Ok(ExitStatus::Success);
|
||||||
|
}
|
||||||
|
|
||||||
// Find the corresponding download
|
let s = if unfilled_requests.len() == 1 {
|
||||||
let download = PythonDownload::from_request(&request)?;
|
""
|
||||||
let version = download.python_version();
|
} else {
|
||||||
|
"s"
|
||||||
|
};
|
||||||
|
writeln!(
|
||||||
|
printer.stderr(),
|
||||||
|
"Installing {} toolchain{s}",
|
||||||
|
unfilled_requests.len()
|
||||||
|
)?;
|
||||||
|
|
||||||
|
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::<Result<Vec<_>, uv_toolchain::downloads::Error>>()?;
|
||||||
|
|
||||||
// Construct a client
|
// Construct a client
|
||||||
let client = uv_client::BaseClientBuilder::new()
|
let client = uv_client::BaseClientBuilder::new()
|
||||||
|
@ -91,24 +105,53 @@ pub(crate) async fn install(
|
||||||
.native_tls(native_tls)
|
.native_tls(native_tls)
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
writeln!(printer.stderr(), "Downloading {}", download.key())?;
|
let mut tasks = futures::stream::iter(downloads.iter())
|
||||||
let result = download.fetch(&client, toolchain_dir).await?;
|
.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 {
|
let mut results = Vec::new();
|
||||||
// Note we should only encounter `AlreadyAvailable` if there's a race condition
|
while let Some(task) = tasks.next().await {
|
||||||
// TODO(zanieb): We should lock the toolchain directory on fetch
|
let (version, result) = task;
|
||||||
DownloadResult::AlreadyAvailable(path) => path,
|
let path = match result? {
|
||||||
DownloadResult::Fetched(path) => path,
|
// We should only encounter already-available during concurrent installs
|
||||||
};
|
DownloadResult::AlreadyAvailable(path) => path,
|
||||||
|
DownloadResult::Fetched(path) => {
|
||||||
let installed = InstalledToolchain::new(path)?;
|
writeln!(
|
||||||
installed.ensure_externally_managed()?;
|
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!(
|
writeln!(
|
||||||
printer.stderr(),
|
printer.stderr(),
|
||||||
"Installed Python {version} to {}",
|
"Installed {} toolchain{s} in {}s",
|
||||||
installed.path().user_display()
|
downloads.len(),
|
||||||
|
start.elapsed().as_secs()
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
Ok(ExitStatus::Success)
|
Ok(ExitStatus::Success)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn parse_target(target: &str, printer: Printer) -> Result<PythonDownloadRequest, Error> {
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
|
@ -796,7 +796,7 @@ async fn run() -> Result<ExitStatus> {
|
||||||
let cache = cache.init()?;
|
let cache = cache.init()?;
|
||||||
|
|
||||||
commands::toolchain_install(
|
commands::toolchain_install(
|
||||||
args.target,
|
args.targets,
|
||||||
args.force,
|
args.force,
|
||||||
globals.native_tls,
|
globals.native_tls,
|
||||||
globals.connectivity,
|
globals.connectivity,
|
||||||
|
|
|
@ -257,7 +257,7 @@ impl ToolchainListSettings {
|
||||||
#[allow(clippy::struct_excessive_bools)]
|
#[allow(clippy::struct_excessive_bools)]
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub(crate) struct ToolchainInstallSettings {
|
pub(crate) struct ToolchainInstallSettings {
|
||||||
pub(crate) target: Option<String>,
|
pub(crate) targets: Vec<String>,
|
||||||
pub(crate) force: bool,
|
pub(crate) force: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -268,9 +268,9 @@ impl ToolchainInstallSettings {
|
||||||
args: ToolchainInstallArgs,
|
args: ToolchainInstallArgs,
|
||||||
_filesystem: Option<FilesystemOptions>,
|
_filesystem: Option<FilesystemOptions>,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
let ToolchainInstallArgs { target, force } = args;
|
let ToolchainInstallArgs { targets, force } = args;
|
||||||
|
|
||||||
Self { target, force }
|
Self { targets, force }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue