Skip to content

Commit accbb9b

Browse files
authored
Add uv toolchain find (#4206)
Adds a command to find a toolchain on the system. Right now, it displays the path to the first matching toolchain. We'll probably have more rich output in the future (after implementing `toolchain show`). The eventual plan (separate from here) is to port all of the toolchain discovery tests to use this command. I'll add a few tests for this command here anyway.
1 parent b7fb0b4 commit accbb9b

File tree

9 files changed

+258
-2
lines changed

9 files changed

+258
-2
lines changed

crates/uv-toolchain/src/lib.rs

+1
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ pub use crate::discovery::{
66
ToolchainSource, ToolchainSources, VersionRequest,
77
};
88
pub use crate::environment::PythonEnvironment;
9+
pub use crate::implementation::ImplementationName;
910
pub use crate::interpreter::Interpreter;
1011
pub use crate::pointer_size::PointerSize;
1112
pub use crate::prefix::Prefix;

crates/uv/src/cli.rs

+11
Original file line numberDiff line numberDiff line change
@@ -1712,6 +1712,10 @@ pub(crate) enum ToolchainCommand {
17121712

17131713
/// Download and install a specific toolchain.
17141714
Install(ToolchainInstallArgs),
1715+
1716+
/// Search for a toolchain
1717+
#[command(disable_version_flag = true)]
1718+
Find(ToolchainFindArgs),
17151719
}
17161720

17171721
#[derive(Args)]
@@ -1743,6 +1747,13 @@ pub(crate) struct ToolchainInstallArgs {
17431747
pub(crate) force: bool,
17441748
}
17451749

1750+
#[derive(Args)]
1751+
#[allow(clippy::struct_excessive_bools)]
1752+
pub(crate) struct ToolchainFindArgs {
1753+
/// The toolchain request.
1754+
pub(crate) request: Option<String>,
1755+
}
1756+
17461757
#[derive(Args)]
17471758
#[allow(clippy::struct_excessive_bools)]
17481759
pub(crate) struct IndexArgs {

crates/uv/src/commands/mod.rs

+1
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ pub(crate) use project::sync::sync;
2424
#[cfg(feature = "self-update")]
2525
pub(crate) use self_update::self_update;
2626
pub(crate) use tool::run::run as run_tool;
27+
pub(crate) use toolchain::find::find as toolchain_find;
2728
pub(crate) use toolchain::install::install as toolchain_install;
2829
pub(crate) use toolchain::list::list as toolchain_list;
2930
use uv_cache::Cache;
+43
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
use anyhow::Result;
2+
use std::fmt::Write;
3+
4+
use uv_cache::Cache;
5+
use uv_configuration::PreviewMode;
6+
use uv_fs::Simplified;
7+
use uv_toolchain::{SystemPython, Toolchain, ToolchainRequest};
8+
use uv_warnings::warn_user;
9+
10+
use crate::commands::ExitStatus;
11+
use crate::printer::Printer;
12+
13+
/// Find a toolchain.
14+
#[allow(clippy::too_many_arguments)]
15+
pub(crate) async fn find(
16+
request: Option<String>,
17+
preview: PreviewMode,
18+
cache: &Cache,
19+
printer: Printer,
20+
) -> Result<ExitStatus> {
21+
if preview.is_disabled() {
22+
warn_user!("`uv toolchain find` is experimental and may change without warning.");
23+
}
24+
25+
let request = match request {
26+
Some(request) => ToolchainRequest::parse(&request),
27+
None => ToolchainRequest::Any,
28+
};
29+
let toolchain = Toolchain::find_requested(
30+
&request,
31+
SystemPython::Required,
32+
PreviewMode::Enabled,
33+
cache,
34+
)?;
35+
36+
writeln!(
37+
printer.stdout(),
38+
"{}",
39+
toolchain.interpreter().sys_executable().user_display()
40+
)?;
41+
42+
Ok(ExitStatus::Success)
43+
}
+1
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
1+
pub(crate) mod find;
12
pub(crate) mod install;
23
pub(crate) mod list;

crates/uv/src/main.rs

+11
Original file line numberDiff line numberDiff line change
@@ -797,6 +797,17 @@ async fn run() -> Result<ExitStatus> {
797797
)
798798
.await
799799
}
800+
Commands::Toolchain(ToolchainNamespace {
801+
command: ToolchainCommand::Find(args),
802+
}) => {
803+
// Resolve the settings from the command-line arguments and workspace configuration.
804+
let args = settings::ToolchainFindSettings::resolve(args, filesystem);
805+
806+
// Initialize the cache.
807+
let cache = cache.init()?;
808+
809+
commands::toolchain_find(args.request, globals.preview, &cache, printer).await
810+
}
800811
}
801812
}
802813

crates/uv/src/settings.rs

+19-1
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,8 @@ use crate::cli::{
2828
AddArgs, BuildArgs, ColorChoice, GlobalArgs, IndexArgs, InstallerArgs, LockArgs, Maybe,
2929
PipCheckArgs, PipCompileArgs, PipFreezeArgs, PipInstallArgs, PipListArgs, PipShowArgs,
3030
PipSyncArgs, PipUninstallArgs, RefreshArgs, RemoveArgs, ResolverArgs, ResolverInstallerArgs,
31-
RunArgs, SyncArgs, ToolRunArgs, ToolchainInstallArgs, ToolchainListArgs, VenvArgs,
31+
RunArgs, SyncArgs, ToolRunArgs, ToolchainFindArgs, ToolchainInstallArgs, ToolchainListArgs,
32+
VenvArgs,
3233
};
3334
use crate::commands::ListFormat;
3435

@@ -273,6 +274,23 @@ impl ToolchainInstallSettings {
273274
}
274275
}
275276

277+
/// The resolved settings to use for a `toolchain find` invocation.
278+
#[allow(clippy::struct_excessive_bools)]
279+
#[derive(Debug, Clone)]
280+
pub(crate) struct ToolchainFindSettings {
281+
pub(crate) request: Option<String>,
282+
}
283+
284+
impl ToolchainFindSettings {
285+
/// Resolve the [`ToolchainFindSettings`] from the CLI and workspace configuration.
286+
#[allow(clippy::needless_pass_by_value)]
287+
pub(crate) fn resolve(args: ToolchainFindArgs, _filesystem: Option<FilesystemOptions>) -> Self {
288+
let ToolchainFindArgs { request } = args;
289+
290+
Self { request }
291+
}
292+
}
293+
276294
/// The resolved settings to use for a `sync` invocation.
277295
#[allow(clippy::struct_excessive_bools, dead_code)]
278296
#[derive(Debug, Clone)]

crates/uv/tests/common/mod.rs

+29-1
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
use assert_cmd::assert::{Assert, OutputAssertExt};
55
use assert_cmd::Command;
66
use assert_fs::assert::PathAssert;
7-
use assert_fs::fixture::PathChild;
7+
use assert_fs::fixture::{ChildPath, PathChild};
88
use regex::Regex;
99
use std::borrow::BorrowMut;
1010
use std::env;
@@ -287,6 +287,34 @@ impl TestContext {
287287
command
288288
}
289289

290+
pub fn toolchains_dir(&self) -> ChildPath {
291+
self.temp_dir.child("toolchains")
292+
}
293+
294+
/// Create a `uv toolchain find` command with options shared across scenarios.
295+
pub fn toolchain_find(&self) -> std::process::Command {
296+
let mut command = std::process::Command::new(get_bin());
297+
command
298+
.arg("toolchain")
299+
.arg("find")
300+
.arg("--cache-dir")
301+
.arg(self.cache_dir.path())
302+
.env("VIRTUAL_ENV", self.venv.as_os_str())
303+
.env("UV_NO_WRAP", "1")
304+
.env("UV_TEST_PYTHON_PATH", "/dev/null")
305+
.env("UV_PREVIEW", "1")
306+
.env("UV_TOOLCHAIN_DIR", self.toolchains_dir().as_os_str())
307+
.current_dir(&self.temp_dir);
308+
309+
if cfg!(all(windows, debug_assertions)) {
310+
// TODO(konstin): Reduce stack usage in debug mode enough that the tests pass with the
311+
// default windows stack of 1MB
312+
command.env("UV_STACK_SIZE", (4 * 1024 * 1024).to_string());
313+
}
314+
315+
command
316+
}
317+
290318
/// Create a `uv run` command with options shared across scenarios.
291319
pub fn run(&self) -> std::process::Command {
292320
let mut command = self.run_without_exclude_newer();

crates/uv/tests/toolchain_find.rs

+142
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
#![cfg(all(feature = "python", feature = "pypi"))]
2+
3+
use common::{python_path_with_versions, uv_snapshot, TestContext};
4+
5+
mod common;
6+
7+
#[test]
8+
fn toolchain_find() {
9+
let context: TestContext = TestContext::new("3.12");
10+
11+
// No interpreters on the path
12+
uv_snapshot!(context.filters(), context.toolchain_find(), @r###"
13+
success: false
14+
exit_code: 2
15+
----- stdout -----
16+
17+
----- stderr -----
18+
error: No Python interpreters found in provided path, active virtual environment, or search path
19+
"###);
20+
21+
let python_path = python_path_with_versions(&context.temp_dir, &["3.11", "3.12"])
22+
.expect("Failed to create Python test path");
23+
24+
// Create some filters for the test interpreters, otherwise they'll be a path on the dev's machine
25+
// TODO(zanieb): Standardize this when writing more tests
26+
let python_path_filters = std::env::split_paths(&python_path)
27+
.zip(["3.11", "3.12"])
28+
.flat_map(|(path, version)| {
29+
TestContext::path_patterns(path)
30+
.into_iter()
31+
.map(move |pattern| {
32+
(
33+
format!("{pattern}python.*"),
34+
format!("[PYTHON-PATH-{version}]"),
35+
)
36+
})
37+
})
38+
.collect::<Vec<_>>();
39+
40+
let filters = python_path_filters
41+
.iter()
42+
.map(|(pattern, replacement)| (pattern.as_str(), replacement.as_str()))
43+
.chain(context.filters())
44+
.collect::<Vec<_>>();
45+
46+
// We find the first interpreter on the path
47+
uv_snapshot!(filters, context.toolchain_find()
48+
.env("UV_TEST_PYTHON_PATH", &python_path), @r###"
49+
success: true
50+
exit_code: 0
51+
----- stdout -----
52+
[PYTHON-PATH-3.11]
53+
54+
----- stderr -----
55+
"###);
56+
57+
// Request Python 3.12
58+
uv_snapshot!(filters, context.toolchain_find()
59+
.arg("3.12")
60+
.env("UV_TEST_PYTHON_PATH", &python_path), @r###"
61+
success: true
62+
exit_code: 0
63+
----- stdout -----
64+
[PYTHON-PATH-3.12]
65+
66+
----- stderr -----
67+
"###);
68+
69+
// Request Python 3.11
70+
uv_snapshot!(filters, context.toolchain_find()
71+
.arg("3.11")
72+
.env("UV_TEST_PYTHON_PATH", &python_path), @r###"
73+
success: true
74+
exit_code: 0
75+
----- stdout -----
76+
[PYTHON-PATH-3.11]
77+
78+
----- stderr -----
79+
"###);
80+
81+
// Request CPython
82+
uv_snapshot!(filters, context.toolchain_find()
83+
.arg("cpython")
84+
.env("UV_TEST_PYTHON_PATH", &python_path), @r###"
85+
success: true
86+
exit_code: 0
87+
----- stdout -----
88+
[PYTHON-PATH-3.11]
89+
90+
----- stderr -----
91+
"###);
92+
93+
// Request CPython 3.12
94+
uv_snapshot!(filters, context.toolchain_find()
95+
96+
.env("UV_TEST_PYTHON_PATH", &python_path), @r###"
97+
success: true
98+
exit_code: 0
99+
----- stdout -----
100+
[PYTHON-PATH-3.12]
101+
102+
----- stderr -----
103+
"###);
104+
105+
// Request PyPy
106+
uv_snapshot!(filters, context.toolchain_find()
107+
.arg("pypy")
108+
.env("UV_TEST_PYTHON_PATH", &python_path), @r###"
109+
success: false
110+
exit_code: 2
111+
----- stdout -----
112+
113+
----- stderr -----
114+
error: No interpreter found for PyPy in provided path, active virtual environment, or search path
115+
"###);
116+
117+
// Swap the order (but don't change the filters to preserve our indices)
118+
let python_path = python_path_with_versions(&context.temp_dir, &["3.12", "3.11"])
119+
.expect("Failed to create Python test path");
120+
121+
uv_snapshot!(filters, context.toolchain_find()
122+
.env("UV_TEST_PYTHON_PATH", &python_path), @r###"
123+
success: true
124+
exit_code: 0
125+
----- stdout -----
126+
[PYTHON-PATH-3.12]
127+
128+
----- stderr -----
129+
"###);
130+
131+
// Request Python 3.11
132+
uv_snapshot!(filters, context.toolchain_find()
133+
.arg("3.11")
134+
.env("UV_TEST_PYTHON_PATH", &python_path), @r###"
135+
success: true
136+
exit_code: 0
137+
----- stdout -----
138+
[PYTHON-PATH-3.11]
139+
140+
----- stderr -----
141+
"###);
142+
}

0 commit comments

Comments
 (0)