mirror of
https://github.com/astral-sh/uv.git
synced 2025-10-02 15:01:16 +00:00
Add uv toolchain install
(#4164)
Adds a command (following #4163) to download and install specific toolchains. While we fetch toolchains on demand, this is useful for, e.g., pre-downloading a toolchain in a Docker image build. ~I kind of think we should call this `install` instead of `fetch`~ I changed the name from `fetch` to `install`.
This commit is contained in:
parent
90947a933d
commit
14d535f384
8 changed files with 204 additions and 10 deletions
|
@ -1126,7 +1126,7 @@ impl VersionRequest {
|
|||
}
|
||||
}
|
||||
|
||||
fn matches_version(self, version: &PythonVersion) -> bool {
|
||||
pub(crate) fn matches_version(self, version: &PythonVersion) -> bool {
|
||||
match self {
|
||||
Self::Any => true,
|
||||
Self::Major(major) => version.major() == major,
|
||||
|
|
|
@ -6,14 +6,16 @@ use std::io::{self, Write};
|
|||
use std::path::{Path, PathBuf};
|
||||
use std::str::FromStr;
|
||||
use thiserror::Error;
|
||||
use tracing::warn;
|
||||
|
||||
use uv_state::{StateBucket, StateStore};
|
||||
|
||||
use crate::downloads::Error as DownloadError;
|
||||
use crate::implementation::Error as ImplementationError;
|
||||
use crate::implementation::{Error as ImplementationError, ImplementationName};
|
||||
use crate::platform::Error as PlatformError;
|
||||
use crate::platform::{Arch, Libc, Os};
|
||||
use crate::python_version::PythonVersion;
|
||||
use crate::ToolchainRequest;
|
||||
use uv_fs::Simplified;
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
|
@ -42,8 +44,10 @@ pub enum Error {
|
|||
#[source]
|
||||
err: io::Error,
|
||||
},
|
||||
#[error("Failed to parse toolchain directory name: {0}")]
|
||||
#[error("Failed to read toolchain directory name: {0}")]
|
||||
NameError(String),
|
||||
#[error("Failed to parse toolchain directory name `{0}`: {1}")]
|
||||
NameParseError(String, String),
|
||||
}
|
||||
/// A collection of uv-managed Python toolchains installed on the current system.
|
||||
#[derive(Debug, Clone)]
|
||||
|
@ -137,7 +141,13 @@ impl InstalledToolchains {
|
|||
};
|
||||
Ok(dirs
|
||||
.into_iter()
|
||||
.map(|path| InstalledToolchain::new(path).unwrap())
|
||||
.filter_map(|path| {
|
||||
InstalledToolchain::new(path)
|
||||
.inspect_err(|err| {
|
||||
warn!("Ignoring malformed toolchain entry:\n {err}");
|
||||
})
|
||||
.ok()
|
||||
})
|
||||
.rev())
|
||||
}
|
||||
|
||||
|
@ -193,7 +203,9 @@ pub struct InstalledToolchain {
|
|||
path: PathBuf,
|
||||
/// The Python version of the toolchain.
|
||||
python_version: PythonVersion,
|
||||
/// An install key for the toolchain
|
||||
/// The name of the Python implementation of the toolchain.
|
||||
implementation: ImplementationName,
|
||||
/// An install key for the toolchain.
|
||||
key: String,
|
||||
}
|
||||
|
||||
|
@ -205,14 +217,27 @@ impl InstalledToolchain {
|
|||
.to_str()
|
||||
.ok_or(Error::NameError("not a valid string".to_string()))?
|
||||
.to_string();
|
||||
let python_version = PythonVersion::from_str(key.split('-').nth(1).ok_or(
|
||||
Error::NameError("not enough `-`-separated values".to_string()),
|
||||
)?)
|
||||
.map_err(|err| Error::NameError(format!("invalid Python version: {err}")))?;
|
||||
|
||||
let parts = key.split('-').collect::<Vec<_>>();
|
||||
let [implementation, version, ..] = parts.as_slice() else {
|
||||
return Err(Error::NameParseError(
|
||||
key.clone(),
|
||||
"not enough `-`-separated values".to_string(),
|
||||
));
|
||||
};
|
||||
|
||||
let implementation = ImplementationName::from_str(implementation).map_err(|err| {
|
||||
Error::NameParseError(key.clone(), format!("invalid Python implementation: {err}"))
|
||||
})?;
|
||||
|
||||
let python_version = PythonVersion::from_str(version).map_err(|err| {
|
||||
Error::NameParseError(key.clone(), format!("invalid Python version: {err}"))
|
||||
})?;
|
||||
|
||||
Ok(Self {
|
||||
path,
|
||||
python_version,
|
||||
implementation,
|
||||
key,
|
||||
})
|
||||
}
|
||||
|
@ -238,6 +263,26 @@ impl InstalledToolchain {
|
|||
pub fn key(&self) -> &str {
|
||||
&self.key
|
||||
}
|
||||
|
||||
pub fn satisfies(&self, request: &ToolchainRequest) -> bool {
|
||||
match request {
|
||||
ToolchainRequest::File(path) => self.executable() == *path,
|
||||
ToolchainRequest::Any => true,
|
||||
ToolchainRequest::Directory(path) => self.path() == *path,
|
||||
ToolchainRequest::ExecutableName(name) => self
|
||||
.executable()
|
||||
.file_name()
|
||||
.map_or(false, |filename| filename.to_string_lossy() == *name),
|
||||
ToolchainRequest::Implementation(implementation) => {
|
||||
*implementation == self.implementation
|
||||
}
|
||||
ToolchainRequest::ImplementationVersion(implementation, version) => {
|
||||
*implementation == self.implementation
|
||||
&& version.matches_version(&self.python_version)
|
||||
}
|
||||
ToolchainRequest::Version(version) => version.matches_version(&self.python_version),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Generate a platform portion of a key from the environment.
|
||||
|
|
|
@ -1988,6 +1988,9 @@ pub(crate) struct ToolchainNamespace {
|
|||
pub(crate) enum ToolchainCommand {
|
||||
/// List the available toolchains.
|
||||
List(ToolchainListArgs),
|
||||
|
||||
/// Download and install a specific toolchain.
|
||||
Install(ToolchainInstallArgs),
|
||||
}
|
||||
|
||||
#[derive(Args)]
|
||||
|
@ -2002,6 +2005,15 @@ pub(crate) struct ToolchainListArgs {
|
|||
pub(crate) only_installed: bool,
|
||||
}
|
||||
|
||||
#[derive(Args)]
|
||||
#[allow(clippy::struct_excessive_bools)]
|
||||
pub(crate) struct ToolchainInstallArgs {
|
||||
/// The toolchain to fetch.
|
||||
///
|
||||
/// If not provided, the latest available version will be installed.
|
||||
pub(crate) target: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Args)]
|
||||
pub(crate) struct IndexArgs {
|
||||
/// The URL of the Python package index (by default: <https://pypi.org/simple>).
|
||||
|
|
|
@ -22,6 +22,7 @@ pub(crate) use project::sync::sync;
|
|||
#[cfg(feature = "self-update")]
|
||||
pub(crate) use self_update::self_update;
|
||||
pub(crate) use tool::run::run as run_tool;
|
||||
pub(crate) use toolchain::install::install as toolchain_install;
|
||||
pub(crate) use toolchain::list::list as toolchain_list;
|
||||
use uv_cache::Cache;
|
||||
use uv_fs::Simplified;
|
||||
|
|
99
crates/uv/src/commands/toolchain/install.rs
Normal file
99
crates/uv/src/commands/toolchain/install.rs
Normal file
|
@ -0,0 +1,99 @@
|
|||
use anyhow::Result;
|
||||
use std::fmt::Write;
|
||||
use uv_cache::Cache;
|
||||
use uv_client::Connectivity;
|
||||
use uv_configuration::PreviewMode;
|
||||
use uv_fs::Simplified;
|
||||
use uv_toolchain::downloads::{DownloadResult, PythonDownload, PythonDownloadRequest};
|
||||
use uv_toolchain::managed::InstalledToolchains;
|
||||
use uv_toolchain::ToolchainRequest;
|
||||
use uv_warnings::warn_user;
|
||||
|
||||
use crate::commands::ExitStatus;
|
||||
use crate::printer::Printer;
|
||||
|
||||
/// Download and install a Python toolchain.
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub(crate) async fn install(
|
||||
target: Option<String>,
|
||||
native_tls: bool,
|
||||
connectivity: Connectivity,
|
||||
preview: PreviewMode,
|
||||
_cache: &Cache,
|
||||
printer: Printer,
|
||||
) -> Result<ExitStatus> {
|
||||
if preview.is_disabled() {
|
||||
warn_user!("`uv toolchain fetch` is experimental and may change without warning.");
|
||||
}
|
||||
|
||||
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
|
||||
} else {
|
||||
writeln!(printer.stderr(), "Using latest Python version")?;
|
||||
ToolchainRequest::default()
|
||||
};
|
||||
|
||||
if let Some(toolchain) = toolchains
|
||||
.find_all()?
|
||||
.find(|toolchain| toolchain.satisfies(&request))
|
||||
{
|
||||
writeln!(
|
||||
printer.stderr(),
|
||||
"Found installed toolchain '{}'",
|
||||
toolchain.key()
|
||||
)?;
|
||||
writeln!(
|
||||
printer.stderr(),
|
||||
"Already installed at {}",
|
||||
toolchain.path().user_display()
|
||||
)?;
|
||||
return Ok(ExitStatus::Success);
|
||||
}
|
||||
|
||||
// Fill platform information missing from the request
|
||||
let request = PythonDownloadRequest::from_request(request)?.fill()?;
|
||||
|
||||
// Find the corresponding download
|
||||
let download = PythonDownload::from_request(&request)?;
|
||||
let version = download.python_version();
|
||||
|
||||
// Construct a client
|
||||
let client = uv_client::BaseClientBuilder::new()
|
||||
.connectivity(connectivity)
|
||||
.native_tls(native_tls)
|
||||
.build();
|
||||
|
||||
writeln!(printer.stderr(), "Downloading {}", download.key())?;
|
||||
let result = download.fetch(&client, toolchain_dir).await?;
|
||||
|
||||
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,
|
||||
};
|
||||
|
||||
writeln!(
|
||||
printer.stderr(),
|
||||
"Installed Python {version} to {}",
|
||||
path.user_display()
|
||||
)?;
|
||||
|
||||
Ok(ExitStatus::Success)
|
||||
}
|
|
@ -1 +1,2 @@
|
|||
pub(crate) mod install;
|
||||
pub(crate) mod list;
|
||||
|
|
|
@ -684,6 +684,25 @@ async fn run() -> Result<ExitStatus> {
|
|||
|
||||
commands::toolchain_list(args.includes, globals.preview, &cache, printer).await
|
||||
}
|
||||
Commands::Toolchain(ToolchainNamespace {
|
||||
command: ToolchainCommand::Install(args),
|
||||
}) => {
|
||||
// Resolve the settings from the command-line arguments and workspace configuration.
|
||||
let args = settings::ToolchainInstallSettings::resolve(args, workspace);
|
||||
|
||||
// Initialize the cache.
|
||||
let cache = cache.init()?;
|
||||
|
||||
commands::toolchain_install(
|
||||
args.target,
|
||||
globals.native_tls,
|
||||
globals.connectivity,
|
||||
globals.preview,
|
||||
&cache,
|
||||
printer,
|
||||
)
|
||||
.await
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -23,7 +23,7 @@ use uv_workspace::{Combine, PipOptions, Workspace};
|
|||
use crate::cli::{
|
||||
ColorChoice, GlobalArgs, LockArgs, Maybe, PipCheckArgs, PipCompileArgs, PipFreezeArgs,
|
||||
PipInstallArgs, PipListArgs, PipShowArgs, PipSyncArgs, PipUninstallArgs, RunArgs, SyncArgs,
|
||||
ToolRunArgs, ToolchainListArgs, VenvArgs,
|
||||
ToolRunArgs, ToolchainInstallArgs, ToolchainListArgs, VenvArgs,
|
||||
};
|
||||
use crate::commands::ListFormat;
|
||||
|
||||
|
@ -266,6 +266,23 @@ impl ToolchainListSettings {
|
|||
}
|
||||
}
|
||||
|
||||
/// The resolved settings to use for a `toolchain fetch` invocation.
|
||||
#[allow(clippy::struct_excessive_bools)]
|
||||
#[derive(Debug, Clone)]
|
||||
pub(crate) struct ToolchainInstallSettings {
|
||||
pub(crate) target: Option<String>,
|
||||
}
|
||||
|
||||
impl ToolchainInstallSettings {
|
||||
/// Resolve the [`ToolchainInstallSettings`] from the CLI and workspace configuration.
|
||||
#[allow(clippy::needless_pass_by_value)]
|
||||
pub(crate) fn resolve(args: ToolchainInstallArgs, _workspace: Option<Workspace>) -> Self {
|
||||
let ToolchainInstallArgs { target } = args;
|
||||
|
||||
Self { target }
|
||||
}
|
||||
}
|
||||
|
||||
/// The resolved settings to use for a `sync` invocation.
|
||||
#[allow(clippy::struct_excessive_bools, dead_code)]
|
||||
#[derive(Debug, Clone)]
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue