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" - name: "Cargo test"
run: | run: |
export UV_BOOTSTRAP_DIR="$(pwd)/bin"
cargo nextest run \ cargo nextest run \
--features python-patch \ --features python-patch \
--workspace \ --workspace \

View file

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

10
Cargo.lock generated
View file

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

View file

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

View file

@ -14,7 +14,7 @@ use tracing::{info, info_span, Instrument};
use uv_fs::Simplified; use uv_fs::Simplified;
use uv_interpreter::managed::{ use uv_interpreter::managed::{
DownloadResult, Error, PythonDownload, PythonDownloadRequest, TOOLCHAIN_DIRECTORY, DownloadResult, Error, InstalledToolchains, PythonDownload, PythonDownloadRequest,
}; };
#[derive(Parser, Debug)] #[derive(Parser, Debug)]
@ -25,13 +25,8 @@ pub(crate) struct FetchPythonArgs {
pub(crate) async fn fetch_python(args: FetchPythonArgs) -> Result<()> { pub(crate) async fn fetch_python(args: FetchPythonArgs) -> Result<()> {
let start = Instant::now(); let start = Instant::now();
let bootstrap_dir = TOOLCHAIN_DIRECTORY.clone().unwrap_or_else(|| { let toolchains = InstalledToolchains::from_settings()?.init()?;
std::env::current_dir() let toolchain_dir = toolchains.root();
.expect("Use `UV_BOOTSTRAP_DIR` if the current directory is not usable.")
.join("bin")
});
fs_err::create_dir_all(&bootstrap_dir)?;
let versions = if args.versions.is_empty() { let versions = if args.versions.is_empty() {
info!("Reading versions from file..."); 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()) let mut tasks = futures::stream::iter(downloads.iter())
.map(|download| { .map(|download| {
async { async {
let result = download.fetch(&client, &bootstrap_dir).await; let result = download.fetch(&client, toolchain_dir).await;
(download.python_version(), result) (download.python_version(), result)
} }
.instrument(info_span!("download", key = %download)) .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 // 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 // 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 // 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 // TODO(zanieb): This path should be a part of the download metadata
let executable = path.join("install").join("bin").join("python3"); let executable = path.join("install").join("bin").join("python3");
for target in [ for target in [
bootstrap_dir.join(format!("python{}", version.python_full_version())), toolchain_dir.join(format!("python{}", version.python_full_version())),
bootstrap_dir.join(format!("python{}.{}", version.major(), version.minor())), toolchain_dir.join(format!("python{}.{}", version.major(), version.minor())),
bootstrap_dir.join(format!("python{}", version.major())), toolchain_dir.join(format!("python{}", version.major())),
bootstrap_dir.join("python"), toolchain_dir.join("python"),
] { ] {
// Attempt to remove it, we'll fail on link if we couldn't remove it for some reason // 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 // 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!("Installed {} versions", requests.len());
info!(
r#"To enable discovery: export UV_BOOTSTRAP_DIR="{}""#,
bootstrap_dir.display()
);
Ok(()) Ok(())
} }

View file

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

View file

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

View file

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

View file

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

View file

@ -1,47 +1,152 @@
use core::fmt;
use fs_err as fs;
use std::collections::BTreeSet; use std::collections::BTreeSet;
use std::ffi::OsStr; use std::ffi::OsStr;
use std::path::PathBuf; use std::io::{self, Write};
use std::path::{Path, PathBuf};
use std::str::FromStr; use std::str::FromStr;
use once_cell::sync::Lazy; use uv_state::{StateBucket, StateStore};
use tracing::debug;
use uv_fs::Simplified;
use crate::managed::downloads::Error; use crate::managed::downloads::Error;
use crate::platform::{Arch, Libc, Os}; use crate::platform::{Arch, Libc, Os};
use crate::python_version::PythonVersion; use crate::python_version::PythonVersion;
/// The directory where Python toolchains we install are stored. /// A collection of installed Python toolchains.
pub static TOOLCHAIN_DIRECTORY: Lazy<Option<PathBuf>> = #[derive(Debug, Clone)]
Lazy::new(|| std::env::var_os("UV_BOOTSTRAP_DIR").map(PathBuf::from)); 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> { impl InstalledToolchains {
let platform_key = platform_key_from_env()?; /// A directory for installed toolchains at `root`.
let iter = toolchain_directories()? pub fn from_path(root: impl Into<PathBuf>) -> Result<Self, io::Error> {
.into_iter() Ok(Self { root: root.into() })
// Sort "newer" versions of Python first }
.rev()
.filter_map(move |path| { /// Prefer, in order:
if path /// 1. The specific toolchain directory specified by the user, i.e., `UV_TOOLCHAIN_DIR`
.file_name() /// 2. A bucket in the system-appropriate user-level data directory, e.g., `~/.local/uv/toolchains`
.map(OsStr::to_string_lossy) /// 3. A bucket in the local data directory, e.g., `./.uv/toolchains`
.is_some_and(|filename| filename.ends_with(&platform_key)) pub fn from_settings() -> Result<Self, io::Error> {
{ if let Some(toolchain_dir) = std::env::var_os("UV_TOOLCHAIN_DIR") {
Toolchain::new(path.clone()) Self::from_path(toolchain_dir)
.inspect_err(|err| { } else {
debug!( Self::from_path(StateStore::from_settings(None)?.bucket(StateBucket::Toolchains))
"Ignoring invalid toolchain directory {}: {err}", }
path.user_display() }
);
/// 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() .collect::<Result<_, std::io::Error>>()
} else { .map_err(|err| Error::ReadError {
None 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. /// An installed Python toolchain.
@ -72,6 +177,7 @@ impl Toolchain {
python_version, python_version,
}) })
} }
pub fn executable(&self) -> PathBuf { pub fn executable(&self) -> PathBuf {
if cfg!(windows) { if cfg!(windows) {
self.path.join("install").join("python.exe") 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. /// Generate a platform portion of a key from the environment.
fn platform_key_from_env() -> Result<String, Error> { fn platform_key_from_env() -> Result<String, Error> {
let os = Os::from_env()?; let os = Os::from_env()?;
@ -172,3 +200,16 @@ fn platform_key_from_env() -> Result<String, Error> {
let libc = Libc::from_env(); let libc = Libc::from_env();
Ok(format!("{os}-{arch}-{libc}").to_lowercase()) 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::downloads::{DownloadResult, Error, PythonDownload, PythonDownloadRequest};
pub use crate::managed::find::{ pub use crate::managed::find::{InstalledToolchains, Toolchain};
toolchains_for_current_platform, toolchains_for_version, Toolchain, TOOLCHAIN_DIRECTORY,
};
mod downloads; mod downloads;
mod find; 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::borrow::BorrowMut;
use std::env; use std::env;
use std::ffi::OsString; use std::ffi::OsString;
use std::iter::Iterator;
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use std::process::Output; use std::process::Output;
use std::str::FromStr; use std::str::FromStr;
use uv_cache::Cache; use uv_cache::Cache;
use uv_fs::Simplified; use uv_fs::Simplified;
use uv_interpreter::managed::toolchains_for_version; use uv_interpreter::managed::InstalledToolchains;
use uv_interpreter::{ use uv_interpreter::{
find_interpreter, InterpreterRequest, PythonVersion, SourceSelector, VersionRequest, 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, cache_dir: &assert_fs::TempDir,
python: &str, python: &str,
) -> PathBuf { ) -> PathBuf {
let python = toolchains_for_version( let python = InstalledToolchains::from_settings()
&PythonVersion::from_str(python).expect("Tests should use a valid Python version"), .map(|installed_toolchains| {
) installed_toolchains
.expect("Tests are run on a supported platform") .find_version(
.first() &PythonVersion::from_str(python)
.map(uv_interpreter::managed::Toolchain::executable) .expect("Tests should use a valid Python version"),
// 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 .expect("Tests are run on a supported platform")
.unwrap_or(PathBuf::from(python)); .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"); let venv = temp_dir.child(".venv");
Command::new(get_bin()) Command::new(get_bin())
@ -380,20 +388,24 @@ pub fn python_path_with_versions(
let selected_pythons = python_versions let selected_pythons = python_versions
.iter() .iter()
.flat_map(|python_version| { .flat_map(|python_version| {
let inner = toolchains_for_version( let inner = InstalledToolchains::from_settings()
&PythonVersion::from_str(python_version) .map(|toolchains| {
.expect("Tests should use a valid Python version"), toolchains
) .find_version(
.expect("Tests are run on a supported platform") &PythonVersion::from_str(python_version)
.iter() .expect("Tests should use a valid Python version"),
.map(|toolchain| { )
toolchain .expect("Tests are run on a supported platform")
.executable() .map(|toolchain| {
.parent() toolchain
.expect("Executables must exist in a directory") .executable()
.to_path_buf() .parent()
}) .expect("Executables must exist in a directory")
.collect::<Vec<_>>(); .to_path_buf()
})
.collect::<Vec<_>>()
})
.unwrap_or_default();
if inner.is_empty() { if inner.is_empty() {
// Fallback to a system lookup if we failed to find one in the toolchain directory // Fallback to a system lookup if we failed to find one in the toolchain directory
let request = InterpreterRequest::Version( let request = InterpreterRequest::Version(