From accbb9b695418318e136f628fe366e845bc224f2 Mon Sep 17 00:00:00 2001 From: Zanie Blue Date: Fri, 14 Jun 2024 13:03:16 -0400 Subject: [PATCH] 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. --- crates/uv-toolchain/src/lib.rs | 1 + crates/uv/src/cli.rs | 11 ++ crates/uv/src/commands/mod.rs | 1 + crates/uv/src/commands/toolchain/find.rs | 43 +++++++ crates/uv/src/commands/toolchain/mod.rs | 1 + crates/uv/src/main.rs | 11 ++ crates/uv/src/settings.rs | 20 +++- crates/uv/tests/common/mod.rs | 30 ++++- crates/uv/tests/toolchain_find.rs | 142 +++++++++++++++++++++++ 9 files changed, 258 insertions(+), 2 deletions(-) create mode 100644 crates/uv/src/commands/toolchain/find.rs create mode 100644 crates/uv/tests/toolchain_find.rs diff --git a/crates/uv-toolchain/src/lib.rs b/crates/uv-toolchain/src/lib.rs index fac2deb7d0da..0e2e592feede 100644 --- a/crates/uv-toolchain/src/lib.rs +++ b/crates/uv-toolchain/src/lib.rs @@ -6,6 +6,7 @@ pub use crate::discovery::{ ToolchainSource, ToolchainSources, VersionRequest, }; pub use crate::environment::PythonEnvironment; +pub use crate::implementation::ImplementationName; pub use crate::interpreter::Interpreter; pub use crate::pointer_size::PointerSize; pub use crate::prefix::Prefix; diff --git a/crates/uv/src/cli.rs b/crates/uv/src/cli.rs index 0c6ebc454da7..7fe3f2cdf3ba 100644 --- a/crates/uv/src/cli.rs +++ b/crates/uv/src/cli.rs @@ -1712,6 +1712,10 @@ pub(crate) enum ToolchainCommand { /// Download and install a specific toolchain. Install(ToolchainInstallArgs), + + /// Search for a toolchain + #[command(disable_version_flag = true)] + Find(ToolchainFindArgs), } #[derive(Args)] @@ -1743,6 +1747,13 @@ pub(crate) struct ToolchainInstallArgs { pub(crate) force: bool, } +#[derive(Args)] +#[allow(clippy::struct_excessive_bools)] +pub(crate) struct ToolchainFindArgs { + /// The toolchain request. + pub(crate) request: Option, +} + #[derive(Args)] #[allow(clippy::struct_excessive_bools)] pub(crate) struct IndexArgs { diff --git a/crates/uv/src/commands/mod.rs b/crates/uv/src/commands/mod.rs index bd6dd9965ffd..78ce4f5ba171 100644 --- a/crates/uv/src/commands/mod.rs +++ b/crates/uv/src/commands/mod.rs @@ -24,6 +24,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::find::find as toolchain_find; pub(crate) use toolchain::install::install as toolchain_install; pub(crate) use toolchain::list::list as toolchain_list; use uv_cache::Cache; diff --git a/crates/uv/src/commands/toolchain/find.rs b/crates/uv/src/commands/toolchain/find.rs new file mode 100644 index 000000000000..9b643609e44a --- /dev/null +++ b/crates/uv/src/commands/toolchain/find.rs @@ -0,0 +1,43 @@ +use anyhow::Result; +use std::fmt::Write; + +use uv_cache::Cache; +use uv_configuration::PreviewMode; +use uv_fs::Simplified; +use uv_toolchain::{SystemPython, Toolchain, ToolchainRequest}; +use uv_warnings::warn_user; + +use crate::commands::ExitStatus; +use crate::printer::Printer; + +/// Find a toolchain. +#[allow(clippy::too_many_arguments)] +pub(crate) async fn find( + request: Option, + preview: PreviewMode, + cache: &Cache, + printer: Printer, +) -> Result { + if preview.is_disabled() { + warn_user!("`uv toolchain find` is experimental and may change without warning."); + } + + let request = match request { + Some(request) => ToolchainRequest::parse(&request), + None => ToolchainRequest::Any, + }; + let toolchain = Toolchain::find_requested( + &request, + SystemPython::Required, + PreviewMode::Enabled, + cache, + )?; + + writeln!( + printer.stdout(), + "{}", + toolchain.interpreter().sys_executable().user_display() + )?; + + Ok(ExitStatus::Success) +} diff --git a/crates/uv/src/commands/toolchain/mod.rs b/crates/uv/src/commands/toolchain/mod.rs index ef5a37748dc7..772c9c26e96f 100644 --- a/crates/uv/src/commands/toolchain/mod.rs +++ b/crates/uv/src/commands/toolchain/mod.rs @@ -1,2 +1,3 @@ +pub(crate) mod find; pub(crate) mod install; pub(crate) mod list; diff --git a/crates/uv/src/main.rs b/crates/uv/src/main.rs index a69fd47ef358..7760cc8a67c7 100644 --- a/crates/uv/src/main.rs +++ b/crates/uv/src/main.rs @@ -797,6 +797,17 @@ async fn run() -> Result { ) .await } + Commands::Toolchain(ToolchainNamespace { + command: ToolchainCommand::Find(args), + }) => { + // Resolve the settings from the command-line arguments and workspace configuration. + let args = settings::ToolchainFindSettings::resolve(args, filesystem); + + // Initialize the cache. + let cache = cache.init()?; + + commands::toolchain_find(args.request, globals.preview, &cache, printer).await + } } } diff --git a/crates/uv/src/settings.rs b/crates/uv/src/settings.rs index 1a432c10b5b5..82795bc60214 100644 --- a/crates/uv/src/settings.rs +++ b/crates/uv/src/settings.rs @@ -28,7 +28,8 @@ use crate::cli::{ AddArgs, BuildArgs, ColorChoice, GlobalArgs, IndexArgs, InstallerArgs, LockArgs, Maybe, PipCheckArgs, PipCompileArgs, PipFreezeArgs, PipInstallArgs, PipListArgs, PipShowArgs, PipSyncArgs, PipUninstallArgs, RefreshArgs, RemoveArgs, ResolverArgs, ResolverInstallerArgs, - RunArgs, SyncArgs, ToolRunArgs, ToolchainInstallArgs, ToolchainListArgs, VenvArgs, + RunArgs, SyncArgs, ToolRunArgs, ToolchainFindArgs, ToolchainInstallArgs, ToolchainListArgs, + VenvArgs, }; use crate::commands::ListFormat; @@ -273,6 +274,23 @@ impl ToolchainInstallSettings { } } +/// The resolved settings to use for a `toolchain find` invocation. +#[allow(clippy::struct_excessive_bools)] +#[derive(Debug, Clone)] +pub(crate) struct ToolchainFindSettings { + pub(crate) request: Option, +} + +impl ToolchainFindSettings { + /// Resolve the [`ToolchainFindSettings`] from the CLI and workspace configuration. + #[allow(clippy::needless_pass_by_value)] + pub(crate) fn resolve(args: ToolchainFindArgs, _filesystem: Option) -> Self { + let ToolchainFindArgs { request } = args; + + Self { request } + } +} + /// The resolved settings to use for a `sync` invocation. #[allow(clippy::struct_excessive_bools, dead_code)] #[derive(Debug, Clone)] diff --git a/crates/uv/tests/common/mod.rs b/crates/uv/tests/common/mod.rs index 31633bd54788..53237038d275 100644 --- a/crates/uv/tests/common/mod.rs +++ b/crates/uv/tests/common/mod.rs @@ -4,7 +4,7 @@ use assert_cmd::assert::{Assert, OutputAssertExt}; use assert_cmd::Command; use assert_fs::assert::PathAssert; -use assert_fs::fixture::PathChild; +use assert_fs::fixture::{ChildPath, PathChild}; use regex::Regex; use std::borrow::BorrowMut; use std::env; @@ -287,6 +287,34 @@ impl TestContext { command } + pub fn toolchains_dir(&self) -> ChildPath { + self.temp_dir.child("toolchains") + } + + /// Create a `uv toolchain find` command with options shared across scenarios. + pub fn toolchain_find(&self) -> std::process::Command { + let mut command = std::process::Command::new(get_bin()); + command + .arg("toolchain") + .arg("find") + .arg("--cache-dir") + .arg(self.cache_dir.path()) + .env("VIRTUAL_ENV", self.venv.as_os_str()) + .env("UV_NO_WRAP", "1") + .env("UV_TEST_PYTHON_PATH", "/dev/null") + .env("UV_PREVIEW", "1") + .env("UV_TOOLCHAIN_DIR", self.toolchains_dir().as_os_str()) + .current_dir(&self.temp_dir); + + if cfg!(all(windows, debug_assertions)) { + // TODO(konstin): Reduce stack usage in debug mode enough that the tests pass with the + // default windows stack of 1MB + command.env("UV_STACK_SIZE", (4 * 1024 * 1024).to_string()); + } + + command + } + /// Create a `uv run` command with options shared across scenarios. pub fn run(&self) -> std::process::Command { let mut command = self.run_without_exclude_newer(); diff --git a/crates/uv/tests/toolchain_find.rs b/crates/uv/tests/toolchain_find.rs new file mode 100644 index 000000000000..ecd07db17c35 --- /dev/null +++ b/crates/uv/tests/toolchain_find.rs @@ -0,0 +1,142 @@ +#![cfg(all(feature = "python", feature = "pypi"))] + +use common::{python_path_with_versions, uv_snapshot, TestContext}; + +mod common; + +#[test] +fn toolchain_find() { + let context: TestContext = TestContext::new("3.12"); + + // No interpreters on the path + uv_snapshot!(context.filters(), context.toolchain_find(), @r###" + success: false + exit_code: 2 + ----- stdout ----- + + ----- stderr ----- + error: No Python interpreters found in provided path, active virtual environment, or search path + "###); + + let python_path = python_path_with_versions(&context.temp_dir, &["3.11", "3.12"]) + .expect("Failed to create Python test path"); + + // Create some filters for the test interpreters, otherwise they'll be a path on the dev's machine + // TODO(zanieb): Standardize this when writing more tests + let python_path_filters = std::env::split_paths(&python_path) + .zip(["3.11", "3.12"]) + .flat_map(|(path, version)| { + TestContext::path_patterns(path) + .into_iter() + .map(move |pattern| { + ( + format!("{pattern}python.*"), + format!("[PYTHON-PATH-{version}]"), + ) + }) + }) + .collect::>(); + + let filters = python_path_filters + .iter() + .map(|(pattern, replacement)| (pattern.as_str(), replacement.as_str())) + .chain(context.filters()) + .collect::>(); + + // We find the first interpreter on the path + uv_snapshot!(filters, context.toolchain_find() + .env("UV_TEST_PYTHON_PATH", &python_path), @r###" + success: true + exit_code: 0 + ----- stdout ----- + [PYTHON-PATH-3.11] + + ----- stderr ----- + "###); + + // Request Python 3.12 + uv_snapshot!(filters, context.toolchain_find() + .arg("3.12") + .env("UV_TEST_PYTHON_PATH", &python_path), @r###" + success: true + exit_code: 0 + ----- stdout ----- + [PYTHON-PATH-3.12] + + ----- stderr ----- + "###); + + // Request Python 3.11 + uv_snapshot!(filters, context.toolchain_find() + .arg("3.11") + .env("UV_TEST_PYTHON_PATH", &python_path), @r###" + success: true + exit_code: 0 + ----- stdout ----- + [PYTHON-PATH-3.11] + + ----- stderr ----- + "###); + + // Request CPython + uv_snapshot!(filters, context.toolchain_find() + .arg("cpython") + .env("UV_TEST_PYTHON_PATH", &python_path), @r###" + success: true + exit_code: 0 + ----- stdout ----- + [PYTHON-PATH-3.11] + + ----- stderr ----- + "###); + + // Request CPython 3.12 + uv_snapshot!(filters, context.toolchain_find() + .arg("cpython@3.12") + .env("UV_TEST_PYTHON_PATH", &python_path), @r###" + success: true + exit_code: 0 + ----- stdout ----- + [PYTHON-PATH-3.12] + + ----- stderr ----- + "###); + + // Request PyPy + uv_snapshot!(filters, context.toolchain_find() + .arg("pypy") + .env("UV_TEST_PYTHON_PATH", &python_path), @r###" + success: false + exit_code: 2 + ----- stdout ----- + + ----- stderr ----- + error: No interpreter found for PyPy in provided path, active virtual environment, or search path + "###); + + // Swap the order (but don't change the filters to preserve our indices) + let python_path = python_path_with_versions(&context.temp_dir, &["3.12", "3.11"]) + .expect("Failed to create Python test path"); + + uv_snapshot!(filters, context.toolchain_find() + .env("UV_TEST_PYTHON_PATH", &python_path), @r###" + success: true + exit_code: 0 + ----- stdout ----- + [PYTHON-PATH-3.12] + + ----- stderr ----- + "###); + + // Request Python 3.11 + uv_snapshot!(filters, context.toolchain_find() + .arg("3.11") + .env("UV_TEST_PYTHON_PATH", &python_path), @r###" + success: true + exit_code: 0 + ----- stdout ----- + [PYTHON-PATH-3.11] + + ----- stderr ----- + "###); +}