Add persistent storage of installed toolchains (#3797)

Extends #3726 

Moves toolchain storage out of `UV_BOOTSTRAP_DIR` (`./bin`) into the
proper user data directory as defined by #3726.

Replaces `UV_BOOTSTRAP_DIR` with `UV_TOOLCHAIN_DIR` for customization.
Installed toolchains will be discovered without opt-in, but the idea is
still that these are not yet user-facing.
This commit is contained in:
Zanie Blue 2024-05-26 23:54:49 -04:00 committed by GitHub
parent 4191c331a7
commit 30e780e1dd
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
15 changed files with 448 additions and 210 deletions

2
.env
View file

@ -1,2 +0,0 @@
PATH=$PWD/bin:$PATH
UV_TEST_PYTHON_PATH=$PWD/bin

View file

@ -144,7 +144,6 @@ jobs:
- name: "Cargo test"
run: |
export UV_BOOTSTRAP_DIR="$(pwd)/bin"
cargo nextest run \
--features python-patch \
--workspace \

View file

@ -36,20 +36,13 @@ If tests fail due to a mismatch in the JSON Schema, run: `cargo dev generate-jso
### Python
Testing uv requires multiple specific Python versions. You can install them into
`<project root>/bin` via our bootstrapping script:
Testing uv requires multiple specific Python versions; they can be installed with:
```shell
cargo run -p uv-dev -- fetch-python
cargo dev fetch-python
```
You may need to add the versions to your `PATH`:
```shell
source .env
```
You can configure the bootstrapping directory with `UV_BOOTSTRAP_DIR`.
The storage directory can be configured with `UV_TOOLCHAIN_DIR`.
### Local testing

10
Cargo.lock generated
View file

@ -4927,6 +4927,7 @@ dependencies = [
"uv-client",
"uv-extract",
"uv-fs",
"uv-state",
"uv-warnings",
"which",
"winapi",
@ -5034,6 +5035,15 @@ dependencies = [
"uv-warnings",
]
[[package]]
name = "uv-state"
version = "0.0.1"
dependencies = [
"directories",
"fs-err",
"tempfile",
]
[[package]]
name = "uv-types"
version = "0.0.1"

View file

@ -42,6 +42,7 @@ uv-interpreter = { path = "crates/uv-interpreter" }
uv-normalize = { path = "crates/uv-normalize" }
uv-requirements = { path = "crates/uv-requirements" }
uv-resolver = { path = "crates/uv-resolver" }
uv-state = { path = "crates/uv-state" }
uv-types = { path = "crates/uv-types" }
uv-version = { path = "crates/uv-version" }
uv-virtualenv = { path = "crates/uv-virtualenv" }

View file

@ -14,7 +14,7 @@ use tracing::{info, info_span, Instrument};
use uv_fs::Simplified;
use uv_interpreter::managed::{
DownloadResult, Error, PythonDownload, PythonDownloadRequest, TOOLCHAIN_DIRECTORY,
DownloadResult, Error, InstalledToolchains, PythonDownload, PythonDownloadRequest,
};
#[derive(Parser, Debug)]
@ -25,13 +25,8 @@ pub(crate) struct FetchPythonArgs {
pub(crate) async fn fetch_python(args: FetchPythonArgs) -> Result<()> {
let start = Instant::now();
let bootstrap_dir = TOOLCHAIN_DIRECTORY.clone().unwrap_or_else(|| {
std::env::current_dir()
.expect("Use `UV_BOOTSTRAP_DIR` if the current directory is not usable.")
.join("bin")
});
fs_err::create_dir_all(&bootstrap_dir)?;
let toolchains = InstalledToolchains::from_settings()?.init()?;
let toolchain_dir = toolchains.root();
let versions = if args.versions.is_empty() {
info!("Reading versions from file...");
@ -61,7 +56,7 @@ pub(crate) async fn fetch_python(args: FetchPythonArgs) -> Result<()> {
let mut tasks = futures::stream::iter(downloads.iter())
.map(|download| {
async {
let result = download.fetch(&client, &bootstrap_dir).await;
let result = download.fetch(&client, toolchain_dir).await;
(download.python_version(), result)
}
.instrument(info_span!("download", key = %download))
@ -98,7 +93,7 @@ pub(crate) async fn fetch_python(args: FetchPythonArgs) -> Result<()> {
};
// Order matters here, as we overwrite previous links
info!("Installing to `{}`...", bootstrap_dir.user_display());
info!("Installing to `{}`...", toolchain_dir.user_display());
// On Windows, linking the executable generally results in broken installations
// and each toolchain path will need to be added to the PATH separately in the
@ -110,10 +105,10 @@ pub(crate) async fn fetch_python(args: FetchPythonArgs) -> Result<()> {
// TODO(zanieb): This path should be a part of the download metadata
let executable = path.join("install").join("bin").join("python3");
for target in [
bootstrap_dir.join(format!("python{}", version.python_full_version())),
bootstrap_dir.join(format!("python{}.{}", version.major(), version.minor())),
bootstrap_dir.join(format!("python{}", version.major())),
bootstrap_dir.join("python"),
toolchain_dir.join(format!("python{}", version.python_full_version())),
toolchain_dir.join(format!("python{}.{}", version.major(), version.minor())),
toolchain_dir.join(format!("python{}", version.major())),
toolchain_dir.join("python"),
] {
// Attempt to remove it, we'll fail on link if we couldn't remove it for some reason
// but if it's missing we don't want to error
@ -132,10 +127,6 @@ pub(crate) async fn fetch_python(args: FetchPythonArgs) -> Result<()> {
};
info!("Installed {} versions", requests.len());
info!(
r#"To enable discovery: export UV_BOOTSTRAP_DIR="{}""#,
bootstrap_dir.display()
);
Ok(())
}

View file

@ -23,6 +23,7 @@ uv-cache = { workspace = true }
uv-client = { workspace = true }
uv-extract = { workspace = true }
uv-fs = { workspace = true }
uv-state = { workspace = true }
uv-warnings = { workspace = true }
anyhow = { workspace = true }

View file

@ -8,7 +8,7 @@ use which::which;
use crate::implementation::{ImplementationName, LenientImplementationName};
use crate::interpreter::Error as InterpreterError;
use crate::managed::toolchains_for_current_platform;
use crate::managed::InstalledToolchains;
use crate::py_launcher::py_list_paths;
use crate::virtualenv::{
conda_prefix_from_env, virtualenv_from_env, virtualenv_from_working_dir,
@ -238,17 +238,20 @@ fn python_executables<'a>(
.chain(
sources.contains(InterpreterSource::ManagedToolchain).then(move ||
std::iter::once(
toolchains_for_current_platform()
.map(|toolchains|
InstalledToolchains::from_settings().map_err(Error::from).and_then(|installed_toolchains| {
debug!("Searching for managed toolchains at `{}`", installed_toolchains.root().user_display());
let toolchains = installed_toolchains.find_matching_current_platform()?;
// Check that the toolchain version satisfies the request to avoid unnecessary interpreter queries later
toolchains.filter(move |toolchain|
version.is_none() || version.is_some_and(|version|
version.matches_version(toolchain.python_version())
Ok(
toolchains.into_iter().filter(move |toolchain|
version.is_none() || version.is_some_and(|version|
version.matches_version(toolchain.python_version())
)
)
.inspect(|toolchain| debug!("Found managed toolchain `{toolchain}`"))
.map(|toolchain| (InterpreterSource::ManagedToolchain, toolchain.executable()))
)
.map(|toolchain| (InterpreterSource::ManagedToolchain, toolchain.executable()))
)
.map_err(Error::from)
})
).flatten_ok()
).into_iter().flatten()
)

View file

@ -78,6 +78,7 @@ mod tests {
discovery::{self, DiscoveredInterpreter, InterpreterRequest, VersionRequest},
find_best_interpreter, find_default_interpreter, find_interpreter,
implementation::ImplementationName,
managed::InstalledToolchains,
virtualenv::virtualenv_python_executable,
Error, InterpreterNotFound, InterpreterSource, PythonEnvironment, PythonVersion,
SourceSelector, SystemPython,
@ -287,12 +288,16 @@ mod tests {
#[test]
fn find_default_interpreter_empty_path() -> Result<()> {
let toolchains = InstalledToolchains::temp()?;
let cache = Cache::temp()?;
with_vars(
[
("UV_TEST_PYTHON_PATH", None),
("UV_BOOTSTRAP_DIR", None),
(
"UV_TOOLCHAIN_DIR",
Some(toolchains.root().to_str().unwrap()),
),
("PATH", Some("")),
],
|| {
@ -310,7 +315,7 @@ mod tests {
with_vars(
[
("UV_TEST_PYTHON_PATH", None),
("UV_BOOTSTRAP_DIR", None),
("UV_TOOLCHAIN_DIR", Some(toolchains.root().into())),
("PATH", None::<OsString>),
],
|| {
@ -332,13 +337,14 @@ mod tests {
fn find_default_interpreter_invalid_executable() -> Result<()> {
let cache = Cache::temp()?;
let tempdir = TempDir::new()?;
let toolchains = InstalledToolchains::temp()?;
let python = tempdir.child(format!("python{}", std::env::consts::EXE_SUFFIX));
python.touch()?;
with_vars(
[
("UV_TEST_PYTHON_PATH", None),
("UV_BOOTSTRAP_DIR", None),
("UV_TOOLCHAIN_DIR", Some(toolchains.root().as_os_str())),
("PATH", Some(tempdir.path().as_os_str())),
],
|| {
@ -359,6 +365,7 @@ mod tests {
#[test]
fn find_default_interpreter_valid_executable() -> Result<()> {
let tempdir = TempDir::new()?;
let toolchains = InstalledToolchains::temp()?;
let cache = Cache::temp()?;
let python = tempdir.child(format!("python{}", std::env::consts::EXE_SUFFIX));
create_mock_interpreter(
@ -371,7 +378,7 @@ mod tests {
with_vars(
[
("UV_TEST_PYTHON_PATH", None),
("UV_BOOTSTRAP_DIR", None),
("UV_TOOLCHAIN_DIR", Some(toolchains.root().as_os_str())),
("PATH", Some(tempdir.path().as_os_str())),
],
|| {
@ -395,6 +402,7 @@ mod tests {
#[test]
fn find_default_interpreter_valid_executable_after_invalid() -> Result<()> {
let tempdir = TempDir::new()?;
let toolchains = InstalledToolchains::temp()?;
let cache = Cache::temp()?;
let children = create_children(
&tempdir,
@ -440,7 +448,7 @@ mod tests {
with_vars(
[
("UV_TEST_PYTHON_PATH", None),
("UV_BOOTSTRAP_DIR", None),
("UV_TOOLCHAIN_DIR", Some(toolchains.root().into())),
(
"PATH",
Some(env::join_paths(
@ -476,6 +484,7 @@ mod tests {
#[test]
fn find_default_interpreter_only_python2_executable() -> Result<()> {
let tempdir = TempDir::new()?;
let toolchains = InstalledToolchains::temp()?;
let pwd = tempdir.child("pwd");
pwd.create_dir_all()?;
let cache = Cache::temp()?;
@ -485,7 +494,7 @@ mod tests {
with_vars(
[
("UV_TEST_PYTHON_PATH", None),
("UV_BOOTSTRAP_DIR", None),
("UV_TOOLCHAIN_DIR", Some(toolchains.root().as_os_str())),
("PATH", Some(tempdir.path().as_os_str())),
("PWD", Some(pwd.path().as_os_str())),
],
@ -508,6 +517,7 @@ mod tests {
#[test]
fn find_default_interpreter_skip_python2_executable() -> Result<()> {
let tempdir = TempDir::new()?;
let toolchains = InstalledToolchains::temp()?;
let cache = Cache::temp()?;
tempdir.child("bad").create_dir_all()?;
tempdir.child("good").create_dir_all()?;
@ -531,7 +541,7 @@ mod tests {
with_vars(
[
("UV_TEST_PYTHON_PATH", None),
("UV_BOOTSTRAP_DIR", None),
("UV_TOOLCHAIN_DIR", Some(toolchains.root().into())),
(
"PATH",
Some(env::join_paths([
@ -566,12 +576,13 @@ mod tests {
#[test]
fn find_interpreter_system_python_allowed() -> Result<()> {
let tempdir = TempDir::new()?;
let toolchains = InstalledToolchains::temp()?;
let cache = Cache::temp()?;
with_vars(
[
("UV_TEST_PYTHON_PATH", None::<OsString>),
("UV_BOOTSTRAP_DIR", None),
("UV_TOOLCHAIN_DIR", Some(toolchains.root().into())),
(
"PATH",
Some(mock_interpreters(
@ -605,7 +616,7 @@ mod tests {
with_vars(
[
("UV_TEST_PYTHON_PATH", None::<OsString>),
("UV_BOOTSTRAP_DIR", None),
("UV_TOOLCHAIN_DIR", Some(toolchains.root().into())),
(
"PATH",
Some(mock_interpreters(
@ -641,12 +652,13 @@ mod tests {
#[test]
fn find_interpreter_system_python_required() -> Result<()> {
let tempdir = TempDir::new()?;
let toolchains = InstalledToolchains::temp()?;
let cache = Cache::temp()?;
with_vars(
[
("UV_TEST_PYTHON_PATH", None::<OsString>),
("UV_BOOTSTRAP_DIR", None),
("UV_TOOLCHAIN_DIR", Some(toolchains.root().into())),
(
"PATH",
Some(mock_interpreters(
@ -682,11 +694,13 @@ mod tests {
#[test]
fn find_interpreter_system_python_disallowed() -> Result<()> {
let tempdir = TempDir::new()?;
let toolchains = InstalledToolchains::temp()?;
let cache = Cache::temp()?;
with_vars(
[
("UV_TEST_PYTHON_PATH", None::<OsString>),
("UV_TOOLCHAIN_DIR", Some(toolchains.root().into())),
(
"PATH",
Some(mock_interpreters(
@ -722,13 +736,14 @@ mod tests {
#[test]
fn find_interpreter_version_minor() -> Result<()> {
let tempdir = TempDir::new()?;
let toolchains = InstalledToolchains::temp()?;
let cache = Cache::temp()?;
let sources = SourceSelector::All;
with_vars(
[
("UV_TEST_PYTHON_PATH", None),
("UV_BOOTSTRAP_DIR", None),
("UV_TOOLCHAIN_DIR", Some(toolchains.root().into())),
(
"PATH",
Some(simple_mock_interpreters(
@ -774,13 +789,14 @@ mod tests {
#[test]
fn find_interpreter_version_patch() -> Result<()> {
let tempdir = TempDir::new()?;
let toolchains = InstalledToolchains::temp()?;
let cache = Cache::temp()?;
let sources = SourceSelector::All;
with_vars(
[
("UV_TEST_PYTHON_PATH", None),
("UV_BOOTSTRAP_DIR", None),
("UV_TOOLCHAIN_DIR", Some(toolchains.root().into())),
(
"PATH",
Some(simple_mock_interpreters(
@ -826,13 +842,14 @@ mod tests {
#[test]
fn find_interpreter_version_minor_no_match() -> Result<()> {
let tempdir = TempDir::new()?;
let toolchains = InstalledToolchains::temp()?;
let cache = Cache::temp()?;
let sources = SourceSelector::All;
with_vars(
[
("UV_TEST_PYTHON_PATH", None),
("UV_BOOTSTRAP_DIR", None),
("UV_TOOLCHAIN_DIR", Some(toolchains.root().into())),
(
"PATH",
Some(simple_mock_interpreters(
@ -868,13 +885,14 @@ mod tests {
#[test]
fn find_interpreter_version_patch_no_match() -> Result<()> {
let tempdir = TempDir::new()?;
let toolchains = InstalledToolchains::temp()?;
let cache = Cache::temp()?;
let sources = SourceSelector::All;
with_vars(
[
("UV_TEST_PYTHON_PATH", None),
("UV_BOOTSTRAP_DIR", None),
("UV_TOOLCHAIN_DIR", Some(toolchains.root().into())),
(
"PATH",
Some(simple_mock_interpreters(
@ -910,12 +928,13 @@ mod tests {
#[test]
fn find_best_interpreter_version_patch_exact() -> Result<()> {
let tempdir = TempDir::new()?;
let toolchains = InstalledToolchains::temp()?;
let cache = Cache::temp()?;
with_vars(
[
("UV_TEST_PYTHON_PATH", None),
("UV_BOOTSTRAP_DIR", None),
("UV_TOOLCHAIN_DIR", Some(toolchains.root().into())),
(
"PATH",
Some(simple_mock_interpreters(
@ -960,12 +979,13 @@ mod tests {
#[test]
fn find_best_interpreter_version_patch_fallback() -> Result<()> {
let tempdir = TempDir::new()?;
let toolchains = InstalledToolchains::temp()?;
let cache = Cache::temp()?;
with_vars(
[
("UV_TEST_PYTHON_PATH", None),
("UV_BOOTSTRAP_DIR", None),
("UV_TOOLCHAIN_DIR", Some(toolchains.root().into())),
(
"PATH",
Some(simple_mock_interpreters(
@ -1007,7 +1027,7 @@ mod tests {
with_vars(
[
("UV_TEST_PYTHON_PATH", None),
("UV_BOOTSTRAP_DIR", None),
("UV_TOOLCHAIN_DIR", Some(toolchains.root().into())),
(
"PATH",
Some(simple_mock_interpreters(
@ -1052,6 +1072,7 @@ mod tests {
#[test]
fn find_best_interpreter_skips_broken_active_environment() -> Result<()> {
let tempdir = TempDir::new()?;
let toolchains = InstalledToolchains::temp()?;
let cache = Cache::temp()?;
let venv = mock_venv(&tempdir, "3.12.0")?;
@ -1061,7 +1082,7 @@ mod tests {
with_vars(
[
("UV_TEST_PYTHON_PATH", None),
("UV_BOOTSTRAP_DIR", None),
("UV_TOOLCHAIN_DIR", Some(toolchains.root().into())),
(
"PATH",
Some(simple_mock_interpreters(&tempdir, &["3.11.1", "3.12.3"])?),
@ -1105,6 +1126,7 @@ mod tests {
#[test]
fn find_best_interpreter_skips_source_without_match() -> Result<()> {
let tempdir = TempDir::new()?;
let toolchains = InstalledToolchains::temp()?;
let cache = Cache::temp()?;
let venv = mock_venv(&tempdir, "3.12.0")?;
@ -1112,7 +1134,7 @@ mod tests {
with_vars(
[
("UV_TEST_PYTHON_PATH", None),
("UV_BOOTSTRAP_DIR", None),
("UV_TOOLCHAIN_DIR", Some(toolchains.root().into())),
(
"PATH",
Some(simple_mock_interpreters(&tempdir, &["3.10.1"])?),
@ -1156,6 +1178,7 @@ mod tests {
#[test]
fn find_best_interpreter_returns_to_earlier_source_on_fallback() -> Result<()> {
let tempdir = TempDir::new()?;
let toolchains = InstalledToolchains::temp()?;
let cache = Cache::temp()?;
let venv = mock_venv(&tempdir, "3.10.0")?;
@ -1163,7 +1186,7 @@ mod tests {
with_vars(
[
("UV_TEST_PYTHON_PATH", None),
("UV_BOOTSTRAP_DIR", None),
("UV_TOOLCHAIN_DIR", Some(toolchains.root().into())),
(
"PATH",
Some(simple_mock_interpreters(&tempdir, &["3.10.3"])?),
@ -1207,6 +1230,7 @@ mod tests {
#[test]
fn find_best_interpreter_virtualenv_used_if_system_not_allowed() -> Result<()> {
let tempdir = TempDir::new()?;
let toolchains = InstalledToolchains::temp()?;
let cache = Cache::temp()?;
let venv = mock_venv(&tempdir, "3.11.1")?;
@ -1215,7 +1239,7 @@ mod tests {
with_vars(
[
("UV_TEST_PYTHON_PATH", None),
("UV_BOOTSTRAP_DIR", None),
("UV_TOOLCHAIN_DIR", Some(toolchains.root().into())),
(
"PATH",
Some(simple_mock_interpreters(&tempdir, &["3.11.2"])?),
@ -1257,7 +1281,7 @@ mod tests {
with_vars(
[
("UV_TEST_PYTHON_PATH", None),
("UV_BOOTSTRAP_DIR", None),
("UV_TOOLCHAIN_DIR", Some(toolchains.root().into())),
(
"PATH",
Some(simple_mock_interpreters(&tempdir, &["3.11.2", "3.10.0"])?),
@ -1301,6 +1325,7 @@ mod tests {
#[test]
fn find_environment_from_active_environment() -> Result<()> {
let tempdir = TempDir::new()?;
let toolchains = InstalledToolchains::temp()?;
let cache = Cache::temp()?;
let venv = mock_venv(&tempdir, "3.12.0")?;
@ -1308,7 +1333,7 @@ mod tests {
with_vars(
[
("UV_TEST_PYTHON_PATH", None),
("UV_BOOTSTRAP_DIR", None),
("UV_TOOLCHAIN_DIR", Some(toolchains.root().into())),
(
"PATH",
Some(simple_mock_interpreters(&tempdir, &["3.10.1", "3.11.2"])?),
@ -1334,13 +1359,14 @@ mod tests {
#[test]
fn find_environment_from_conda_prefix() -> Result<()> {
let tempdir = TempDir::new()?;
let toolchains = InstalledToolchains::temp()?;
let cache = Cache::temp()?;
let conda_prefix = mock_conda_prefix(&tempdir, "3.12.0")?;
with_vars(
[
("UV_TEST_PYTHON_PATH", None),
("UV_BOOTSTRAP_DIR", None),
("UV_TOOLCHAIN_DIR", Some(toolchains.root().into())),
(
"PATH",
Some(simple_mock_interpreters(&tempdir, &["3.10.1", "3.11.2"])?),
@ -1367,6 +1393,7 @@ mod tests {
#[test]
fn find_environment_from_conda_prefix_and_virtualenv() -> Result<()> {
let tempdir = TempDir::new()?;
let toolchains = InstalledToolchains::temp()?;
let cache = Cache::temp()?;
let generic = mock_venv(&tempdir, "3.12.0")?;
let conda = mock_conda_prefix(&tempdir, "3.12.1")?;
@ -1374,7 +1401,7 @@ mod tests {
with_vars(
[
("UV_TEST_PYTHON_PATH", None),
("UV_BOOTSTRAP_DIR", None),
("UV_TOOLCHAIN_DIR", Some(toolchains.root().into())),
(
"PATH",
Some(simple_mock_interpreters(&tempdir, &["3.10.2", "3.11.3"])?),
@ -1402,6 +1429,7 @@ mod tests {
#[test]
fn find_environment_from_discovered_environment() -> Result<()> {
let tempdir = TempDir::new()?;
let toolchains = InstalledToolchains::temp()?;
let cache = Cache::temp()?;
let _venv = mock_venv(&tempdir, "3.12.0")?;
@ -1409,7 +1437,7 @@ mod tests {
with_vars(
[
("UV_TEST_PYTHON_PATH", None),
("UV_BOOTSTRAP_DIR", None),
("UV_TOOLCHAIN_DIR", Some(toolchains.root().into())),
(
"PATH",
Some(simple_mock_interpreters(&tempdir, &["3.10.1", "3.11.2"])?),
@ -1434,6 +1462,7 @@ mod tests {
#[test]
fn find_environment_from_parent_interpreter() -> Result<()> {
let tempdir = TempDir::new()?;
let toolchains = InstalledToolchains::temp()?;
let cache = Cache::temp()?;
let pwd = tempdir.child("pwd");
pwd.create_dir_all()?;
@ -1449,7 +1478,7 @@ mod tests {
with_vars(
[
("UV_TEST_PYTHON_PATH", None),
("UV_BOOTSTRAP_DIR", None),
("UV_TOOLCHAIN_DIR", Some(toolchains.root().into())),
(
"PATH",
Some(simple_mock_interpreters(&tempdir, &["3.12.2", "3.12.3"])?),
@ -1476,6 +1505,7 @@ mod tests {
#[test]
fn find_environment_from_parent_interpreter_system_explicit() -> Result<()> {
let tempdir = TempDir::new()?;
let toolchains = InstalledToolchains::temp()?;
let cache = Cache::temp()?;
let pwd = tempdir.child("pwd");
pwd.create_dir_all()?;
@ -1491,7 +1521,7 @@ mod tests {
with_vars(
[
("UV_TEST_PYTHON_PATH", None),
("UV_BOOTSTRAP_DIR", None),
("UV_TOOLCHAIN_DIR", Some(toolchains.root().into())),
(
"PATH",
Some(simple_mock_interpreters(&tempdir, &["3.12.2", "3.12.3"])?),
@ -1518,6 +1548,7 @@ mod tests {
#[test]
fn find_environment_from_parent_interpreter_system_disallowed() -> Result<()> {
let tempdir = TempDir::new()?;
let toolchains = InstalledToolchains::temp()?;
let cache = Cache::temp()?;
let pwd = tempdir.child("pwd");
pwd.create_dir_all()?;
@ -1533,7 +1564,7 @@ mod tests {
with_vars(
[
("UV_TEST_PYTHON_PATH", None),
("UV_BOOTSTRAP_DIR", None),
("UV_TOOLCHAIN_DIR", Some(toolchains.root().into())),
(
"PATH",
Some(simple_mock_interpreters(&tempdir, &["3.12.2", "3.12.3"])?),
@ -1560,6 +1591,7 @@ mod tests {
#[test]
fn find_environment_from_parent_interpreter_system_required() -> Result<()> {
let tempdir = TempDir::new()?;
let toolchains = InstalledToolchains::temp()?;
let cache = Cache::temp()?;
let pwd = tempdir.child("pwd");
pwd.create_dir_all()?;
@ -1575,7 +1607,7 @@ mod tests {
with_vars(
[
("UV_TEST_PYTHON_PATH", None),
("UV_BOOTSTRAP_DIR", None),
("UV_TOOLCHAIN_DIR", Some(toolchains.root().into())),
(
"PATH",
Some(simple_mock_interpreters(&tempdir, &["3.12.2", "3.12.3"])?),
@ -1602,6 +1634,7 @@ mod tests {
#[test]
fn find_environment_active_environment_skipped_if_system_required() -> Result<()> {
let tempdir = TempDir::new()?;
let toolchains = InstalledToolchains::temp()?;
let cache = Cache::temp()?;
let venv = mock_venv(&tempdir, "3.12.0")?;
@ -1610,7 +1643,7 @@ mod tests {
with_vars(
[
("UV_TEST_PYTHON_PATH", None),
("UV_BOOTSTRAP_DIR", None),
("UV_TOOLCHAIN_DIR", Some(toolchains.root().into())),
(
"PATH",
Some(simple_mock_interpreters(&tempdir, &["3.10.1", "3.11.2"])?),
@ -1633,7 +1666,7 @@ mod tests {
with_vars(
[
("UV_TEST_PYTHON_PATH", None),
("UV_BOOTSTRAP_DIR", None),
("UV_TOOLCHAIN_DIR", Some(toolchains.root().into())),
(
"PATH",
Some(simple_mock_interpreters(&tempdir, &["3.10.1", "3.12.2"])?),
@ -1656,7 +1689,7 @@ mod tests {
with_vars(
[
("UV_TEST_PYTHON_PATH", None),
("UV_BOOTSTRAP_DIR", None),
("UV_TOOLCHAIN_DIR", Some(toolchains.root().into())),
(
"PATH",
Some(simple_mock_interpreters(&tempdir, &["3.10.1", "3.12.2"])?),
@ -1680,12 +1713,13 @@ mod tests {
#[test]
fn find_environment_fails_if_no_virtualenv_and_system_not_allowed() -> Result<()> {
let tempdir = TempDir::new()?;
let toolchains = InstalledToolchains::temp()?;
let cache = Cache::temp()?;
with_vars(
[
("UV_TEST_PYTHON_PATH", None),
("UV_BOOTSTRAP_DIR", None),
("UV_TOOLCHAIN_DIR", Some(toolchains.root().into())),
(
"PATH",
Some(simple_mock_interpreters(&tempdir, &["3.10.1", "3.11.2"])?),
@ -1707,6 +1741,7 @@ mod tests {
#[test]
fn find_environment_allows_name_in_working_directory() -> Result<()> {
let tempdir = TempDir::new()?;
let toolchains = InstalledToolchains::temp()?;
let cache = Cache::temp()?;
let python = tempdir.join("foobar");
create_mock_interpreter(
@ -1719,6 +1754,7 @@ mod tests {
with_vars(
[
("UV_TEST_PYTHON_PATH", None::<OsString>),
("UV_TOOLCHAIN_DIR", Some(toolchains.root().into())),
("PATH", None),
("PWD", Some(tempdir.path().into())),
],
@ -1740,6 +1776,7 @@ mod tests {
#[test]
fn find_environment_allows_relative_file_path() -> Result<()> {
let tempdir = TempDir::new()?;
let toolchains = InstalledToolchains::temp()?;
let cache = Cache::temp()?;
tempdir.child("foo").create_dir_all()?;
let python = tempdir.child("foo").join("bar");
@ -1753,6 +1790,7 @@ mod tests {
with_vars(
[
("UV_TEST_PYTHON_PATH", None::<OsString>),
("UV_TOOLCHAIN_DIR", Some(toolchains.root().into())),
("PATH", None),
("PWD", Some(tempdir.path().into())),
],
@ -1777,6 +1815,7 @@ mod tests {
#[test]
fn find_environment_allows_absolute_file_path() -> Result<()> {
let tempdir = TempDir::new()?;
let toolchains = InstalledToolchains::temp()?;
let cache = Cache::temp()?;
tempdir.child("foo").create_dir_all()?;
let python = tempdir.child("foo").join("bar");
@ -1790,6 +1829,7 @@ mod tests {
with_vars(
[
("UV_TEST_PYTHON_PATH", None::<OsString>),
("UV_TOOLCHAIN_DIR", Some(toolchains.root().into())),
("PATH", None),
("PWD", Some(tempdir.path().into())),
],
@ -1814,6 +1854,7 @@ mod tests {
#[test]
fn find_environment_allows_venv_directory_path() -> Result<()> {
let tempdir = TempDir::new()?;
let toolchains = InstalledToolchains::temp()?;
// Create a separate pwd to avoid ancestor discovery of the venv
let pwd = TempDir::new()?;
let cache = Cache::temp()?;
@ -1822,6 +1863,7 @@ mod tests {
with_vars(
[
("UV_TEST_PYTHON_PATH", None::<OsString>),
("UV_TOOLCHAIN_DIR", Some(toolchains.root().into())),
("PATH", None),
("PWD", Some(pwd.path().into())),
],
@ -1846,6 +1888,7 @@ mod tests {
#[test]
fn find_environment_allows_file_path_with_system_explicit() -> Result<()> {
let tempdir = TempDir::new()?;
let toolchains = InstalledToolchains::temp()?;
let cache = Cache::temp()?;
tempdir.child("foo").create_dir_all()?;
let python = tempdir.child("foo").join("bar");
@ -1859,6 +1902,7 @@ mod tests {
with_vars(
[
("UV_TEST_PYTHON_PATH", None::<OsString>),
("UV_TOOLCHAIN_DIR", Some(toolchains.root().into())),
("PATH", None),
("PWD", Some(tempdir.path().into())),
],
@ -1883,6 +1927,7 @@ mod tests {
#[test]
fn find_environment_does_not_allow_file_path_with_system_disallowed() -> Result<()> {
let tempdir = TempDir::new()?;
let toolchains = InstalledToolchains::temp()?;
let cache = Cache::temp()?;
tempdir.child("foo").create_dir_all()?;
let python = tempdir.child("foo").join("bar");
@ -1896,6 +1941,7 @@ mod tests {
with_vars(
[
("UV_TEST_PYTHON_PATH", None::<OsString>),
("UV_TOOLCHAIN_DIR", Some(toolchains.root().into())),
("PATH", None),
("PWD", Some(tempdir.path().into())),
],
@ -1924,12 +1970,14 @@ mod tests {
#[test]
fn find_environment_treats_missing_file_path_as_file() -> Result<()> {
let tempdir = TempDir::new()?;
let toolchains = InstalledToolchains::temp()?;
let cache = Cache::temp()?;
tempdir.child("foo").create_dir_all()?;
with_vars(
[
("UV_TEST_PYTHON_PATH", None::<OsString>),
("UV_TOOLCHAIN_DIR", Some(toolchains.root().into())),
("PATH", None),
("PWD", Some(tempdir.path().into())),
],
@ -1956,6 +2004,7 @@ mod tests {
#[test]
fn find_environment_executable_name_in_search_path() -> Result<()> {
let tempdir = TempDir::new()?;
let toolchains = InstalledToolchains::temp()?;
let pwd = tempdir.child("pwd");
pwd.create_dir_all()?;
let cache = Cache::temp()?;
@ -1970,6 +2019,7 @@ mod tests {
with_vars(
[
("UV_TEST_PYTHON_PATH", None::<OsString>),
("UV_TOOLCHAIN_DIR", Some(toolchains.root().into())),
("PATH", Some(tempdir.path().into())),
("PWD", Some(pwd.path().into())),
],
@ -1991,11 +2041,13 @@ mod tests {
#[test]
fn find_environment_pypy() -> Result<()> {
let tempdir = TempDir::new()?;
let toolchains = InstalledToolchains::temp()?;
let cache = Cache::temp()?;
with_vars(
[
("UV_TEST_PYTHON_PATH", None::<OsString>),
("UV_TOOLCHAIN_DIR", Some(toolchains.root().into())),
(
"PATH",
Some(mock_interpreters(
@ -2023,11 +2075,13 @@ mod tests {
#[test]
fn find_environment_pypy_request_ignores_cpython() -> Result<()> {
let tempdir = TempDir::new()?;
let toolchains = InstalledToolchains::temp()?;
let cache = Cache::temp()?;
with_vars(
[
("UV_TEST_PYTHON_PATH", None::<OsString>),
("UV_TOOLCHAIN_DIR", Some(toolchains.root().into())),
(
"PATH",
Some(mock_interpreters(
@ -2058,12 +2112,14 @@ mod tests {
#[test]
fn find_environment_pypy_request_skips_wrong_versions() -> Result<()> {
let tempdir = TempDir::new()?;
let toolchains = InstalledToolchains::temp()?;
let cache = Cache::temp()?;
// We should prefer the `pypy` executable with the requested version
with_vars(
[
("UV_TEST_PYTHON_PATH", None::<OsString>),
("UV_TOOLCHAIN_DIR", Some(toolchains.root().into())),
(
"PATH",
Some(mock_interpreters(
@ -2094,12 +2150,14 @@ mod tests {
#[test]
fn find_environment_pypy_finds_executable_with_version_name() -> Result<()> {
let tempdir = TempDir::new()?;
let toolchains = InstalledToolchains::temp()?;
let cache = Cache::temp()?;
// We should find executables that include the version number
with_vars(
[
("UV_TEST_PYTHON_PATH", None::<OsString>),
("UV_TOOLCHAIN_DIR", Some(toolchains.root().into())),
(
"PATH",
Some(mock_interpreters(
@ -2134,6 +2192,7 @@ mod tests {
#[test]
fn find_environment_pypy_prefers_executable_with_implementation_name() -> Result<()> {
let tempdir = TempDir::new()?;
let toolchains = InstalledToolchains::temp()?;
let cache = Cache::temp()?;
// We should prefer `pypy` executables over `python` executables even if they are both pypy
@ -2152,6 +2211,7 @@ mod tests {
with_vars(
[
("UV_TEST_PYTHON_PATH", None::<OsString>),
("UV_TOOLCHAIN_DIR", Some(toolchains.root().into())),
("PATH", Some(tempdir.path().into())),
("PWD", Some(tempdir.path().into())),
],
@ -2209,6 +2269,7 @@ mod tests {
// We should prefer executables with the version number over those with implementation names
let tempdir = TempDir::new()?;
let toolchains = InstalledToolchains::temp()?;
create_mock_interpreter(
&tempdir.path().join("pypy3.10"),
&PythonVersion::from_str("3.10.0").unwrap(),
@ -2224,6 +2285,7 @@ mod tests {
with_vars(
[
("UV_TEST_PYTHON_PATH", None::<OsString>),
("UV_TOOLCHAIN_DIR", Some(toolchains.root().into())),
("PATH", Some(tempdir.path().into())),
("PWD", Some(tempdir.path().into())),
],
@ -2258,6 +2320,7 @@ mod tests {
with_vars(
[
("UV_TEST_PYTHON_PATH", None::<OsString>),
("UV_TOOLCHAIN_DIR", Some(toolchains.root().into())),
("PATH", Some(tempdir.path().into())),
("PWD", Some(tempdir.path().into())),
],

View file

@ -18,6 +18,8 @@ use uv_fs::Simplified;
#[derive(Error, Debug)]
pub enum Error {
#[error(transparent)]
IO(#[from] io::Error),
#[error(transparent)]
PlatformError(#[from] PlatformError),
#[error(transparent)]

View file

@ -1,47 +1,152 @@
use core::fmt;
use fs_err as fs;
use std::collections::BTreeSet;
use std::ffi::OsStr;
use std::path::PathBuf;
use std::io::{self, Write};
use std::path::{Path, PathBuf};
use std::str::FromStr;
use once_cell::sync::Lazy;
use tracing::debug;
use uv_fs::Simplified;
use uv_state::{StateBucket, StateStore};
use crate::managed::downloads::Error;
use crate::platform::{Arch, Libc, Os};
use crate::python_version::PythonVersion;
/// The directory where Python toolchains we install are stored.
pub static TOOLCHAIN_DIRECTORY: Lazy<Option<PathBuf>> =
Lazy::new(|| std::env::var_os("UV_BOOTSTRAP_DIR").map(PathBuf::from));
/// A collection of installed Python toolchains.
#[derive(Debug, Clone)]
pub struct InstalledToolchains {
/// The path to the top-level directory of the installed toolchains.
root: PathBuf,
}
pub fn toolchains_for_current_platform() -> Result<impl Iterator<Item = Toolchain>, Error> {
let platform_key = platform_key_from_env()?;
let iter = toolchain_directories()?
.into_iter()
// Sort "newer" versions of Python first
.rev()
.filter_map(move |path| {
if path
.file_name()
.map(OsStr::to_string_lossy)
.is_some_and(|filename| filename.ends_with(&platform_key))
{
Toolchain::new(path.clone())
.inspect_err(|err| {
debug!(
"Ignoring invalid toolchain directory {}: {err}",
path.user_display()
);
impl InstalledToolchains {
/// A directory for installed toolchains at `root`.
pub fn from_path(root: impl Into<PathBuf>) -> Result<Self, io::Error> {
Ok(Self { root: root.into() })
}
/// Prefer, in order:
/// 1. The specific toolchain directory specified by the user, i.e., `UV_TOOLCHAIN_DIR`
/// 2. A bucket in the system-appropriate user-level data directory, e.g., `~/.local/uv/toolchains`
/// 3. A bucket in the local data directory, e.g., `./.uv/toolchains`
pub fn from_settings() -> Result<Self, io::Error> {
if let Some(toolchain_dir) = std::env::var_os("UV_TOOLCHAIN_DIR") {
Self::from_path(toolchain_dir)
} else {
Self::from_path(StateStore::from_settings(None)?.bucket(StateBucket::Toolchains))
}
}
/// Create a temporary installed toolchain directory.
pub fn temp() -> Result<Self, io::Error> {
Self::from_path(StateStore::temp()?.bucket(StateBucket::Toolchains))
}
/// Initialize the installed toolchain directory.
///
/// Ensures the directory is created.
pub fn init(self) -> Result<Self, io::Error> {
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 => (),
Err(err) => return Err(err),
}
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.
fn find_all(&self) -> Result<impl DoubleEndedIterator<Item = Toolchain>, Error> {
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)),
})
.ok()
} else {
None
.collect::<Result<_, std::io::Error>>()
.map_err(|err| Error::ReadError {
dir: self.root.clone(),
err,
})?;
directories
}
});
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()
.map(|path| Toolchain::new(path).unwrap())
.rev())
}
Ok(iter)
/// Iterate over toolchains that support the current platform.
pub fn find_matching_current_platform(
&self,
) -> Result<impl DoubleEndedIterator<Item = Toolchain>, Error> {
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)
}
/// 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,
) -> Result<impl DoubleEndedIterator<Item = Toolchain> + 'a, Error> {
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
}
}
/// An installed Python toolchain.
@ -72,6 +177,7 @@ impl Toolchain {
python_version,
})
}
pub fn executable(&self) -> PathBuf {
if cfg!(windows) {
self.path.join("install").join("python.exe")
@ -87,84 +193,6 @@ impl Toolchain {
}
}
/// Return the directories in the toolchain directory.
///
/// Toolchain directories 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.
fn toolchain_directories() -> Result<BTreeSet<PathBuf>, Error> {
let Some(toolchain_dir) = TOOLCHAIN_DIRECTORY.as_ref() else {
return Ok(BTreeSet::default());
};
match fs_err::read_dir(toolchain_dir.clone()) {
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)),
})
.collect::<Result<_, std::io::Error>>()
.map_err(|err| Error::ReadError {
dir: toolchain_dir.clone(),
err,
})?;
Ok(directories)
}
Err(err) if err.kind() == std::io::ErrorKind::NotFound => Ok(BTreeSet::default()),
Err(err) => Err(Error::ReadError {
dir: toolchain_dir.clone(),
err,
}),
}
}
/// Return the 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 toolchains_for_version(version: &PythonVersion) -> Result<Vec<Toolchain>, Error> {
let platform_key = platform_key_from_env()?;
// TODO(zanieb): Consider returning an iterator instead of a `Vec`
// Note we need to collect paths regardless for sorting by version.
let toolchain_dirs = toolchain_directories()?;
Ok(toolchain_dirs
.into_iter()
// Sort "newer" versions of Python first
.rev()
.filter_map(|path| {
if path
.file_name()
.map(OsStr::to_string_lossy)
.is_some_and(|filename| {
filename.starts_with(&format!("cpython-{version}"))
&& filename.ends_with(&platform_key)
})
{
Toolchain::new(path.clone())
.inspect_err(|err| {
debug!(
"Ignoring invalid toolchain directory {}: {err}",
path.user_display()
);
})
.ok()
} else {
None
}
})
.collect::<Vec<_>>())
}
/// Generate a platform portion of a key from the environment.
fn platform_key_from_env() -> Result<String, Error> {
let os = Os::from_env()?;
@ -172,3 +200,16 @@ fn platform_key_from_env() -> Result<String, Error> {
let libc = Libc::from_env();
Ok(format!("{os}-{arch}-{libc}").to_lowercase())
}
impl fmt::Display for Toolchain {
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()
)
}
}

View file

@ -1,7 +1,5 @@
pub use crate::managed::downloads::{DownloadResult, Error, PythonDownload, PythonDownloadRequest};
pub use crate::managed::find::{
toolchains_for_current_platform, toolchains_for_version, Toolchain, TOOLCHAIN_DIRECTORY,
};
pub use crate::managed::find::{InstalledToolchains, Toolchain};
mod downloads;
mod find;

View file

@ -0,0 +1,18 @@
[package]
name = "uv-state"
version = "0.0.1"
edition = { workspace = true }
rust-version = { workspace = true }
homepage = { workspace = true }
documentation = { workspace = true }
repository = { workspace = true }
authors = { workspace = true }
license = { workspace = true }
[lints]
workspace = true
[dependencies]
directories = { workspace = true }
tempfile = { workspace = true }
fs-err = { workspace = true }

108
crates/uv-state/src/lib.rs Normal file
View file

@ -0,0 +1,108 @@
use std::{
io::{self, Write},
path::{Path, PathBuf},
sync::Arc,
};
use directories::ProjectDirs;
use fs_err as fs;
use tempfile::{tempdir, TempDir};
/// The main state storage abstraction.
///
/// This is appropriate
#[derive(Debug, Clone)]
pub struct StateStore {
/// The state storage.
root: PathBuf,
/// A temporary state storage.
///
/// Included to ensure that the temporary store exists for the length of the operation, but
/// is dropped at the end as appropriate.
_temp_dir_drop: Option<Arc<TempDir>>,
}
impl StateStore {
/// A persistent state store at `root`.
pub fn from_path(root: impl Into<PathBuf>) -> Result<Self, io::Error> {
Ok(Self {
root: root.into(),
_temp_dir_drop: None,
})
}
/// Create a temporary state store.
pub fn temp() -> Result<Self, io::Error> {
let temp_dir = tempdir()?;
Ok(Self {
root: temp_dir.path().to_path_buf(),
_temp_dir_drop: Some(Arc::new(temp_dir)),
})
}
/// Return the root of the state store.
pub fn root(&self) -> &Path {
&self.root
}
/// Initialize the state store.
pub fn init(self) -> Result<Self, io::Error> {
let root = &self.root;
// Create the state store 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 => (),
Err(err) => return Err(err),
}
Ok(Self {
root: fs::canonicalize(root)?,
..self
})
}
/// The folder for a specific cache bucket
pub fn bucket(&self, state_bucket: StateBucket) -> PathBuf {
self.root.join(state_bucket.to_str())
}
/// Prefer, in order:
/// 1. The specific state directory specified by the user.
/// 2. The system-appropriate user-level data directory.
/// 3. A `.uv` directory in the current working directory.
///
/// Returns an absolute cache dir.
pub fn from_settings(state_dir: Option<PathBuf>) -> Result<Self, io::Error> {
if let Some(state_dir) = state_dir {
StateStore::from_path(state_dir)
} else if let Some(project_dirs) = ProjectDirs::from("", "", "uv") {
StateStore::from_path(project_dirs.data_dir())
} else {
StateStore::from_path(".uv")
}
}
}
/// The different kinds of data in the state store are stored in different bucket, which in our case
/// are subdirectories of the state store root.
#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash)]
pub enum StateBucket {
// Managed toolchain
Toolchains,
}
impl StateBucket {
fn to_str(self) -> &'static str {
match self {
Self::Toolchains => "toolchains",
}
}
}

View file

@ -9,13 +9,14 @@ use regex::Regex;
use std::borrow::BorrowMut;
use std::env;
use std::ffi::OsString;
use std::iter::Iterator;
use std::path::{Path, PathBuf};
use std::process::Output;
use std::str::FromStr;
use uv_cache::Cache;
use uv_fs::Simplified;
use uv_interpreter::managed::toolchains_for_version;
use uv_interpreter::managed::InstalledToolchains;
use uv_interpreter::{
find_interpreter, InterpreterRequest, PythonVersion, SourceSelector, VersionRequest,
};
@ -337,15 +338,22 @@ pub fn create_venv<Parent: assert_fs::prelude::PathChild + AsRef<std::path::Path
cache_dir: &assert_fs::TempDir,
python: &str,
) -> PathBuf {
let python = toolchains_for_version(
&PythonVersion::from_str(python).expect("Tests should use a valid Python version"),
)
.expect("Tests are run on a supported platform")
.first()
.map(uv_interpreter::managed::Toolchain::executable)
// We'll search for the request Python on the PATH if not found in the toolchain versions
// We hack this into a `PathBuf` to satisfy the compiler but it's just a string
.unwrap_or(PathBuf::from(python));
let python = InstalledToolchains::from_settings()
.map(|installed_toolchains| {
installed_toolchains
.find_version(
&PythonVersion::from_str(python)
.expect("Tests should use a valid Python version"),
)
.expect("Tests are run on a supported platform")
.next()
.as_ref()
.map(uv_interpreter::managed::Toolchain::executable)
})
// We'll search for the request Python on the PATH if not found in the toolchain versions
// We hack this into a `PathBuf` to satisfy the compiler but it's just a string
.unwrap_or_default()
.unwrap_or(PathBuf::from(python));
let venv = temp_dir.child(".venv");
Command::new(get_bin())
@ -380,20 +388,24 @@ pub fn python_path_with_versions(
let selected_pythons = python_versions
.iter()
.flat_map(|python_version| {
let inner = toolchains_for_version(
&PythonVersion::from_str(python_version)
.expect("Tests should use a valid Python version"),
)
.expect("Tests are run on a supported platform")
.iter()
.map(|toolchain| {
toolchain
.executable()
.parent()
.expect("Executables must exist in a directory")
.to_path_buf()
})
.collect::<Vec<_>>();
let inner = InstalledToolchains::from_settings()
.map(|toolchains| {
toolchains
.find_version(
&PythonVersion::from_str(python_version)
.expect("Tests should use a valid Python version"),
)
.expect("Tests are run on a supported platform")
.map(|toolchain| {
toolchain
.executable()
.parent()
.expect("Executables must exist in a directory")
.to_path_buf()
})
.collect::<Vec<_>>()
})
.unwrap_or_default();
if inner.is_empty() {
// Fallback to a system lookup if we failed to find one in the toolchain directory
let request = InterpreterRequest::Version(