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:
Zanie Blue 2024-06-10 10:22:00 -04:00 committed by GitHub
parent a2e6aaa0ff
commit c6da4f15b7
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 230 additions and 42 deletions

View file

@ -128,6 +128,7 @@ impl PythonDownloadRequest {
self self
} }
/// Construct a new [`PythonDownloadRequest`] from a [`ToolchainRequest`].
pub fn from_request(request: ToolchainRequest) -> Result<Self, Error> { pub fn from_request(request: ToolchainRequest) -> Result<Self, Error> {
let result = Self::default(); let result = Self::default();
let result = match request { let result = match request {
@ -149,6 +150,9 @@ impl PythonDownloadRequest {
Ok(result) Ok(result)
} }
/// Fill empty entries with default values.
///
/// Platform information is pulled from the environment.
pub fn fill(mut self) -> Result<Self, Error> { pub fn fill(mut self) -> Result<Self, Error> {
if self.implementation.is_none() { if self.implementation.is_none() {
self.implementation = Some(ImplementationName::CPython); self.implementation = Some(ImplementationName::CPython);
@ -164,6 +168,48 @@ impl PythonDownloadRequest {
} }
Ok(self) 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 { impl Display for PythonDownloadRequest {
@ -211,41 +257,27 @@ impl PythonDownload {
PYTHON_DOWNLOADS.iter().find(|&value| value.key == key) 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> { pub fn from_request(request: &PythonDownloadRequest) -> Result<&'static PythonDownload, Error> {
for download in PYTHON_DOWNLOADS { request
if let Some(arch) = &request.arch { .iter_downloads()
if download.arch != *arch { .next()
continue; .ok_or(Error::NoDownloadFound(request.clone()))
} }
}
if let Some(os) = &request.os { /// Iterate over all [`PythonDownload`]'s.
if download.os != *os { pub fn iter_all() -> impl Iterator<Item = &'static PythonDownload> {
continue; PYTHON_DOWNLOADS.iter()
}
}
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()))
} }
pub fn url(&self) -> &str { pub fn url(&self) -> &str {
self.url self.url
} }
pub fn key(&self) -> &str {
self.key
}
pub fn sha256(&self) -> Option<&str> { pub fn sha256(&self) -> Option<&str> {
self.sha256 self.sha256
} }

View file

@ -108,7 +108,7 @@ impl InstalledToolchains {
/// ordering across platforms. This also results in newer Python versions coming first, /// 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 /// but should not be relied on — instead the toolchains should be sorted later by
/// the parsed Python version. /// 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) { let dirs = match fs_err::read_dir(&self.root) {
Ok(toolchain_dirs) => { Ok(toolchain_dirs) => {
// Collect sorted directory paths; `read_dir` is not stable across platforms // Collect sorted directory paths; `read_dir` is not stable across platforms
@ -191,27 +191,29 @@ impl InstalledToolchains {
pub struct InstalledToolchain { pub struct InstalledToolchain {
/// The path to the top-level directory of the installed toolchain. /// The path to the top-level directory of the installed toolchain.
path: PathBuf, path: PathBuf,
/// The Python version of the toolchain.
python_version: PythonVersion, python_version: PythonVersion,
/// An install key for the toolchain
key: String,
} }
impl InstalledToolchain { impl InstalledToolchain {
pub fn new(path: PathBuf) -> Result<Self, Error> { pub fn new(path: PathBuf) -> Result<Self, Error> {
let python_version = PythonVersion::from_str( let key = path
path.file_name() .file_name()
.ok_or(Error::NameError("name is empty".to_string()))? .ok_or(Error::NameError("name is empty".to_string()))?
.to_str() .to_str()
.ok_or(Error::NameError("not a valid string".to_string()))? .ok_or(Error::NameError("not a valid string".to_string()))?
.split('-') .to_string();
.nth(1) let python_version = PythonVersion::from_str(key.split('-').nth(1).ok_or(
.ok_or(Error::NameError( Error::NameError("not enough `-`-separated values".to_string()),
"not enough `-`-separated values".to_string(), )?)
))?,
)
.map_err(|err| Error::NameError(format!("invalid Python version: {err}")))?; .map_err(|err| Error::NameError(format!("invalid Python version: {err}")))?;
Ok(Self { Ok(Self {
path, path,
python_version, python_version,
key,
}) })
} }
@ -228,6 +230,14 @@ impl InstalledToolchain {
pub fn python_version(&self) -> &PythonVersion { pub fn python_version(&self) -> &PythonVersion {
&self.python_version &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. /// Generate a platform portion of a key from the environment.

View file

@ -130,6 +130,8 @@ pub(crate) enum Commands {
Pip(PipNamespace), Pip(PipNamespace),
/// Run and manage executable Python packages. /// Run and manage executable Python packages.
Tool(ToolNamespace), Tool(ToolNamespace),
/// Manage Python installations.
Toolchain(ToolchainNamespace),
/// Create a virtual environment. /// Create a virtual environment.
#[command(alias = "virtualenv", alias = "v")] #[command(alias = "virtualenv", alias = "v")]
Venv(VenvArgs), Venv(VenvArgs),
@ -1976,6 +1978,30 @@ pub(crate) struct ToolRunArgs {
pub(crate) python: Option<String>, 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)] #[derive(Args)]
pub(crate) struct IndexArgs { pub(crate) struct IndexArgs {
/// The URL of the Python package index (by default: <https://pypi.org/simple>). /// The URL of the Python package index (by default: <https://pypi.org/simple>).

View file

@ -22,6 +22,7 @@ pub(crate) use project::sync::sync;
#[cfg(feature = "self-update")] #[cfg(feature = "self-update")]
pub(crate) use self_update::self_update; pub(crate) use self_update::self_update;
pub(crate) use tool::run::run as run_tool; pub(crate) use tool::run::run as run_tool;
pub(crate) use toolchain::list::list as toolchain_list;
use uv_cache::Cache; use uv_cache::Cache;
use uv_fs::Simplified; use uv_fs::Simplified;
use uv_installer::compile_tree; use uv_installer::compile_tree;
@ -39,6 +40,7 @@ mod pip;
mod project; mod project;
pub(crate) mod reporters; pub(crate) mod reporters;
mod tool; mod tool;
mod toolchain;
#[cfg(feature = "self-update")] #[cfg(feature = "self-update")]
mod self_update; mod self_update;

View 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)
}

View file

@ -0,0 +1 @@
pub(crate) mod list;

View file

@ -10,7 +10,7 @@ use clap::{CommandFactory, Parser};
use owo_colors::OwoColorize; use owo_colors::OwoColorize;
use tracing::instrument; use tracing::instrument;
use cli::{ToolCommand, ToolNamespace}; use cli::{ToolCommand, ToolNamespace, ToolchainCommand, ToolchainNamespace};
use uv_cache::Cache; use uv_cache::Cache;
use uv_requirements::RequirementsSource; use uv_requirements::RequirementsSource;
use uv_workspace::Combine; use uv_workspace::Combine;
@ -671,6 +671,17 @@ async fn run() -> Result<ExitStatus> {
) )
.await .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
}
} }
} }

View file

@ -23,7 +23,7 @@ use uv_workspace::{Combine, PipOptions, Workspace};
use crate::cli::{ use crate::cli::{
ColorChoice, GlobalArgs, LockArgs, Maybe, PipCheckArgs, PipCompileArgs, PipFreezeArgs, ColorChoice, GlobalArgs, LockArgs, Maybe, PipCheckArgs, PipCompileArgs, PipFreezeArgs,
PipInstallArgs, PipListArgs, PipShowArgs, PipSyncArgs, PipUninstallArgs, RunArgs, SyncArgs, PipInstallArgs, PipListArgs, PipShowArgs, PipSyncArgs, PipUninstallArgs, RunArgs, SyncArgs,
ToolRunArgs, VenvArgs, ToolRunArgs, ToolchainListArgs, VenvArgs,
}; };
use crate::commands::ListFormat; 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. /// The resolved settings to use for a `sync` invocation.
#[allow(clippy::struct_excessive_bools, dead_code)] #[allow(clippy::struct_excessive_bools, dead_code)]
#[derive(Debug, Clone)] #[derive(Debug, Clone)]