diff --git a/.env b/.env deleted file mode 100644 index 439560f09..000000000 --- a/.env +++ /dev/null @@ -1,2 +0,0 @@ -PATH=$PWD/bin:$PATH -UV_TEST_PYTHON_PATH=$PWD/bin diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e866e32f4..af2d95cd0 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -144,7 +144,6 @@ jobs: - name: "Cargo test" run: | - export UV_BOOTSTRAP_DIR="$(pwd)/bin" cargo nextest run \ --features python-patch \ --workspace \ diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 6e49169cc..98bffb0a1 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -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 -`/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 diff --git a/Cargo.lock b/Cargo.lock index 76bd63834..146b09947 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -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" diff --git a/Cargo.toml b/Cargo.toml index 518ed1667..d48c72db3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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" } diff --git a/crates/uv-dev/src/fetch_python.rs b/crates/uv-dev/src/fetch_python.rs index 3869a5316..f28001618 100644 --- a/crates/uv-dev/src/fetch_python.rs +++ b/crates/uv-dev/src/fetch_python.rs @@ -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(()) } diff --git a/crates/uv-interpreter/Cargo.toml b/crates/uv-interpreter/Cargo.toml index 98c9859fb..656704b64 100644 --- a/crates/uv-interpreter/Cargo.toml +++ b/crates/uv-interpreter/Cargo.toml @@ -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 } diff --git a/crates/uv-interpreter/src/discovery.rs b/crates/uv-interpreter/src/discovery.rs index d52575538..22804b4f5 100644 --- a/crates/uv-interpreter/src/discovery.rs +++ b/crates/uv-interpreter/src/discovery.rs @@ -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() ) diff --git a/crates/uv-interpreter/src/lib.rs b/crates/uv-interpreter/src/lib.rs index 83d84f843..99ae46073 100644 --- a/crates/uv-interpreter/src/lib.rs +++ b/crates/uv-interpreter/src/lib.rs @@ -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::), ], || { @@ -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::), - ("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::), - ("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::), - ("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::), + ("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::), + ("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::), + ("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::), + ("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::), + ("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::), + ("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::), + ("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::), + ("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::), + ("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::), + ("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::), + ("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::), + ("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::), + ("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::), + ("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::), + ("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::), + ("UV_TOOLCHAIN_DIR", Some(toolchains.root().into())), ("PATH", Some(tempdir.path().into())), ("PWD", Some(tempdir.path().into())), ], diff --git a/crates/uv-interpreter/src/managed/downloads.rs b/crates/uv-interpreter/src/managed/downloads.rs index 0642a7670..d6c23fdf5 100644 --- a/crates/uv-interpreter/src/managed/downloads.rs +++ b/crates/uv-interpreter/src/managed/downloads.rs @@ -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)] diff --git a/crates/uv-interpreter/src/managed/find.rs b/crates/uv-interpreter/src/managed/find.rs index f21bb68a4..a85507c8a 100644 --- a/crates/uv-interpreter/src/managed/find.rs +++ b/crates/uv-interpreter/src/managed/find.rs @@ -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> = - 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, 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) -> Result { + 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 { + 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::from_path(StateStore::temp()?.bucket(StateBucket::Toolchains)) + } + + /// Initialize the installed toolchain directory. + /// + /// Ensures the directory is created. + pub fn init(self) -> Result { + 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, 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::>() + .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, 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 + '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, 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::>() - .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, 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::>()) -} - /// Generate a platform portion of a key from the environment. fn platform_key_from_env() -> Result { let os = Os::from_env()?; @@ -172,3 +200,16 @@ fn platform_key_from_env() -> Result { 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() + ) + } +} diff --git a/crates/uv-interpreter/src/managed/mod.rs b/crates/uv-interpreter/src/managed/mod.rs index 83cf680cc..bf743556d 100644 --- a/crates/uv-interpreter/src/managed/mod.rs +++ b/crates/uv-interpreter/src/managed/mod.rs @@ -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; diff --git a/crates/uv-state/Cargo.toml b/crates/uv-state/Cargo.toml new file mode 100644 index 000000000..7f20e5898 --- /dev/null +++ b/crates/uv-state/Cargo.toml @@ -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 } diff --git a/crates/uv-state/src/lib.rs b/crates/uv-state/src/lib.rs new file mode 100644 index 000000000..dc3ba55a7 --- /dev/null +++ b/crates/uv-state/src/lib.rs @@ -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>, +} + +impl StateStore { + /// A persistent state store at `root`. + pub fn from_path(root: impl Into) -> Result { + Ok(Self { + root: root.into(), + _temp_dir_drop: None, + }) + } + + /// Create a temporary state store. + pub fn temp() -> Result { + 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 { + 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) -> Result { + 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", + } + } +} diff --git a/crates/uv/tests/common/mod.rs b/crates/uv/tests/common/mod.rs index 5dd73d896..e0dcb1e42 100644 --- a/crates/uv/tests/common/mod.rs +++ b/crates/uv/tests/common/mod.rs @@ -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 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::>(); + 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::>() + }) + .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(