mirror of
https://github.com/astral-sh/uv.git
synced 2025-10-02 15:01:16 +00:00
Add uv toolchain list
(#4163)
Adds the `uv toolchain` namespace and a `list` command to get us started. ``` ❯ cargo run -q -- toolchain list warning: `uv toolchain list` is experimental and may change without warning. 3.8.12 (cpython-3.8.12-macos-aarch64-none) 3.8.13 (cpython-3.8.13-macos-aarch64-none) 3.8.14 (cpython-3.8.14-macos-aarch64-none) 3.8.15 (cpython-3.8.15-macos-aarch64-none) 3.8.16 (cpython-3.8.16-macos-aarch64-none) 3.8.17 (cpython-3.8.17-macos-aarch64-none) 3.8.18 (cpython-3.8.18-macos-aarch64-none) 3.8.18 (cpython-3.8.18-macos-aarch64-none) 3.8.19 (cpython-3.8.19-macos-aarch64-none) 3.9.2 (cpython-3.9.2-macos-aarch64-none) 3.9.3 (cpython-3.9.3-macos-aarch64-none) 3.9.4 (cpython-3.9.4-macos-aarch64-none) 3.9.5 (cpython-3.9.5-macos-aarch64-none) 3.9.6 (cpython-3.9.6-macos-aarch64-none) 3.9.7 (cpython-3.9.7-macos-aarch64-none) 3.9.10 (cpython-3.9.10-macos-aarch64-none) 3.9.11 (cpython-3.9.11-macos-aarch64-none) 3.9.12 (cpython-3.9.12-macos-aarch64-none) 3.9.13 (cpython-3.9.13-macos-aarch64-none) 3.9.14 (cpython-3.9.14-macos-aarch64-none) 3.9.15 (cpython-3.9.15-macos-aarch64-none) 3.9.16 (cpython-3.9.16-macos-aarch64-none) 3.9.17 (cpython-3.9.17-macos-aarch64-none) 3.9.18 (cpython-3.9.18-macos-aarch64-none) 3.9.19 (cpython-3.9.19-macos-aarch64-none) 3.10.0 (cpython-3.10.0-macos-aarch64-none) 3.10.2 (cpython-3.10.2-macos-aarch64-none) 3.10.3 (cpython-3.10.3-macos-aarch64-none) 3.10.4 (cpython-3.10.4-macos-aarch64-none) 3.10.5 (cpython-3.10.5-macos-aarch64-none) 3.10.6 (cpython-3.10.6-macos-aarch64-none) 3.10.7 (cpython-3.10.7-macos-aarch64-none) 3.10.8 (cpython-3.10.8-macos-aarch64-none) 3.10.9 (cpython-3.10.9-macos-aarch64-none) 3.10.11 (cpython-3.10.11-macos-aarch64-none) 3.10.12 (cpython-3.10.12-macos-aarch64-none) 3.10.13 (cpython-3.10.13-macos-aarch64-none) 3.10.14 (cpython-3.10.14-macos-aarch64-none) 3.11.1 (cpython-3.11.1-macos-aarch64-none) 3.11.3 (cpython-3.11.3-macos-aarch64-none) 3.11.4 (cpython-3.11.4-macos-aarch64-none) 3.11.5 (cpython-3.11.5-macos-aarch64-none) 3.11.6 (cpython-3.11.6-macos-aarch64-none) 3.11.7 (cpython-3.11.7-macos-aarch64-none) 3.11.8 (cpython-3.11.8-macos-aarch64-none) 3.11.9 (cpython-3.11.9-macos-aarch64-none) 3.12.0 (cpython-3.12.0-macos-aarch64-none) 3.12.1 (cpython-3.12.1-macos-aarch64-none) 3.12.2 (cpython-3.12.2-macos-aarch64-none) 3.12.3 (cpython-3.12.3-macos-aarch64-none) ``` Closes https://github.com/astral-sh/uv/issues/4189
This commit is contained in:
parent
a2e6aaa0ff
commit
c6da4f15b7
8 changed files with 230 additions and 42 deletions
|
@ -128,6 +128,7 @@ impl PythonDownloadRequest {
|
|||
self
|
||||
}
|
||||
|
||||
/// Construct a new [`PythonDownloadRequest`] from a [`ToolchainRequest`].
|
||||
pub fn from_request(request: ToolchainRequest) -> Result<Self, Error> {
|
||||
let result = Self::default();
|
||||
let result = match request {
|
||||
|
@ -149,6 +150,9 @@ impl PythonDownloadRequest {
|
|||
Ok(result)
|
||||
}
|
||||
|
||||
/// Fill empty entries with default values.
|
||||
///
|
||||
/// Platform information is pulled from the environment.
|
||||
pub fn fill(mut self) -> Result<Self, Error> {
|
||||
if self.implementation.is_none() {
|
||||
self.implementation = Some(ImplementationName::CPython);
|
||||
|
@ -164,6 +168,48 @@ impl PythonDownloadRequest {
|
|||
}
|
||||
Ok(self)
|
||||
}
|
||||
|
||||
/// Construct a new [`PythonDownloadRequest`] with platform information from the environment.
|
||||
pub fn from_env() -> Result<Self, Error> {
|
||||
Ok(Self::new(
|
||||
None,
|
||||
None,
|
||||
Some(Arch::from_env()?),
|
||||
Some(Os::from_env()?),
|
||||
Some(Libc::from_env()),
|
||||
))
|
||||
}
|
||||
|
||||
/// Iterate over all [`PythonDownload`]'s that match this request.
|
||||
pub fn iter_downloads(&self) -> impl Iterator<Item = &'static PythonDownload> + '_ {
|
||||
PythonDownload::iter_all().filter(move |download| {
|
||||
if let Some(arch) = &self.arch {
|
||||
if download.arch != *arch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if let Some(os) = &self.os {
|
||||
if download.os != *os {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if let Some(implementation) = &self.implementation {
|
||||
if download.implementation != *implementation {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if let Some(version) = &self.version {
|
||||
if !version.matches_major_minor_patch(
|
||||
download.major,
|
||||
download.minor,
|
||||
download.patch,
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
true
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for PythonDownloadRequest {
|
||||
|
@ -211,41 +257,27 @@ impl PythonDownload {
|
|||
PYTHON_DOWNLOADS.iter().find(|&value| value.key == key)
|
||||
}
|
||||
|
||||
/// Return the first [`PythonDownload`] matching a request, if any.
|
||||
pub fn from_request(request: &PythonDownloadRequest) -> Result<&'static PythonDownload, Error> {
|
||||
for download in PYTHON_DOWNLOADS {
|
||||
if let Some(arch) = &request.arch {
|
||||
if download.arch != *arch {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
if let Some(os) = &request.os {
|
||||
if download.os != *os {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
if let Some(implementation) = &request.implementation {
|
||||
if download.implementation != *implementation {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
if let Some(version) = &request.version {
|
||||
if !version.matches_major_minor_patch(
|
||||
download.major,
|
||||
download.minor,
|
||||
download.patch,
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
return Ok(download);
|
||||
}
|
||||
Err(Error::NoDownloadFound(request.clone()))
|
||||
request
|
||||
.iter_downloads()
|
||||
.next()
|
||||
.ok_or(Error::NoDownloadFound(request.clone()))
|
||||
}
|
||||
|
||||
/// Iterate over all [`PythonDownload`]'s.
|
||||
pub fn iter_all() -> impl Iterator<Item = &'static PythonDownload> {
|
||||
PYTHON_DOWNLOADS.iter()
|
||||
}
|
||||
|
||||
pub fn url(&self) -> &str {
|
||||
self.url
|
||||
}
|
||||
|
||||
pub fn key(&self) -> &str {
|
||||
self.key
|
||||
}
|
||||
|
||||
pub fn sha256(&self) -> Option<&str> {
|
||||
self.sha256
|
||||
}
|
||||
|
|
|
@ -108,7 +108,7 @@ impl InstalledToolchains {
|
|||
/// ordering across platforms. This also results in newer Python versions coming first,
|
||||
/// but should not be relied on — instead the toolchains should be sorted later by
|
||||
/// the parsed Python version.
|
||||
fn find_all(&self) -> Result<impl DoubleEndedIterator<Item = InstalledToolchain>, Error> {
|
||||
pub fn find_all(&self) -> Result<impl DoubleEndedIterator<Item = InstalledToolchain>, Error> {
|
||||
let dirs = match fs_err::read_dir(&self.root) {
|
||||
Ok(toolchain_dirs) => {
|
||||
// Collect sorted directory paths; `read_dir` is not stable across platforms
|
||||
|
@ -191,27 +191,29 @@ impl InstalledToolchains {
|
|||
pub struct InstalledToolchain {
|
||||
/// The path to the top-level directory of the installed toolchain.
|
||||
path: PathBuf,
|
||||
/// The Python version of the toolchain.
|
||||
python_version: PythonVersion,
|
||||
/// An install key for the toolchain
|
||||
key: String,
|
||||
}
|
||||
|
||||
impl InstalledToolchain {
|
||||
pub fn new(path: PathBuf) -> Result<Self, Error> {
|
||||
let python_version = PythonVersion::from_str(
|
||||
path.file_name()
|
||||
.ok_or(Error::NameError("name is empty".to_string()))?
|
||||
.to_str()
|
||||
.ok_or(Error::NameError("not a valid string".to_string()))?
|
||||
.split('-')
|
||||
.nth(1)
|
||||
.ok_or(Error::NameError(
|
||||
"not enough `-`-separated values".to_string(),
|
||||
))?,
|
||||
)
|
||||
let key = path
|
||||
.file_name()
|
||||
.ok_or(Error::NameError("name is empty".to_string()))?
|
||||
.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}")))?;
|
||||
|
||||
Ok(Self {
|
||||
path,
|
||||
python_version,
|
||||
key,
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -228,6 +230,14 @@ impl InstalledToolchain {
|
|||
pub fn python_version(&self) -> &PythonVersion {
|
||||
&self.python_version
|
||||
}
|
||||
|
||||
pub fn path(&self) -> &Path {
|
||||
&self.path
|
||||
}
|
||||
|
||||
pub fn key(&self) -> &str {
|
||||
&self.key
|
||||
}
|
||||
}
|
||||
|
||||
/// Generate a platform portion of a key from the environment.
|
||||
|
|
|
@ -130,6 +130,8 @@ pub(crate) enum Commands {
|
|||
Pip(PipNamespace),
|
||||
/// Run and manage executable Python packages.
|
||||
Tool(ToolNamespace),
|
||||
/// Manage Python installations.
|
||||
Toolchain(ToolchainNamespace),
|
||||
/// Create a virtual environment.
|
||||
#[command(alias = "virtualenv", alias = "v")]
|
||||
Venv(VenvArgs),
|
||||
|
@ -1976,6 +1978,30 @@ pub(crate) struct ToolRunArgs {
|
|||
pub(crate) python: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Args)]
|
||||
pub(crate) struct ToolchainNamespace {
|
||||
#[command(subcommand)]
|
||||
pub(crate) command: ToolchainCommand,
|
||||
}
|
||||
|
||||
#[derive(Subcommand)]
|
||||
pub(crate) enum ToolchainCommand {
|
||||
/// List the available toolchains.
|
||||
List(ToolchainListArgs),
|
||||
}
|
||||
|
||||
#[derive(Args)]
|
||||
#[allow(clippy::struct_excessive_bools)]
|
||||
pub(crate) struct ToolchainListArgs {
|
||||
/// List all available toolchains, including those that do not match the current platform.
|
||||
#[arg(long, conflicts_with = "only_installed")]
|
||||
pub(crate) all: bool,
|
||||
|
||||
/// Only list installed toolchains.
|
||||
#[arg(long, conflicts_with = "all")]
|
||||
pub(crate) only_installed: bool,
|
||||
}
|
||||
|
||||
#[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::list::list as toolchain_list;
|
||||
use uv_cache::Cache;
|
||||
use uv_fs::Simplified;
|
||||
use uv_installer::compile_tree;
|
||||
|
@ -39,6 +40,7 @@ mod pip;
|
|||
mod project;
|
||||
pub(crate) mod reporters;
|
||||
mod tool;
|
||||
mod toolchain;
|
||||
|
||||
#[cfg(feature = "self-update")]
|
||||
mod self_update;
|
||||
|
|
70
crates/uv/src/commands/toolchain/list.rs
Normal file
70
crates/uv/src/commands/toolchain/list.rs
Normal file
|
@ -0,0 +1,70 @@
|
|||
use std::fmt::Write;
|
||||
use std::ops::Deref;
|
||||
|
||||
use anyhow::Result;
|
||||
use itertools::Itertools;
|
||||
|
||||
use uv_cache::Cache;
|
||||
use uv_configuration::PreviewMode;
|
||||
use uv_toolchain::downloads::PythonDownloadRequest;
|
||||
use uv_toolchain::managed::InstalledToolchains;
|
||||
use uv_warnings::warn_user;
|
||||
|
||||
use crate::commands::ExitStatus;
|
||||
use crate::printer::Printer;
|
||||
use crate::settings::ToolchainListIncludes;
|
||||
|
||||
/// List available toolchains.
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub(crate) async fn list(
|
||||
includes: ToolchainListIncludes,
|
||||
preview: PreviewMode,
|
||||
_cache: &Cache,
|
||||
printer: Printer,
|
||||
) -> Result<ExitStatus> {
|
||||
if preview.is_disabled() {
|
||||
warn_user!("`uv toolchain list` is experimental and may change without warning.");
|
||||
}
|
||||
|
||||
let downloads = match includes {
|
||||
ToolchainListIncludes::All => {
|
||||
let request = PythonDownloadRequest::default();
|
||||
request.iter_downloads().collect()
|
||||
}
|
||||
ToolchainListIncludes::Installed => Vec::new(),
|
||||
ToolchainListIncludes::Default => {
|
||||
let request = PythonDownloadRequest::from_env()?;
|
||||
request.iter_downloads().collect()
|
||||
}
|
||||
};
|
||||
|
||||
let installed = {
|
||||
InstalledToolchains::from_settings()?
|
||||
.init()?
|
||||
.find_all()?
|
||||
.collect_vec()
|
||||
};
|
||||
|
||||
let mut output = Vec::new();
|
||||
for toolchain in installed {
|
||||
output.push((
|
||||
toolchain.python_version().deref().version.clone(),
|
||||
toolchain.key().to_owned(),
|
||||
));
|
||||
}
|
||||
for download in downloads {
|
||||
output.push((
|
||||
download.python_version().deref().version.clone(),
|
||||
download.key().to_owned(),
|
||||
));
|
||||
}
|
||||
|
||||
output.sort();
|
||||
output.dedup();
|
||||
|
||||
for (version, key) in output {
|
||||
writeln!(printer.stdout(), "{:<8} ({key})", version.to_string())?;
|
||||
}
|
||||
|
||||
Ok(ExitStatus::Success)
|
||||
}
|
1
crates/uv/src/commands/toolchain/mod.rs
Normal file
1
crates/uv/src/commands/toolchain/mod.rs
Normal file
|
@ -0,0 +1 @@
|
|||
pub(crate) mod list;
|
|
@ -10,7 +10,7 @@ use clap::{CommandFactory, Parser};
|
|||
use owo_colors::OwoColorize;
|
||||
use tracing::instrument;
|
||||
|
||||
use cli::{ToolCommand, ToolNamespace};
|
||||
use cli::{ToolCommand, ToolNamespace, ToolchainCommand, ToolchainNamespace};
|
||||
use uv_cache::Cache;
|
||||
use uv_requirements::RequirementsSource;
|
||||
use uv_workspace::Combine;
|
||||
|
@ -671,6 +671,17 @@ async fn run() -> Result<ExitStatus> {
|
|||
)
|
||||
.await
|
||||
}
|
||||
Commands::Toolchain(ToolchainNamespace {
|
||||
command: ToolchainCommand::List(args),
|
||||
}) => {
|
||||
// Resolve the settings from the command-line arguments and workspace configuration.
|
||||
let args = settings::ToolchainListSettings::resolve(args, workspace);
|
||||
|
||||
// Initialize the cache.
|
||||
let cache = cache.init()?;
|
||||
|
||||
commands::toolchain_list(args.includes, 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, VenvArgs,
|
||||
ToolRunArgs, ToolchainListArgs, VenvArgs,
|
||||
};
|
||||
use crate::commands::ListFormat;
|
||||
|
||||
|
@ -230,6 +230,42 @@ impl ToolRunSettings {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub(crate) enum ToolchainListIncludes {
|
||||
#[default]
|
||||
Default,
|
||||
All,
|
||||
Installed,
|
||||
}
|
||||
|
||||
/// The resolved settings to use for a `tool run` invocation.
|
||||
#[allow(clippy::struct_excessive_bools)]
|
||||
#[derive(Debug, Clone)]
|
||||
pub(crate) struct ToolchainListSettings {
|
||||
pub(crate) includes: ToolchainListIncludes,
|
||||
}
|
||||
|
||||
impl ToolchainListSettings {
|
||||
/// Resolve the [`ToolchainListSettings`] from the CLI and workspace configuration.
|
||||
#[allow(clippy::needless_pass_by_value)]
|
||||
pub(crate) fn resolve(args: ToolchainListArgs, _workspace: Option<Workspace>) -> Self {
|
||||
let ToolchainListArgs {
|
||||
all,
|
||||
only_installed,
|
||||
} = args;
|
||||
|
||||
let includes = if all {
|
||||
ToolchainListIncludes::All
|
||||
} else if only_installed {
|
||||
ToolchainListIncludes::Installed
|
||||
} else {
|
||||
ToolchainListIncludes::default()
|
||||
};
|
||||
|
||||
Self { includes }
|
||||
}
|
||||
}
|
||||
|
||||
/// 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