Skip to content

Commit bfdea67

Browse files
committed
Implement uv toolchain install
1 parent c6da4f1 commit bfdea67

File tree

8 files changed

+201
-9
lines changed

8 files changed

+201
-9
lines changed

crates/uv-toolchain/src/discovery.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -1037,7 +1037,7 @@ impl VersionRequest {
10371037
}
10381038
}
10391039

1040-
fn matches_version(self, version: &PythonVersion) -> bool {
1040+
pub(crate) fn matches_version(self, version: &PythonVersion) -> bool {
10411041
match self {
10421042
Self::Any => true,
10431043
Self::Major(major) => version.major() == major,

crates/uv-toolchain/src/managed.rs

+50-7
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,16 @@ use std::io::{self, Write};
66
use std::path::{Path, PathBuf};
77
use std::str::FromStr;
88
use thiserror::Error;
9+
use tracing::warn;
910

1011
use uv_state::{StateBucket, StateStore};
1112

1213
use crate::downloads::Error as DownloadError;
13-
use crate::implementation::Error as ImplementationError;
14+
use crate::implementation::{Error as ImplementationError, ImplementationName};
1415
use crate::platform::Error as PlatformError;
1516
use crate::platform::{Arch, Libc, Os};
1617
use crate::python_version::PythonVersion;
18+
use crate::ToolchainRequest;
1719
use uv_fs::Simplified;
1820

1921
#[derive(Error, Debug)]
@@ -42,8 +44,10 @@ pub enum Error {
4244
#[source]
4345
err: io::Error,
4446
},
45-
#[error("Failed to parse toolchain directory name: {0}")]
47+
#[error("Failed to read toolchain directory name: {0}")]
4648
NameError(String),
49+
#[error("Failed to parse toolchain directory name `{0}`: {1}")]
50+
NameParseError(String, String),
4751
}
4852
/// A collection of uv-managed Python toolchains installed on the current system.
4953
#[derive(Debug, Clone)]
@@ -137,7 +141,13 @@ impl InstalledToolchains {
137141
};
138142
Ok(dirs
139143
.into_iter()
140-
.map(|path| InstalledToolchain::new(path).unwrap())
144+
.filter_map(|path| {
145+
InstalledToolchain::new(path)
146+
.inspect_err(|err| {
147+
warn!("Ignoring malformed toolchain entry:\n {err}");
148+
})
149+
.ok()
150+
})
141151
.rev())
142152
}
143153

@@ -193,7 +203,9 @@ pub struct InstalledToolchain {
193203
path: PathBuf,
194204
/// The Python version of the toolchain.
195205
python_version: PythonVersion,
196-
/// An install key for the toolchain
206+
/// The name of the Python implementation of the toolchain.
207+
implementation: ImplementationName,
208+
/// An install key for the toolchain.
197209
key: String,
198210
}
199211

@@ -205,14 +217,25 @@ impl InstalledToolchain {
205217
.to_str()
206218
.ok_or(Error::NameError("not a valid string".to_string()))?
207219
.to_string();
208-
let python_version = PythonVersion::from_str(key.split('-').nth(1).ok_or(
209-
Error::NameError("not enough `-`-separated values".to_string()),
220+
let parts: Vec<&str> = key.split('-').collect();
221+
let implementation = ImplementationName::from_str(parts.first().ok_or(
222+
Error::NameParseError(key.clone(), "not enough `-`-separated values".to_string()),
210223
)?)
211-
.map_err(|err| Error::NameError(format!("invalid Python version: {err}")))?;
224+
.map_err(|err| {
225+
Error::NameParseError(key.clone(), format!("invalid Python implementation: {err}"))
226+
})?;
227+
let python_version = PythonVersion::from_str(parts.get(1).ok_or(Error::NameParseError(
228+
key.clone(),
229+
"not enough `-`-separated values".to_string(),
230+
))?)
231+
.map_err(|err| {
232+
Error::NameParseError(key.clone(), format!("invalid Python version: {err}"))
233+
})?;
212234

213235
Ok(Self {
214236
path,
215237
python_version,
238+
implementation,
216239
key,
217240
})
218241
}
@@ -238,6 +261,26 @@ impl InstalledToolchain {
238261
pub fn key(&self) -> &str {
239262
&self.key
240263
}
264+
265+
pub fn satisfies(&self, request: &ToolchainRequest) -> bool {
266+
match request {
267+
ToolchainRequest::File(path) => self.executable() == *path,
268+
ToolchainRequest::Any => true,
269+
ToolchainRequest::Directory(path) => self.path() == *path,
270+
ToolchainRequest::ExecutableName(name) => self
271+
.executable()
272+
.file_name()
273+
.map_or(false, |filename| filename.to_string_lossy() == *name),
274+
ToolchainRequest::Implementation(implementation) => {
275+
*implementation == self.implementation
276+
}
277+
ToolchainRequest::ImplementationVersion(implementation, version) => {
278+
*implementation == self.implementation
279+
&& version.matches_version(&self.python_version)
280+
}
281+
ToolchainRequest::Version(version) => version.matches_version(&self.python_version),
282+
}
283+
}
241284
}
242285

243286
/// Generate a platform portion of a key from the environment.

crates/uv/src/cli.rs

+12
Original file line numberDiff line numberDiff line change
@@ -1988,6 +1988,9 @@ pub(crate) struct ToolchainNamespace {
19881988
pub(crate) enum ToolchainCommand {
19891989
/// List the available toolchains.
19901990
List(ToolchainListArgs),
1991+
1992+
/// Download and install a specific toolchain.
1993+
Install(ToolchainInstallArgs),
19911994
}
19921995

19931996
#[derive(Args)]
@@ -2002,6 +2005,15 @@ pub(crate) struct ToolchainListArgs {
20022005
pub(crate) only_installed: bool,
20032006
}
20042007

2008+
#[derive(Args)]
2009+
#[allow(clippy::struct_excessive_bools)]
2010+
pub(crate) struct ToolchainInstallArgs {
2011+
/// The toolchain to fetch.
2012+
///
2013+
/// If not provided, the latest available version will be installed.
2014+
pub(crate) target: Option<String>,
2015+
}
2016+
20052017
#[derive(Args)]
20062018
pub(crate) struct IndexArgs {
20072019
/// The URL of the Python package index (by default: <https://pypi.org/simple>).

crates/uv/src/commands/mod.rs

+1
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ pub(crate) use project::sync::sync;
2222
#[cfg(feature = "self-update")]
2323
pub(crate) use self_update::self_update;
2424
pub(crate) use tool::run::run as run_tool;
25+
pub(crate) use toolchain::install::install as toolchain_install;
2526
pub(crate) use toolchain::list::list as toolchain_list;
2627
use uv_cache::Cache;
2728
use uv_fs::Simplified;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
use anyhow::Result;
2+
use std::fmt::Write;
3+
use uv_cache::Cache;
4+
use uv_client::Connectivity;
5+
use uv_configuration::PreviewMode;
6+
use uv_fs::Simplified;
7+
use uv_toolchain::downloads::{DownloadResult, PythonDownload, PythonDownloadRequest};
8+
use uv_toolchain::managed::InstalledToolchains;
9+
use uv_toolchain::ToolchainRequest;
10+
use uv_warnings::warn_user;
11+
12+
use crate::commands::ExitStatus;
13+
use crate::printer::Printer;
14+
15+
/// Download and install a Python toolchain.
16+
#[allow(clippy::too_many_arguments)]
17+
pub(crate) async fn install(
18+
target: Option<String>,
19+
native_tls: bool,
20+
connectivity: Connectivity,
21+
preview: PreviewMode,
22+
_cache: &Cache,
23+
printer: Printer,
24+
) -> Result<ExitStatus> {
25+
if preview.is_disabled() {
26+
warn_user!("`uv toolchain fetch` is experimental and may change without warning.");
27+
}
28+
29+
let toolchains = InstalledToolchains::from_settings()?.init()?;
30+
let toolchain_dir = toolchains.root();
31+
32+
let request = if let Some(target) = target {
33+
let request = ToolchainRequest::parse(&target);
34+
match request {
35+
ToolchainRequest::Any => (),
36+
ToolchainRequest::Directory(_)
37+
| ToolchainRequest::ExecutableName(_)
38+
| ToolchainRequest::File(_) => {
39+
writeln!(printer.stderr(), "Invalid toolchain request '{target}'")?;
40+
return Ok(ExitStatus::Failure);
41+
}
42+
_ => {
43+
writeln!(printer.stderr(), "Looking for {request}")?;
44+
}
45+
}
46+
request
47+
} else {
48+
writeln!(printer.stderr(), "Using latest Python version")?;
49+
ToolchainRequest::default()
50+
};
51+
52+
if let Some(toolchain) = toolchains
53+
.find_all()?
54+
.find(|toolchain| toolchain.satisfies(&request))
55+
{
56+
writeln!(
57+
printer.stderr(),
58+
"Found installed toolchain '{}'",
59+
toolchain.key()
60+
)?;
61+
writeln!(
62+
printer.stderr(),
63+
"Already installed at {}",
64+
toolchain.path().user_display()
65+
)?;
66+
return Ok(ExitStatus::Success);
67+
}
68+
69+
// Fill platform information missing from the request
70+
let request = PythonDownloadRequest::from_request(request)?.fill()?;
71+
72+
// Find the corresponding download
73+
let download = PythonDownload::from_request(&request)?;
74+
let version = download.python_version();
75+
76+
// Construct a client
77+
let client = uv_client::BaseClientBuilder::new()
78+
.connectivity(connectivity)
79+
.native_tls(native_tls)
80+
.build();
81+
82+
writeln!(printer.stderr(), "Downloading {}", download.key())?;
83+
let result = download.fetch(&client, toolchain_dir).await?;
84+
85+
let path = match result {
86+
// Note we should only encounter `AlreadyAvailable` if there's a race condition
87+
// TODO(zanieb): We should lock the toolchain directory on fetch
88+
DownloadResult::AlreadyAvailable(path) => path,
89+
DownloadResult::Fetched(path) => path,
90+
};
91+
92+
writeln!(
93+
printer.stderr(),
94+
"Installed Python {version} to {}",
95+
path.user_display()
96+
)?;
97+
98+
Ok(ExitStatus::Success)
99+
}
+1
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
1+
pub(crate) mod install;
12
pub(crate) mod list;

crates/uv/src/main.rs

+19
Original file line numberDiff line numberDiff line change
@@ -682,6 +682,25 @@ async fn run() -> Result<ExitStatus> {
682682

683683
commands::toolchain_list(args.includes, globals.preview, &cache, printer).await
684684
}
685+
Commands::Toolchain(ToolchainNamespace {
686+
command: ToolchainCommand::Install(args),
687+
}) => {
688+
// Resolve the settings from the command-line arguments and workspace configuration.
689+
let args = settings::ToolchainInstallSettings::resolve(args, workspace);
690+
691+
// Initialize the cache.
692+
let cache = cache.init()?;
693+
694+
commands::toolchain_install(
695+
args.target,
696+
globals.native_tls,
697+
globals.connectivity,
698+
globals.preview,
699+
&cache,
700+
printer,
701+
)
702+
.await
703+
}
685704
}
686705
}
687706

crates/uv/src/settings.rs

+18-1
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ use uv_workspace::{Combine, PipOptions, Workspace};
2323
use crate::cli::{
2424
ColorChoice, GlobalArgs, LockArgs, Maybe, PipCheckArgs, PipCompileArgs, PipFreezeArgs,
2525
PipInstallArgs, PipListArgs, PipShowArgs, PipSyncArgs, PipUninstallArgs, RunArgs, SyncArgs,
26-
ToolRunArgs, ToolchainListArgs, VenvArgs,
26+
ToolRunArgs, ToolchainInstallArgs, ToolchainListArgs, VenvArgs,
2727
};
2828
use crate::commands::ListFormat;
2929

@@ -266,6 +266,23 @@ impl ToolchainListSettings {
266266
}
267267
}
268268

269+
/// The resolved settings to use for a `toolchain fetch` invocation.
270+
#[allow(clippy::struct_excessive_bools)]
271+
#[derive(Debug, Clone)]
272+
pub(crate) struct ToolchainInstallSettings {
273+
pub(crate) target: Option<String>,
274+
}
275+
276+
impl ToolchainInstallSettings {
277+
/// Resolve the [`ToolchainInstallSettings`] from the CLI and workspace configuration.
278+
#[allow(clippy::needless_pass_by_value)]
279+
pub(crate) fn resolve(args: ToolchainInstallArgs, _workspace: Option<Workspace>) -> Self {
280+
let ToolchainInstallArgs { target } = args;
281+
282+
Self { target }
283+
}
284+
}
285+
269286
/// The resolved settings to use for a `sync` invocation.
270287
#[allow(clippy::struct_excessive_bools, dead_code)]
271288
#[derive(Debug, Clone)]

0 commit comments

Comments
 (0)