Skip to content

Commit

Permalink
Implement uv toolchain fetch
Browse files Browse the repository at this point in the history
  • Loading branch information
zanieb committed Jun 8, 2024
1 parent 7a05c31 commit abf3421
Show file tree
Hide file tree
Showing 8 changed files with 201 additions and 9 deletions.
2 changes: 1 addition & 1 deletion crates/uv-toolchain/src/discovery.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1037,7 +1037,7 @@ impl VersionRequest {
}
}

fn matches_version(self, version: &PythonVersion) -> bool {
pub(crate) fn matches_version(self, version: &PythonVersion) -> bool {
match self {
Self::Any => true,
Self::Major(major) => version.major() == major,
Expand Down
57 changes: 50 additions & 7 deletions crates/uv-toolchain/src/managed.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,16 @@ use std::io::{self, Write};
use std::path::{Path, PathBuf};
use std::str::FromStr;
use thiserror::Error;
use tracing::warn;

use uv_state::{StateBucket, StateStore};

use crate::downloads::Error as DownloadError;
use crate::implementation::Error as ImplementationError;
use crate::implementation::{Error as ImplementationError, ImplementationName};
use crate::platform::Error as PlatformError;
use crate::platform::{Arch, Libc, Os};
use crate::python_version::PythonVersion;
use crate::ToolchainRequest;
use uv_fs::Simplified;

#[derive(Error, Debug)]
Expand Down Expand Up @@ -42,8 +44,10 @@ pub enum Error {
#[source]
err: io::Error,
},
#[error("Failed to parse toolchain directory name: {0}")]
#[error("Failed to read toolchain directory name: {0}")]
NameError(String),
#[error("Failed to parse toolchain directory name `{0}`: {1}")]
NameParseError(String, String),
}
/// A collection of uv-managed Python toolchains installed on the current system.
#[derive(Debug, Clone)]
Expand Down Expand Up @@ -133,7 +137,13 @@ impl InstalledToolchains {
};
Ok(dirs
.into_iter()
.map(|path| InstalledToolchain::new(path).unwrap())
.filter_map(|path| {
InstalledToolchain::new(path)
.inspect_err(|err| {
warn!("Ignoring malformed toolchain entry:\n {err}");
})
.ok()
})
.rev())
}

Expand Down Expand Up @@ -189,7 +199,9 @@ pub struct InstalledToolchain {
path: PathBuf,
/// The Python version of the toolchain.
python_version: PythonVersion,
/// An install key for the toolchain
/// The name of the Python implementation of the toolchain.
implementation: ImplementationName,
/// An install key for the toolchain.
key: String,
}

Expand All @@ -201,14 +213,25 @@ impl InstalledToolchain {
.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()),
let parts: Vec<&str> = key.split('-').collect();
let implementation = ImplementationName::from_str(parts.first().ok_or(
Error::NameParseError(key.clone(), "not enough `-`-separated values".to_string()),
)?)
.map_err(|err| Error::NameError(format!("invalid Python version: {err}")))?;
.map_err(|err| {
Error::NameParseError(key.clone(), format!("invalid Python implementation: {err}"))
})?;
let python_version = PythonVersion::from_str(parts.get(1).ok_or(Error::NameParseError(
key.clone(),
"not enough `-`-separated values".to_string(),
))?)
.map_err(|err| {
Error::NameParseError(key.clone(), format!("invalid Python version: {err}"))
})?;

Ok(Self {
path,
python_version,
implementation,
key,
})
}
Expand All @@ -234,6 +257,26 @@ impl InstalledToolchain {
pub fn key(&self) -> &str {
&self.key
}

pub fn satisfies(&self, request: &ToolchainRequest) -> bool {
match request {
ToolchainRequest::File(path) => self.executable() == *path,
ToolchainRequest::Any => true,
ToolchainRequest::Directory(path) => self.path() == *path,
ToolchainRequest::ExecutableName(name) => self
.executable()
.file_name()
.map_or(false, |filename| filename.to_string_lossy() == *name),
ToolchainRequest::Implementation(implementation) => {
*implementation == self.implementation
}
ToolchainRequest::ImplementationVersion(implementation, version) => {
*implementation == self.implementation
&& version.matches_version(&self.python_version)
}
ToolchainRequest::Version(version) => version.matches_version(&self.python_version),
}
}
}

/// Generate a platform portion of a key from the environment.
Expand Down
12 changes: 12 additions & 0 deletions crates/uv/src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1967,6 +1967,9 @@ pub(crate) struct ToolchainNamespace {
pub(crate) enum ToolchainCommand {
/// List the available toolchains.
List(ToolchainListArgs),

/// Download and install a specific toolchain.
Fetch(ToolchainFetchArgs),
}

#[derive(Args)]
Expand All @@ -1981,6 +1984,15 @@ pub(crate) struct ToolchainListArgs {
pub(crate) only_installed: bool,
}

#[derive(Args)]
#[allow(clippy::struct_excessive_bools)]
pub(crate) struct ToolchainFetchArgs {
/// The toolchain to fetch.
///
/// If not provided, the latest available version will be installed.
pub(crate) target: Option<String>,
}

#[derive(Args)]
pub(crate) struct IndexArgs {
/// The URL of the Python package index (by default: <https://pypi.org/simple>).
Expand Down
1 change: 1 addition & 0 deletions crates/uv/src/commands/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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::fetch::fetch as toolchain_fetch;
pub(crate) use toolchain::list::list as toolchain_list;
use uv_cache::Cache;
use uv_fs::Simplified;
Expand Down
99 changes: 99 additions & 0 deletions crates/uv/src/commands/toolchain/fetch.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
use anyhow::Result;
use std::fmt::Write;
use uv_cache::Cache;
use uv_client::Connectivity;
use uv_configuration::PreviewMode;
use uv_fs::Simplified;
use uv_toolchain::downloads::{DownloadResult, PythonDownload, PythonDownloadRequest};
use uv_toolchain::managed::InstalledToolchains;
use uv_toolchain::ToolchainRequest;
use uv_warnings::warn_user;

use crate::commands::ExitStatus;
use crate::printer::Printer;

/// Download and install a Python toolchain.
#[allow(clippy::too_many_arguments)]
pub(crate) async fn fetch(
target: Option<String>,
native_tls: bool,
connectivity: Connectivity,
preview: PreviewMode,
_cache: &Cache,
printer: Printer,
) -> Result<ExitStatus> {
if preview.is_disabled() {
warn_user!("`uv toolchain fetch` is experimental and may change without warning.");
}

let toolchains = InstalledToolchains::from_settings()?.init()?;
let toolchain_dir = toolchains.root();

let request = if let Some(target) = target {
let request = ToolchainRequest::parse(&target);
match request {
ToolchainRequest::Any => (),
ToolchainRequest::Directory(_)
| ToolchainRequest::ExecutableName(_)
| ToolchainRequest::File(_) => {
writeln!(printer.stderr(), "Invalid toolchain request '{target}'")?;
return Ok(ExitStatus::Failure);
}
_ => {
writeln!(printer.stderr(), "Looking for {request}")?;
}
}
request
} else {
writeln!(printer.stderr(), "Using latest Python version")?;
ToolchainRequest::default()
};

if let Some(toolchain) = toolchains
.find_all()?
.find(|toolchain| toolchain.satisfies(&request))
{
writeln!(
printer.stderr(),
"Found installed toolchain '{}'",
toolchain.key()
)?;
writeln!(
printer.stderr(),
"Already installed at {}",
toolchain.path().user_display()
)?;
return Ok(ExitStatus::Success);
}

// Fill platform information missing from the request
let request = PythonDownloadRequest::from_request(request)?.fill()?;

// Find the corresponding download
let download = PythonDownload::from_request(&request)?;
let version = download.python_version();

// Construct a client
let client = uv_client::BaseClientBuilder::new()
.connectivity(connectivity)
.native_tls(native_tls)
.build();

writeln!(printer.stderr(), "Downloading {}", download.key())?;
let result = download.fetch(&client, toolchain_dir).await?;

let path = match result {
// Note we should only encounter `AlreadyAvailable` if there's a race condition
// TODO(zanieb): We should lock the toolchain directory on fetch
DownloadResult::AlreadyAvailable(path) => path,
DownloadResult::Fetched(path) => path,
};

writeln!(
printer.stderr(),
"Installed Python {version} to {}",
path.user_display()
)?;

Ok(ExitStatus::Success)
}
1 change: 1 addition & 0 deletions crates/uv/src/commands/toolchain/mod.rs
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
pub(crate) mod fetch;
pub(crate) mod list;
19 changes: 19 additions & 0 deletions crates/uv/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -682,6 +682,25 @@ async fn run() -> Result<ExitStatus> {

commands::toolchain_list(args.includes, globals.preview, &cache, printer).await
}
Commands::Toolchain(ToolchainNamespace {
command: ToolchainCommand::Fetch(args),
}) => {
// Resolve the settings from the command-line arguments and workspace configuration.
let args = settings::ToolchainFetchSettings::resolve(args, workspace);

// Initialize the cache.
let cache = cache.init()?;

commands::toolchain_fetch(
args.target,
globals.native_tls,
globals.connectivity,
globals.preview,
&cache,
printer,
)
.await
}
}
}

Expand Down
19 changes: 18 additions & 1 deletion crates/uv/src/settings.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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, ToolchainListArgs, VenvArgs,
ToolRunArgs, ToolchainFetchArgs, ToolchainListArgs, VenvArgs,
};
use crate::commands::ListFormat;

Expand Down Expand Up @@ -266,6 +266,23 @@ impl ToolchainListSettings {
}
}

/// The resolved settings to use for a `toolchain fetch` invocation.
#[allow(clippy::struct_excessive_bools)]
#[derive(Debug, Clone)]
pub(crate) struct ToolchainFetchSettings {
pub(crate) target: Option<String>,
}

impl ToolchainFetchSettings {
/// Resolve the [`ToolchainFetchSettings`] from the CLI and workspace configuration.
#[allow(clippy::needless_pass_by_value)]
pub(crate) fn resolve(args: ToolchainFetchArgs, _workspace: Option<Workspace>) -> Self {
let ToolchainFetchArgs { target } = args;

Self { target }
}
}

/// The resolved settings to use for a `sync` invocation.
#[allow(clippy::struct_excessive_bools, dead_code)]
#[derive(Debug, Clone)]
Expand Down

0 comments on commit abf3421

Please sign in to comment.