2024-05-26 23:54:49 -04:00
|
|
|
use core::fmt;
|
|
|
|
|
use fs_err as fs;
|
2024-04-10 11:22:41 -05:00
|
|
|
use std::collections::BTreeSet;
|
|
|
|
|
use std::ffi::OsStr;
|
2024-05-26 23:54:49 -04:00
|
|
|
use std::io::{self, Write};
|
|
|
|
|
use std::path::{Path, PathBuf};
|
2024-05-21 15:37:23 -04:00
|
|
|
use std::str::FromStr;
|
2024-06-10 10:10:45 -04:00
|
|
|
use thiserror::Error;
|
2024-04-10 11:22:41 -05:00
|
|
|
|
2024-05-26 23:54:49 -04:00
|
|
|
use uv_state::{StateBucket, StateStore};
|
2024-05-21 15:37:23 -04:00
|
|
|
|
2024-06-10 10:10:45 -04:00
|
|
|
use crate::downloads::Error as DownloadError;
|
|
|
|
|
use crate::implementation::Error as ImplementationError;
|
|
|
|
|
use crate::platform::Error as PlatformError;
|
2024-05-21 15:37:23 -04:00
|
|
|
use crate::platform::{Arch, Libc, Os};
|
|
|
|
|
use crate::python_version::PythonVersion;
|
2024-06-10 10:10:45 -04:00
|
|
|
use uv_fs::Simplified;
|
2024-04-10 11:22:41 -05:00
|
|
|
|
2024-06-10 10:10:45 -04:00
|
|
|
#[derive(Error, Debug)]
|
|
|
|
|
pub enum Error {
|
|
|
|
|
#[error(transparent)]
|
|
|
|
|
IO(#[from] io::Error),
|
|
|
|
|
#[error(transparent)]
|
|
|
|
|
Download(#[from] DownloadError),
|
|
|
|
|
#[error(transparent)]
|
|
|
|
|
PlatformError(#[from] PlatformError),
|
|
|
|
|
#[error(transparent)]
|
|
|
|
|
ImplementationError(#[from] ImplementationError),
|
|
|
|
|
#[error("Invalid python version: {0}")]
|
|
|
|
|
InvalidPythonVersion(String),
|
|
|
|
|
#[error(transparent)]
|
|
|
|
|
ExtractError(#[from] uv_extract::Error),
|
|
|
|
|
#[error("Failed to copy to: {0}", to.user_display())]
|
|
|
|
|
CopyError {
|
|
|
|
|
to: PathBuf,
|
|
|
|
|
#[source]
|
|
|
|
|
err: io::Error,
|
|
|
|
|
},
|
|
|
|
|
#[error("Failed to read toolchain directory: {0}", dir.user_display())]
|
|
|
|
|
ReadError {
|
|
|
|
|
dir: PathBuf,
|
|
|
|
|
#[source]
|
|
|
|
|
err: io::Error,
|
|
|
|
|
},
|
|
|
|
|
#[error("Failed to parse toolchain directory name: {0}")]
|
|
|
|
|
NameError(String),
|
|
|
|
|
}
|
2024-06-07 15:20:28 -04:00
|
|
|
/// A collection of uv-managed Python toolchains installed on the current system.
|
2024-05-26 23:54:49 -04:00
|
|
|
#[derive(Debug, Clone)]
|
|
|
|
|
pub struct InstalledToolchains {
|
|
|
|
|
/// The path to the top-level directory of the installed toolchains.
|
|
|
|
|
root: PathBuf,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl InstalledToolchains {
|
|
|
|
|
/// A directory for installed toolchains at `root`.
|
2024-06-10 10:10:45 -04:00
|
|
|
fn from_path(root: impl Into<PathBuf>) -> Self {
|
|
|
|
|
Self { root: root.into() }
|
2024-05-26 23:54:49 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Prefer, in order:
|
|
|
|
|
/// 1. The specific toolchain directory specified by the user, i.e., `UV_TOOLCHAIN_DIR`
|
2024-06-05 08:27:30 -04:00
|
|
|
/// 2. A directory in the system-appropriate user-level data directory, e.g., `~/.local/uv/toolchains`
|
|
|
|
|
/// 3. A directory in the local data directory, e.g., `./.uv/toolchains`
|
2024-06-10 10:10:45 -04:00
|
|
|
pub fn from_settings() -> Result<Self, Error> {
|
2024-05-26 23:54:49 -04:00
|
|
|
if let Some(toolchain_dir) = std::env::var_os("UV_TOOLCHAIN_DIR") {
|
2024-06-10 10:10:45 -04:00
|
|
|
Ok(Self::from_path(toolchain_dir))
|
2024-05-26 23:54:49 -04:00
|
|
|
} else {
|
2024-06-10 10:10:45 -04:00
|
|
|
Ok(Self::from_path(
|
|
|
|
|
StateStore::from_settings(None)?.bucket(StateBucket::Toolchains),
|
|
|
|
|
))
|
2024-05-26 23:54:49 -04:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Create a temporary installed toolchain directory.
|
2024-06-10 10:10:45 -04:00
|
|
|
pub fn temp() -> Result<Self, Error> {
|
|
|
|
|
Ok(Self::from_path(
|
|
|
|
|
StateStore::temp()?.bucket(StateBucket::Toolchains),
|
|
|
|
|
))
|
2024-05-26 23:54:49 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Initialize the installed toolchain directory.
|
|
|
|
|
///
|
|
|
|
|
/// Ensures the directory is created.
|
2024-06-10 10:10:45 -04:00
|
|
|
pub fn init(self) -> Result<Self, Error> {
|
2024-05-26 23:54:49 -04:00
|
|
|
let root = &self.root;
|
|
|
|
|
|
|
|
|
|
// Create the cache directory, if it doesn't exist.
|
|
|
|
|
fs::create_dir_all(root)?;
|
|
|
|
|
|
|
|
|
|
// Add a .gitignore.
|
|
|
|
|
match fs::OpenOptions::new()
|
|
|
|
|
.write(true)
|
|
|
|
|
.create_new(true)
|
|
|
|
|
.open(root.join(".gitignore"))
|
|
|
|
|
{
|
|
|
|
|
Ok(mut file) => file.write_all(b"*")?,
|
|
|
|
|
Err(err) if err.kind() == io::ErrorKind::AlreadyExists => (),
|
2024-06-10 10:10:45 -04:00
|
|
|
Err(err) => return Err(err.into()),
|
2024-05-26 23:54:49 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Ok(self)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Iterate over each installed toolchain in this directory.
|
|
|
|
|
///
|
|
|
|
|
/// Toolchains are sorted descending by name, such that we get deterministic
|
|
|
|
|
/// 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.
|
2024-06-10 10:22:00 -04:00
|
|
|
pub fn find_all(&self) -> Result<impl DoubleEndedIterator<Item = InstalledToolchain>, Error> {
|
2024-05-26 23:54:49 -04:00
|
|
|
let dirs = match fs_err::read_dir(&self.root) {
|
|
|
|
|
Ok(toolchain_dirs) => {
|
|
|
|
|
// Collect sorted directory paths; `read_dir` is not stable across platforms
|
|
|
|
|
let directories: BTreeSet<_> = toolchain_dirs
|
|
|
|
|
.filter_map(|read_dir| match read_dir {
|
|
|
|
|
Ok(entry) => match entry.file_type() {
|
|
|
|
|
Ok(file_type) => file_type.is_dir().then_some(Ok(entry.path())),
|
|
|
|
|
Err(err) => Some(Err(err)),
|
|
|
|
|
},
|
|
|
|
|
Err(err) => Some(Err(err)),
|
2024-05-21 15:37:23 -04:00
|
|
|
})
|
2024-05-26 23:54:49 -04:00
|
|
|
.collect::<Result<_, std::io::Error>>()
|
|
|
|
|
.map_err(|err| Error::ReadError {
|
|
|
|
|
dir: self.root.clone(),
|
|
|
|
|
err,
|
|
|
|
|
})?;
|
|
|
|
|
directories
|
2024-05-21 15:37:23 -04:00
|
|
|
}
|
2024-05-26 23:54:49 -04:00
|
|
|
Err(err) if err.kind() == std::io::ErrorKind::NotFound => BTreeSet::default(),
|
|
|
|
|
Err(err) => {
|
|
|
|
|
return Err(Error::ReadError {
|
|
|
|
|
dir: self.root.clone(),
|
|
|
|
|
err,
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
Ok(dirs
|
|
|
|
|
.into_iter()
|
2024-06-07 15:20:28 -04:00
|
|
|
.map(|path| InstalledToolchain::new(path).unwrap())
|
2024-05-26 23:54:49 -04:00
|
|
|
.rev())
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Iterate over toolchains that support the current platform.
|
|
|
|
|
pub fn find_matching_current_platform(
|
|
|
|
|
&self,
|
2024-06-07 15:20:28 -04:00
|
|
|
) -> Result<impl DoubleEndedIterator<Item = InstalledToolchain>, Error> {
|
2024-05-26 23:54:49 -04:00
|
|
|
let platform_key = platform_key_from_env()?;
|
|
|
|
|
|
|
|
|
|
let iter = InstalledToolchains::from_settings()?
|
|
|
|
|
.find_all()?
|
|
|
|
|
.filter(move |toolchain| {
|
|
|
|
|
toolchain
|
|
|
|
|
.path
|
|
|
|
|
.file_name()
|
|
|
|
|
.map(OsStr::to_string_lossy)
|
|
|
|
|
.is_some_and(|filename| filename.ends_with(&platform_key))
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
Ok(iter)
|
|
|
|
|
}
|
2024-05-21 15:37:23 -04:00
|
|
|
|
2024-05-26 23:54:49 -04:00
|
|
|
/// Iterate over toolchains that satisfy the given Python version on this platform.
|
|
|
|
|
///
|
|
|
|
|
/// ## Errors
|
|
|
|
|
///
|
|
|
|
|
/// - The platform metadata cannot be read
|
|
|
|
|
/// - A directory in the toolchain directory cannot be read
|
|
|
|
|
pub fn find_version<'a>(
|
|
|
|
|
&self,
|
|
|
|
|
version: &'a PythonVersion,
|
2024-06-07 15:20:28 -04:00
|
|
|
) -> Result<impl DoubleEndedIterator<Item = InstalledToolchain> + 'a, Error> {
|
2024-05-26 23:54:49 -04:00
|
|
|
Ok(self
|
|
|
|
|
.find_matching_current_platform()?
|
|
|
|
|
.filter(move |toolchain| {
|
|
|
|
|
toolchain
|
|
|
|
|
.path
|
|
|
|
|
.file_name()
|
|
|
|
|
.map(OsStr::to_string_lossy)
|
|
|
|
|
.is_some_and(|filename| filename.starts_with(&format!("cpython-{version}")))
|
|
|
|
|
}))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn root(&self) -> &Path {
|
|
|
|
|
&self.root
|
|
|
|
|
}
|
2024-05-21 15:37:23 -04:00
|
|
|
}
|
2024-04-10 11:22:41 -05:00
|
|
|
|
2024-06-07 15:20:28 -04:00
|
|
|
/// A uv-managed Python toolchain installed on the current system..
|
2024-04-10 11:22:41 -05:00
|
|
|
#[derive(Debug, Clone)]
|
2024-06-07 15:20:28 -04:00
|
|
|
pub struct InstalledToolchain {
|
2024-04-10 11:22:41 -05:00
|
|
|
/// The path to the top-level directory of the installed toolchain.
|
|
|
|
|
path: PathBuf,
|
2024-06-10 10:22:00 -04:00
|
|
|
/// The Python version of the toolchain.
|
2024-05-21 15:37:23 -04:00
|
|
|
python_version: PythonVersion,
|
2024-06-10 10:22:00 -04:00
|
|
|
/// An install key for the toolchain
|
|
|
|
|
key: String,
|
2024-04-10 11:22:41 -05:00
|
|
|
}
|
|
|
|
|
|
2024-06-07 15:20:28 -04:00
|
|
|
impl InstalledToolchain {
|
2024-05-21 15:37:23 -04:00
|
|
|
pub fn new(path: PathBuf) -> Result<Self, Error> {
|
2024-06-10 10:22:00 -04:00
|
|
|
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()),
|
|
|
|
|
)?)
|
2024-06-07 15:20:28 -04:00
|
|
|
.map_err(|err| Error::NameError(format!("invalid Python version: {err}")))?;
|
2024-05-21 15:37:23 -04:00
|
|
|
|
|
|
|
|
Ok(Self {
|
|
|
|
|
path,
|
|
|
|
|
python_version,
|
2024-06-10 10:22:00 -04:00
|
|
|
key,
|
2024-05-21 15:37:23 -04:00
|
|
|
})
|
|
|
|
|
}
|
2024-05-26 23:54:49 -04:00
|
|
|
|
2024-04-10 11:22:41 -05:00
|
|
|
pub fn executable(&self) -> PathBuf {
|
|
|
|
|
if cfg!(windows) {
|
|
|
|
|
self.path.join("install").join("python.exe")
|
|
|
|
|
} else if cfg!(unix) {
|
|
|
|
|
self.path.join("install").join("bin").join("python3")
|
|
|
|
|
} else {
|
|
|
|
|
unimplemented!("Only Windows and Unix systems are supported.")
|
|
|
|
|
}
|
|
|
|
|
}
|
2024-05-21 15:37:23 -04:00
|
|
|
|
|
|
|
|
pub fn python_version(&self) -> &PythonVersion {
|
|
|
|
|
&self.python_version
|
|
|
|
|
}
|
2024-06-10 10:22:00 -04:00
|
|
|
|
|
|
|
|
pub fn path(&self) -> &Path {
|
|
|
|
|
&self.path
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn key(&self) -> &str {
|
|
|
|
|
&self.key
|
|
|
|
|
}
|
2024-04-10 11:22:41 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Generate a platform portion of a key from the environment.
|
|
|
|
|
fn platform_key_from_env() -> Result<String, Error> {
|
|
|
|
|
let os = Os::from_env()?;
|
|
|
|
|
let arch = Arch::from_env()?;
|
2024-05-25 12:05:10 +02:00
|
|
|
let libc = Libc::from_env();
|
2024-04-10 11:22:41 -05:00
|
|
|
Ok(format!("{os}-{arch}-{libc}").to_lowercase())
|
|
|
|
|
}
|
2024-05-26 23:54:49 -04:00
|
|
|
|
2024-06-07 15:20:28 -04:00
|
|
|
impl fmt::Display for InstalledToolchain {
|
2024-05-26 23:54:49 -04:00
|
|
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
|
|
|
write!(
|
|
|
|
|
f,
|
|
|
|
|
"{}",
|
|
|
|
|
self.path
|
|
|
|
|
.file_name()
|
|
|
|
|
.unwrap_or(self.path.as_os_str())
|
|
|
|
|
.to_string_lossy()
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
}
|