mirror of
https://github.com/astral-sh/uv.git
synced 2025-07-07 13:25:00 +00:00
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:
parent
4191c331a7
commit
30e780e1dd
15 changed files with 448 additions and 210 deletions
2
.env
2
.env
|
@ -1,2 +0,0 @@
|
|||
PATH=$PWD/bin:$PATH
|
||||
UV_TEST_PYTHON_PATH=$PWD/bin
|
1
.github/workflows/ci.yml
vendored
1
.github/workflows/ci.yml
vendored
|
@ -144,7 +144,6 @@ jobs:
|
|||
|
||||
- name: "Cargo test"
|
||||
run: |
|
||||
export UV_BOOTSTRAP_DIR="$(pwd)/bin"
|
||||
cargo nextest run \
|
||||
--features python-patch \
|
||||
--workspace \
|
||||
|
|
|
@ -36,20 +36,13 @@ If tests fail due to a mismatch in the JSON Schema, run: `cargo dev generate-jso
|
|||
|
||||
### Python
|
||||
|
||||
Testing uv requires multiple specific Python versions. You can install them into
|
||||
`<project root>/bin` via our bootstrapping script:
|
||||
Testing uv requires multiple specific Python versions; they can be installed with:
|
||||
|
||||
```shell
|
||||
cargo run -p uv-dev -- fetch-python
|
||||
cargo dev fetch-python
|
||||
```
|
||||
|
||||
You may need to add the versions to your `PATH`:
|
||||
|
||||
```shell
|
||||
source .env
|
||||
```
|
||||
|
||||
You can configure the bootstrapping directory with `UV_BOOTSTRAP_DIR`.
|
||||
The storage directory can be configured with `UV_TOOLCHAIN_DIR`.
|
||||
|
||||
### Local testing
|
||||
|
||||
|
|
10
Cargo.lock
generated
10
Cargo.lock
generated
|
@ -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"
|
||||
|
|
|
@ -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" }
|
||||
|
|
|
@ -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(())
|
||||
}
|
||||
|
|
|
@ -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 }
|
||||
|
|
|
@ -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()
|
||||
)
|
||||
|
|
|
@ -78,6 +78,7 @@ mod tests {
|
|||
discovery::{self, DiscoveredInterpreter, InterpreterRequest, VersionRequest},
|
||||
find_best_interpreter, find_default_interpreter, find_interpreter,
|
||||
implementation::ImplementationName,
|
||||
managed::InstalledToolchains,
|
||||
virtualenv::virtualenv_python_executable,
|
||||
Error, InterpreterNotFound, InterpreterSource, PythonEnvironment, PythonVersion,
|
||||
SourceSelector, SystemPython,
|
||||
|
@ -287,12 +288,16 @@ mod tests {
|
|||
|
||||
#[test]
|
||||
fn find_default_interpreter_empty_path() -> Result<()> {
|
||||
let toolchains = InstalledToolchains::temp()?;
|
||||
let cache = Cache::temp()?;
|
||||
|
||||
with_vars(
|
||||
[
|
||||
("UV_TEST_PYTHON_PATH", None),
|
||||
("UV_BOOTSTRAP_DIR", None),
|
||||
(
|
||||
"UV_TOOLCHAIN_DIR",
|
||||
Some(toolchains.root().to_str().unwrap()),
|
||||
),
|
||||
("PATH", Some("")),
|
||||
],
|
||||
|| {
|
||||
|
@ -310,7 +315,7 @@ mod tests {
|
|||
with_vars(
|
||||
[
|
||||
("UV_TEST_PYTHON_PATH", None),
|
||||
("UV_BOOTSTRAP_DIR", None),
|
||||
("UV_TOOLCHAIN_DIR", Some(toolchains.root().into())),
|
||||
("PATH", None::<OsString>),
|
||||
],
|
||||
|| {
|
||||
|
@ -332,13 +337,14 @@ mod tests {
|
|||
fn find_default_interpreter_invalid_executable() -> Result<()> {
|
||||
let cache = Cache::temp()?;
|
||||
let tempdir = TempDir::new()?;
|
||||
let toolchains = InstalledToolchains::temp()?;
|
||||
let python = tempdir.child(format!("python{}", std::env::consts::EXE_SUFFIX));
|
||||
python.touch()?;
|
||||
|
||||
with_vars(
|
||||
[
|
||||
("UV_TEST_PYTHON_PATH", None),
|
||||
("UV_BOOTSTRAP_DIR", None),
|
||||
("UV_TOOLCHAIN_DIR", Some(toolchains.root().as_os_str())),
|
||||
("PATH", Some(tempdir.path().as_os_str())),
|
||||
],
|
||||
|| {
|
||||
|
@ -359,6 +365,7 @@ mod tests {
|
|||
#[test]
|
||||
fn find_default_interpreter_valid_executable() -> Result<()> {
|
||||
let tempdir = TempDir::new()?;
|
||||
let toolchains = InstalledToolchains::temp()?;
|
||||
let cache = Cache::temp()?;
|
||||
let python = tempdir.child(format!("python{}", std::env::consts::EXE_SUFFIX));
|
||||
create_mock_interpreter(
|
||||
|
@ -371,7 +378,7 @@ mod tests {
|
|||
with_vars(
|
||||
[
|
||||
("UV_TEST_PYTHON_PATH", None),
|
||||
("UV_BOOTSTRAP_DIR", None),
|
||||
("UV_TOOLCHAIN_DIR", Some(toolchains.root().as_os_str())),
|
||||
("PATH", Some(tempdir.path().as_os_str())),
|
||||
],
|
||||
|| {
|
||||
|
@ -395,6 +402,7 @@ mod tests {
|
|||
#[test]
|
||||
fn find_default_interpreter_valid_executable_after_invalid() -> Result<()> {
|
||||
let tempdir = TempDir::new()?;
|
||||
let toolchains = InstalledToolchains::temp()?;
|
||||
let cache = Cache::temp()?;
|
||||
let children = create_children(
|
||||
&tempdir,
|
||||
|
@ -440,7 +448,7 @@ mod tests {
|
|||
with_vars(
|
||||
[
|
||||
("UV_TEST_PYTHON_PATH", None),
|
||||
("UV_BOOTSTRAP_DIR", None),
|
||||
("UV_TOOLCHAIN_DIR", Some(toolchains.root().into())),
|
||||
(
|
||||
"PATH",
|
||||
Some(env::join_paths(
|
||||
|
@ -476,6 +484,7 @@ mod tests {
|
|||
#[test]
|
||||
fn find_default_interpreter_only_python2_executable() -> Result<()> {
|
||||
let tempdir = TempDir::new()?;
|
||||
let toolchains = InstalledToolchains::temp()?;
|
||||
let pwd = tempdir.child("pwd");
|
||||
pwd.create_dir_all()?;
|
||||
let cache = Cache::temp()?;
|
||||
|
@ -485,7 +494,7 @@ mod tests {
|
|||
with_vars(
|
||||
[
|
||||
("UV_TEST_PYTHON_PATH", None),
|
||||
("UV_BOOTSTRAP_DIR", None),
|
||||
("UV_TOOLCHAIN_DIR", Some(toolchains.root().as_os_str())),
|
||||
("PATH", Some(tempdir.path().as_os_str())),
|
||||
("PWD", Some(pwd.path().as_os_str())),
|
||||
],
|
||||
|
@ -508,6 +517,7 @@ mod tests {
|
|||
#[test]
|
||||
fn find_default_interpreter_skip_python2_executable() -> Result<()> {
|
||||
let tempdir = TempDir::new()?;
|
||||
let toolchains = InstalledToolchains::temp()?;
|
||||
let cache = Cache::temp()?;
|
||||
tempdir.child("bad").create_dir_all()?;
|
||||
tempdir.child("good").create_dir_all()?;
|
||||
|
@ -531,7 +541,7 @@ mod tests {
|
|||
with_vars(
|
||||
[
|
||||
("UV_TEST_PYTHON_PATH", None),
|
||||
("UV_BOOTSTRAP_DIR", None),
|
||||
("UV_TOOLCHAIN_DIR", Some(toolchains.root().into())),
|
||||
(
|
||||
"PATH",
|
||||
Some(env::join_paths([
|
||||
|
@ -566,12 +576,13 @@ mod tests {
|
|||
#[test]
|
||||
fn find_interpreter_system_python_allowed() -> Result<()> {
|
||||
let tempdir = TempDir::new()?;
|
||||
let toolchains = InstalledToolchains::temp()?;
|
||||
let cache = Cache::temp()?;
|
||||
|
||||
with_vars(
|
||||
[
|
||||
("UV_TEST_PYTHON_PATH", None::<OsString>),
|
||||
("UV_BOOTSTRAP_DIR", None),
|
||||
("UV_TOOLCHAIN_DIR", Some(toolchains.root().into())),
|
||||
(
|
||||
"PATH",
|
||||
Some(mock_interpreters(
|
||||
|
@ -605,7 +616,7 @@ mod tests {
|
|||
with_vars(
|
||||
[
|
||||
("UV_TEST_PYTHON_PATH", None::<OsString>),
|
||||
("UV_BOOTSTRAP_DIR", None),
|
||||
("UV_TOOLCHAIN_DIR", Some(toolchains.root().into())),
|
||||
(
|
||||
"PATH",
|
||||
Some(mock_interpreters(
|
||||
|
@ -641,12 +652,13 @@ mod tests {
|
|||
#[test]
|
||||
fn find_interpreter_system_python_required() -> Result<()> {
|
||||
let tempdir = TempDir::new()?;
|
||||
let toolchains = InstalledToolchains::temp()?;
|
||||
let cache = Cache::temp()?;
|
||||
|
||||
with_vars(
|
||||
[
|
||||
("UV_TEST_PYTHON_PATH", None::<OsString>),
|
||||
("UV_BOOTSTRAP_DIR", None),
|
||||
("UV_TOOLCHAIN_DIR", Some(toolchains.root().into())),
|
||||
(
|
||||
"PATH",
|
||||
Some(mock_interpreters(
|
||||
|
@ -682,11 +694,13 @@ mod tests {
|
|||
#[test]
|
||||
fn find_interpreter_system_python_disallowed() -> Result<()> {
|
||||
let tempdir = TempDir::new()?;
|
||||
let toolchains = InstalledToolchains::temp()?;
|
||||
let cache = Cache::temp()?;
|
||||
|
||||
with_vars(
|
||||
[
|
||||
("UV_TEST_PYTHON_PATH", None::<OsString>),
|
||||
("UV_TOOLCHAIN_DIR", Some(toolchains.root().into())),
|
||||
(
|
||||
"PATH",
|
||||
Some(mock_interpreters(
|
||||
|
@ -722,13 +736,14 @@ mod tests {
|
|||
#[test]
|
||||
fn find_interpreter_version_minor() -> Result<()> {
|
||||
let tempdir = TempDir::new()?;
|
||||
let toolchains = InstalledToolchains::temp()?;
|
||||
let cache = Cache::temp()?;
|
||||
let sources = SourceSelector::All;
|
||||
|
||||
with_vars(
|
||||
[
|
||||
("UV_TEST_PYTHON_PATH", None),
|
||||
("UV_BOOTSTRAP_DIR", None),
|
||||
("UV_TOOLCHAIN_DIR", Some(toolchains.root().into())),
|
||||
(
|
||||
"PATH",
|
||||
Some(simple_mock_interpreters(
|
||||
|
@ -774,13 +789,14 @@ mod tests {
|
|||
#[test]
|
||||
fn find_interpreter_version_patch() -> Result<()> {
|
||||
let tempdir = TempDir::new()?;
|
||||
let toolchains = InstalledToolchains::temp()?;
|
||||
let cache = Cache::temp()?;
|
||||
let sources = SourceSelector::All;
|
||||
|
||||
with_vars(
|
||||
[
|
||||
("UV_TEST_PYTHON_PATH", None),
|
||||
("UV_BOOTSTRAP_DIR", None),
|
||||
("UV_TOOLCHAIN_DIR", Some(toolchains.root().into())),
|
||||
(
|
||||
"PATH",
|
||||
Some(simple_mock_interpreters(
|
||||
|
@ -826,13 +842,14 @@ mod tests {
|
|||
#[test]
|
||||
fn find_interpreter_version_minor_no_match() -> Result<()> {
|
||||
let tempdir = TempDir::new()?;
|
||||
let toolchains = InstalledToolchains::temp()?;
|
||||
let cache = Cache::temp()?;
|
||||
let sources = SourceSelector::All;
|
||||
|
||||
with_vars(
|
||||
[
|
||||
("UV_TEST_PYTHON_PATH", None),
|
||||
("UV_BOOTSTRAP_DIR", None),
|
||||
("UV_TOOLCHAIN_DIR", Some(toolchains.root().into())),
|
||||
(
|
||||
"PATH",
|
||||
Some(simple_mock_interpreters(
|
||||
|
@ -868,13 +885,14 @@ mod tests {
|
|||
#[test]
|
||||
fn find_interpreter_version_patch_no_match() -> Result<()> {
|
||||
let tempdir = TempDir::new()?;
|
||||
let toolchains = InstalledToolchains::temp()?;
|
||||
let cache = Cache::temp()?;
|
||||
let sources = SourceSelector::All;
|
||||
|
||||
with_vars(
|
||||
[
|
||||
("UV_TEST_PYTHON_PATH", None),
|
||||
("UV_BOOTSTRAP_DIR", None),
|
||||
("UV_TOOLCHAIN_DIR", Some(toolchains.root().into())),
|
||||
(
|
||||
"PATH",
|
||||
Some(simple_mock_interpreters(
|
||||
|
@ -910,12 +928,13 @@ mod tests {
|
|||
#[test]
|
||||
fn find_best_interpreter_version_patch_exact() -> Result<()> {
|
||||
let tempdir = TempDir::new()?;
|
||||
let toolchains = InstalledToolchains::temp()?;
|
||||
let cache = Cache::temp()?;
|
||||
|
||||
with_vars(
|
||||
[
|
||||
("UV_TEST_PYTHON_PATH", None),
|
||||
("UV_BOOTSTRAP_DIR", None),
|
||||
("UV_TOOLCHAIN_DIR", Some(toolchains.root().into())),
|
||||
(
|
||||
"PATH",
|
||||
Some(simple_mock_interpreters(
|
||||
|
@ -960,12 +979,13 @@ mod tests {
|
|||
#[test]
|
||||
fn find_best_interpreter_version_patch_fallback() -> Result<()> {
|
||||
let tempdir = TempDir::new()?;
|
||||
let toolchains = InstalledToolchains::temp()?;
|
||||
let cache = Cache::temp()?;
|
||||
|
||||
with_vars(
|
||||
[
|
||||
("UV_TEST_PYTHON_PATH", None),
|
||||
("UV_BOOTSTRAP_DIR", None),
|
||||
("UV_TOOLCHAIN_DIR", Some(toolchains.root().into())),
|
||||
(
|
||||
"PATH",
|
||||
Some(simple_mock_interpreters(
|
||||
|
@ -1007,7 +1027,7 @@ mod tests {
|
|||
with_vars(
|
||||
[
|
||||
("UV_TEST_PYTHON_PATH", None),
|
||||
("UV_BOOTSTRAP_DIR", None),
|
||||
("UV_TOOLCHAIN_DIR", Some(toolchains.root().into())),
|
||||
(
|
||||
"PATH",
|
||||
Some(simple_mock_interpreters(
|
||||
|
@ -1052,6 +1072,7 @@ mod tests {
|
|||
#[test]
|
||||
fn find_best_interpreter_skips_broken_active_environment() -> Result<()> {
|
||||
let tempdir = TempDir::new()?;
|
||||
let toolchains = InstalledToolchains::temp()?;
|
||||
let cache = Cache::temp()?;
|
||||
|
||||
let venv = mock_venv(&tempdir, "3.12.0")?;
|
||||
|
@ -1061,7 +1082,7 @@ mod tests {
|
|||
with_vars(
|
||||
[
|
||||
("UV_TEST_PYTHON_PATH", None),
|
||||
("UV_BOOTSTRAP_DIR", None),
|
||||
("UV_TOOLCHAIN_DIR", Some(toolchains.root().into())),
|
||||
(
|
||||
"PATH",
|
||||
Some(simple_mock_interpreters(&tempdir, &["3.11.1", "3.12.3"])?),
|
||||
|
@ -1105,6 +1126,7 @@ mod tests {
|
|||
#[test]
|
||||
fn find_best_interpreter_skips_source_without_match() -> Result<()> {
|
||||
let tempdir = TempDir::new()?;
|
||||
let toolchains = InstalledToolchains::temp()?;
|
||||
let cache = Cache::temp()?;
|
||||
|
||||
let venv = mock_venv(&tempdir, "3.12.0")?;
|
||||
|
@ -1112,7 +1134,7 @@ mod tests {
|
|||
with_vars(
|
||||
[
|
||||
("UV_TEST_PYTHON_PATH", None),
|
||||
("UV_BOOTSTRAP_DIR", None),
|
||||
("UV_TOOLCHAIN_DIR", Some(toolchains.root().into())),
|
||||
(
|
||||
"PATH",
|
||||
Some(simple_mock_interpreters(&tempdir, &["3.10.1"])?),
|
||||
|
@ -1156,6 +1178,7 @@ mod tests {
|
|||
#[test]
|
||||
fn find_best_interpreter_returns_to_earlier_source_on_fallback() -> Result<()> {
|
||||
let tempdir = TempDir::new()?;
|
||||
let toolchains = InstalledToolchains::temp()?;
|
||||
let cache = Cache::temp()?;
|
||||
|
||||
let venv = mock_venv(&tempdir, "3.10.0")?;
|
||||
|
@ -1163,7 +1186,7 @@ mod tests {
|
|||
with_vars(
|
||||
[
|
||||
("UV_TEST_PYTHON_PATH", None),
|
||||
("UV_BOOTSTRAP_DIR", None),
|
||||
("UV_TOOLCHAIN_DIR", Some(toolchains.root().into())),
|
||||
(
|
||||
"PATH",
|
||||
Some(simple_mock_interpreters(&tempdir, &["3.10.3"])?),
|
||||
|
@ -1207,6 +1230,7 @@ mod tests {
|
|||
#[test]
|
||||
fn find_best_interpreter_virtualenv_used_if_system_not_allowed() -> Result<()> {
|
||||
let tempdir = TempDir::new()?;
|
||||
let toolchains = InstalledToolchains::temp()?;
|
||||
let cache = Cache::temp()?;
|
||||
|
||||
let venv = mock_venv(&tempdir, "3.11.1")?;
|
||||
|
@ -1215,7 +1239,7 @@ mod tests {
|
|||
with_vars(
|
||||
[
|
||||
("UV_TEST_PYTHON_PATH", None),
|
||||
("UV_BOOTSTRAP_DIR", None),
|
||||
("UV_TOOLCHAIN_DIR", Some(toolchains.root().into())),
|
||||
(
|
||||
"PATH",
|
||||
Some(simple_mock_interpreters(&tempdir, &["3.11.2"])?),
|
||||
|
@ -1257,7 +1281,7 @@ mod tests {
|
|||
with_vars(
|
||||
[
|
||||
("UV_TEST_PYTHON_PATH", None),
|
||||
("UV_BOOTSTRAP_DIR", None),
|
||||
("UV_TOOLCHAIN_DIR", Some(toolchains.root().into())),
|
||||
(
|
||||
"PATH",
|
||||
Some(simple_mock_interpreters(&tempdir, &["3.11.2", "3.10.0"])?),
|
||||
|
@ -1301,6 +1325,7 @@ mod tests {
|
|||
#[test]
|
||||
fn find_environment_from_active_environment() -> Result<()> {
|
||||
let tempdir = TempDir::new()?;
|
||||
let toolchains = InstalledToolchains::temp()?;
|
||||
let cache = Cache::temp()?;
|
||||
|
||||
let venv = mock_venv(&tempdir, "3.12.0")?;
|
||||
|
@ -1308,7 +1333,7 @@ mod tests {
|
|||
with_vars(
|
||||
[
|
||||
("UV_TEST_PYTHON_PATH", None),
|
||||
("UV_BOOTSTRAP_DIR", None),
|
||||
("UV_TOOLCHAIN_DIR", Some(toolchains.root().into())),
|
||||
(
|
||||
"PATH",
|
||||
Some(simple_mock_interpreters(&tempdir, &["3.10.1", "3.11.2"])?),
|
||||
|
@ -1334,13 +1359,14 @@ mod tests {
|
|||
#[test]
|
||||
fn find_environment_from_conda_prefix() -> Result<()> {
|
||||
let tempdir = TempDir::new()?;
|
||||
let toolchains = InstalledToolchains::temp()?;
|
||||
let cache = Cache::temp()?;
|
||||
let conda_prefix = mock_conda_prefix(&tempdir, "3.12.0")?;
|
||||
|
||||
with_vars(
|
||||
[
|
||||
("UV_TEST_PYTHON_PATH", None),
|
||||
("UV_BOOTSTRAP_DIR", None),
|
||||
("UV_TOOLCHAIN_DIR", Some(toolchains.root().into())),
|
||||
(
|
||||
"PATH",
|
||||
Some(simple_mock_interpreters(&tempdir, &["3.10.1", "3.11.2"])?),
|
||||
|
@ -1367,6 +1393,7 @@ mod tests {
|
|||
#[test]
|
||||
fn find_environment_from_conda_prefix_and_virtualenv() -> Result<()> {
|
||||
let tempdir = TempDir::new()?;
|
||||
let toolchains = InstalledToolchains::temp()?;
|
||||
let cache = Cache::temp()?;
|
||||
let generic = mock_venv(&tempdir, "3.12.0")?;
|
||||
let conda = mock_conda_prefix(&tempdir, "3.12.1")?;
|
||||
|
@ -1374,7 +1401,7 @@ mod tests {
|
|||
with_vars(
|
||||
[
|
||||
("UV_TEST_PYTHON_PATH", None),
|
||||
("UV_BOOTSTRAP_DIR", None),
|
||||
("UV_TOOLCHAIN_DIR", Some(toolchains.root().into())),
|
||||
(
|
||||
"PATH",
|
||||
Some(simple_mock_interpreters(&tempdir, &["3.10.2", "3.11.3"])?),
|
||||
|
@ -1402,6 +1429,7 @@ mod tests {
|
|||
#[test]
|
||||
fn find_environment_from_discovered_environment() -> Result<()> {
|
||||
let tempdir = TempDir::new()?;
|
||||
let toolchains = InstalledToolchains::temp()?;
|
||||
let cache = Cache::temp()?;
|
||||
|
||||
let _venv = mock_venv(&tempdir, "3.12.0")?;
|
||||
|
@ -1409,7 +1437,7 @@ mod tests {
|
|||
with_vars(
|
||||
[
|
||||
("UV_TEST_PYTHON_PATH", None),
|
||||
("UV_BOOTSTRAP_DIR", None),
|
||||
("UV_TOOLCHAIN_DIR", Some(toolchains.root().into())),
|
||||
(
|
||||
"PATH",
|
||||
Some(simple_mock_interpreters(&tempdir, &["3.10.1", "3.11.2"])?),
|
||||
|
@ -1434,6 +1462,7 @@ mod tests {
|
|||
#[test]
|
||||
fn find_environment_from_parent_interpreter() -> Result<()> {
|
||||
let tempdir = TempDir::new()?;
|
||||
let toolchains = InstalledToolchains::temp()?;
|
||||
let cache = Cache::temp()?;
|
||||
let pwd = tempdir.child("pwd");
|
||||
pwd.create_dir_all()?;
|
||||
|
@ -1449,7 +1478,7 @@ mod tests {
|
|||
with_vars(
|
||||
[
|
||||
("UV_TEST_PYTHON_PATH", None),
|
||||
("UV_BOOTSTRAP_DIR", None),
|
||||
("UV_TOOLCHAIN_DIR", Some(toolchains.root().into())),
|
||||
(
|
||||
"PATH",
|
||||
Some(simple_mock_interpreters(&tempdir, &["3.12.2", "3.12.3"])?),
|
||||
|
@ -1476,6 +1505,7 @@ mod tests {
|
|||
#[test]
|
||||
fn find_environment_from_parent_interpreter_system_explicit() -> Result<()> {
|
||||
let tempdir = TempDir::new()?;
|
||||
let toolchains = InstalledToolchains::temp()?;
|
||||
let cache = Cache::temp()?;
|
||||
let pwd = tempdir.child("pwd");
|
||||
pwd.create_dir_all()?;
|
||||
|
@ -1491,7 +1521,7 @@ mod tests {
|
|||
with_vars(
|
||||
[
|
||||
("UV_TEST_PYTHON_PATH", None),
|
||||
("UV_BOOTSTRAP_DIR", None),
|
||||
("UV_TOOLCHAIN_DIR", Some(toolchains.root().into())),
|
||||
(
|
||||
"PATH",
|
||||
Some(simple_mock_interpreters(&tempdir, &["3.12.2", "3.12.3"])?),
|
||||
|
@ -1518,6 +1548,7 @@ mod tests {
|
|||
#[test]
|
||||
fn find_environment_from_parent_interpreter_system_disallowed() -> Result<()> {
|
||||
let tempdir = TempDir::new()?;
|
||||
let toolchains = InstalledToolchains::temp()?;
|
||||
let cache = Cache::temp()?;
|
||||
let pwd = tempdir.child("pwd");
|
||||
pwd.create_dir_all()?;
|
||||
|
@ -1533,7 +1564,7 @@ mod tests {
|
|||
with_vars(
|
||||
[
|
||||
("UV_TEST_PYTHON_PATH", None),
|
||||
("UV_BOOTSTRAP_DIR", None),
|
||||
("UV_TOOLCHAIN_DIR", Some(toolchains.root().into())),
|
||||
(
|
||||
"PATH",
|
||||
Some(simple_mock_interpreters(&tempdir, &["3.12.2", "3.12.3"])?),
|
||||
|
@ -1560,6 +1591,7 @@ mod tests {
|
|||
#[test]
|
||||
fn find_environment_from_parent_interpreter_system_required() -> Result<()> {
|
||||
let tempdir = TempDir::new()?;
|
||||
let toolchains = InstalledToolchains::temp()?;
|
||||
let cache = Cache::temp()?;
|
||||
let pwd = tempdir.child("pwd");
|
||||
pwd.create_dir_all()?;
|
||||
|
@ -1575,7 +1607,7 @@ mod tests {
|
|||
with_vars(
|
||||
[
|
||||
("UV_TEST_PYTHON_PATH", None),
|
||||
("UV_BOOTSTRAP_DIR", None),
|
||||
("UV_TOOLCHAIN_DIR", Some(toolchains.root().into())),
|
||||
(
|
||||
"PATH",
|
||||
Some(simple_mock_interpreters(&tempdir, &["3.12.2", "3.12.3"])?),
|
||||
|
@ -1602,6 +1634,7 @@ mod tests {
|
|||
#[test]
|
||||
fn find_environment_active_environment_skipped_if_system_required() -> Result<()> {
|
||||
let tempdir = TempDir::new()?;
|
||||
let toolchains = InstalledToolchains::temp()?;
|
||||
let cache = Cache::temp()?;
|
||||
|
||||
let venv = mock_venv(&tempdir, "3.12.0")?;
|
||||
|
@ -1610,7 +1643,7 @@ mod tests {
|
|||
with_vars(
|
||||
[
|
||||
("UV_TEST_PYTHON_PATH", None),
|
||||
("UV_BOOTSTRAP_DIR", None),
|
||||
("UV_TOOLCHAIN_DIR", Some(toolchains.root().into())),
|
||||
(
|
||||
"PATH",
|
||||
Some(simple_mock_interpreters(&tempdir, &["3.10.1", "3.11.2"])?),
|
||||
|
@ -1633,7 +1666,7 @@ mod tests {
|
|||
with_vars(
|
||||
[
|
||||
("UV_TEST_PYTHON_PATH", None),
|
||||
("UV_BOOTSTRAP_DIR", None),
|
||||
("UV_TOOLCHAIN_DIR", Some(toolchains.root().into())),
|
||||
(
|
||||
"PATH",
|
||||
Some(simple_mock_interpreters(&tempdir, &["3.10.1", "3.12.2"])?),
|
||||
|
@ -1656,7 +1689,7 @@ mod tests {
|
|||
with_vars(
|
||||
[
|
||||
("UV_TEST_PYTHON_PATH", None),
|
||||
("UV_BOOTSTRAP_DIR", None),
|
||||
("UV_TOOLCHAIN_DIR", Some(toolchains.root().into())),
|
||||
(
|
||||
"PATH",
|
||||
Some(simple_mock_interpreters(&tempdir, &["3.10.1", "3.12.2"])?),
|
||||
|
@ -1680,12 +1713,13 @@ mod tests {
|
|||
#[test]
|
||||
fn find_environment_fails_if_no_virtualenv_and_system_not_allowed() -> Result<()> {
|
||||
let tempdir = TempDir::new()?;
|
||||
let toolchains = InstalledToolchains::temp()?;
|
||||
let cache = Cache::temp()?;
|
||||
|
||||
with_vars(
|
||||
[
|
||||
("UV_TEST_PYTHON_PATH", None),
|
||||
("UV_BOOTSTRAP_DIR", None),
|
||||
("UV_TOOLCHAIN_DIR", Some(toolchains.root().into())),
|
||||
(
|
||||
"PATH",
|
||||
Some(simple_mock_interpreters(&tempdir, &["3.10.1", "3.11.2"])?),
|
||||
|
@ -1707,6 +1741,7 @@ mod tests {
|
|||
#[test]
|
||||
fn find_environment_allows_name_in_working_directory() -> Result<()> {
|
||||
let tempdir = TempDir::new()?;
|
||||
let toolchains = InstalledToolchains::temp()?;
|
||||
let cache = Cache::temp()?;
|
||||
let python = tempdir.join("foobar");
|
||||
create_mock_interpreter(
|
||||
|
@ -1719,6 +1754,7 @@ mod tests {
|
|||
with_vars(
|
||||
[
|
||||
("UV_TEST_PYTHON_PATH", None::<OsString>),
|
||||
("UV_TOOLCHAIN_DIR", Some(toolchains.root().into())),
|
||||
("PATH", None),
|
||||
("PWD", Some(tempdir.path().into())),
|
||||
],
|
||||
|
@ -1740,6 +1776,7 @@ mod tests {
|
|||
#[test]
|
||||
fn find_environment_allows_relative_file_path() -> Result<()> {
|
||||
let tempdir = TempDir::new()?;
|
||||
let toolchains = InstalledToolchains::temp()?;
|
||||
let cache = Cache::temp()?;
|
||||
tempdir.child("foo").create_dir_all()?;
|
||||
let python = tempdir.child("foo").join("bar");
|
||||
|
@ -1753,6 +1790,7 @@ mod tests {
|
|||
with_vars(
|
||||
[
|
||||
("UV_TEST_PYTHON_PATH", None::<OsString>),
|
||||
("UV_TOOLCHAIN_DIR", Some(toolchains.root().into())),
|
||||
("PATH", None),
|
||||
("PWD", Some(tempdir.path().into())),
|
||||
],
|
||||
|
@ -1777,6 +1815,7 @@ mod tests {
|
|||
#[test]
|
||||
fn find_environment_allows_absolute_file_path() -> Result<()> {
|
||||
let tempdir = TempDir::new()?;
|
||||
let toolchains = InstalledToolchains::temp()?;
|
||||
let cache = Cache::temp()?;
|
||||
tempdir.child("foo").create_dir_all()?;
|
||||
let python = tempdir.child("foo").join("bar");
|
||||
|
@ -1790,6 +1829,7 @@ mod tests {
|
|||
with_vars(
|
||||
[
|
||||
("UV_TEST_PYTHON_PATH", None::<OsString>),
|
||||
("UV_TOOLCHAIN_DIR", Some(toolchains.root().into())),
|
||||
("PATH", None),
|
||||
("PWD", Some(tempdir.path().into())),
|
||||
],
|
||||
|
@ -1814,6 +1854,7 @@ mod tests {
|
|||
#[test]
|
||||
fn find_environment_allows_venv_directory_path() -> Result<()> {
|
||||
let tempdir = TempDir::new()?;
|
||||
let toolchains = InstalledToolchains::temp()?;
|
||||
// Create a separate pwd to avoid ancestor discovery of the venv
|
||||
let pwd = TempDir::new()?;
|
||||
let cache = Cache::temp()?;
|
||||
|
@ -1822,6 +1863,7 @@ mod tests {
|
|||
with_vars(
|
||||
[
|
||||
("UV_TEST_PYTHON_PATH", None::<OsString>),
|
||||
("UV_TOOLCHAIN_DIR", Some(toolchains.root().into())),
|
||||
("PATH", None),
|
||||
("PWD", Some(pwd.path().into())),
|
||||
],
|
||||
|
@ -1846,6 +1888,7 @@ mod tests {
|
|||
#[test]
|
||||
fn find_environment_allows_file_path_with_system_explicit() -> Result<()> {
|
||||
let tempdir = TempDir::new()?;
|
||||
let toolchains = InstalledToolchains::temp()?;
|
||||
let cache = Cache::temp()?;
|
||||
tempdir.child("foo").create_dir_all()?;
|
||||
let python = tempdir.child("foo").join("bar");
|
||||
|
@ -1859,6 +1902,7 @@ mod tests {
|
|||
with_vars(
|
||||
[
|
||||
("UV_TEST_PYTHON_PATH", None::<OsString>),
|
||||
("UV_TOOLCHAIN_DIR", Some(toolchains.root().into())),
|
||||
("PATH", None),
|
||||
("PWD", Some(tempdir.path().into())),
|
||||
],
|
||||
|
@ -1883,6 +1927,7 @@ mod tests {
|
|||
#[test]
|
||||
fn find_environment_does_not_allow_file_path_with_system_disallowed() -> Result<()> {
|
||||
let tempdir = TempDir::new()?;
|
||||
let toolchains = InstalledToolchains::temp()?;
|
||||
let cache = Cache::temp()?;
|
||||
tempdir.child("foo").create_dir_all()?;
|
||||
let python = tempdir.child("foo").join("bar");
|
||||
|
@ -1896,6 +1941,7 @@ mod tests {
|
|||
with_vars(
|
||||
[
|
||||
("UV_TEST_PYTHON_PATH", None::<OsString>),
|
||||
("UV_TOOLCHAIN_DIR", Some(toolchains.root().into())),
|
||||
("PATH", None),
|
||||
("PWD", Some(tempdir.path().into())),
|
||||
],
|
||||
|
@ -1924,12 +1970,14 @@ mod tests {
|
|||
#[test]
|
||||
fn find_environment_treats_missing_file_path_as_file() -> Result<()> {
|
||||
let tempdir = TempDir::new()?;
|
||||
let toolchains = InstalledToolchains::temp()?;
|
||||
let cache = Cache::temp()?;
|
||||
tempdir.child("foo").create_dir_all()?;
|
||||
|
||||
with_vars(
|
||||
[
|
||||
("UV_TEST_PYTHON_PATH", None::<OsString>),
|
||||
("UV_TOOLCHAIN_DIR", Some(toolchains.root().into())),
|
||||
("PATH", None),
|
||||
("PWD", Some(tempdir.path().into())),
|
||||
],
|
||||
|
@ -1956,6 +2004,7 @@ mod tests {
|
|||
#[test]
|
||||
fn find_environment_executable_name_in_search_path() -> Result<()> {
|
||||
let tempdir = TempDir::new()?;
|
||||
let toolchains = InstalledToolchains::temp()?;
|
||||
let pwd = tempdir.child("pwd");
|
||||
pwd.create_dir_all()?;
|
||||
let cache = Cache::temp()?;
|
||||
|
@ -1970,6 +2019,7 @@ mod tests {
|
|||
with_vars(
|
||||
[
|
||||
("UV_TEST_PYTHON_PATH", None::<OsString>),
|
||||
("UV_TOOLCHAIN_DIR", Some(toolchains.root().into())),
|
||||
("PATH", Some(tempdir.path().into())),
|
||||
("PWD", Some(pwd.path().into())),
|
||||
],
|
||||
|
@ -1991,11 +2041,13 @@ mod tests {
|
|||
#[test]
|
||||
fn find_environment_pypy() -> Result<()> {
|
||||
let tempdir = TempDir::new()?;
|
||||
let toolchains = InstalledToolchains::temp()?;
|
||||
let cache = Cache::temp()?;
|
||||
|
||||
with_vars(
|
||||
[
|
||||
("UV_TEST_PYTHON_PATH", None::<OsString>),
|
||||
("UV_TOOLCHAIN_DIR", Some(toolchains.root().into())),
|
||||
(
|
||||
"PATH",
|
||||
Some(mock_interpreters(
|
||||
|
@ -2023,11 +2075,13 @@ mod tests {
|
|||
#[test]
|
||||
fn find_environment_pypy_request_ignores_cpython() -> Result<()> {
|
||||
let tempdir = TempDir::new()?;
|
||||
let toolchains = InstalledToolchains::temp()?;
|
||||
let cache = Cache::temp()?;
|
||||
|
||||
with_vars(
|
||||
[
|
||||
("UV_TEST_PYTHON_PATH", None::<OsString>),
|
||||
("UV_TOOLCHAIN_DIR", Some(toolchains.root().into())),
|
||||
(
|
||||
"PATH",
|
||||
Some(mock_interpreters(
|
||||
|
@ -2058,12 +2112,14 @@ mod tests {
|
|||
#[test]
|
||||
fn find_environment_pypy_request_skips_wrong_versions() -> Result<()> {
|
||||
let tempdir = TempDir::new()?;
|
||||
let toolchains = InstalledToolchains::temp()?;
|
||||
let cache = Cache::temp()?;
|
||||
|
||||
// We should prefer the `pypy` executable with the requested version
|
||||
with_vars(
|
||||
[
|
||||
("UV_TEST_PYTHON_PATH", None::<OsString>),
|
||||
("UV_TOOLCHAIN_DIR", Some(toolchains.root().into())),
|
||||
(
|
||||
"PATH",
|
||||
Some(mock_interpreters(
|
||||
|
@ -2094,12 +2150,14 @@ mod tests {
|
|||
#[test]
|
||||
fn find_environment_pypy_finds_executable_with_version_name() -> Result<()> {
|
||||
let tempdir = TempDir::new()?;
|
||||
let toolchains = InstalledToolchains::temp()?;
|
||||
let cache = Cache::temp()?;
|
||||
|
||||
// We should find executables that include the version number
|
||||
with_vars(
|
||||
[
|
||||
("UV_TEST_PYTHON_PATH", None::<OsString>),
|
||||
("UV_TOOLCHAIN_DIR", Some(toolchains.root().into())),
|
||||
(
|
||||
"PATH",
|
||||
Some(mock_interpreters(
|
||||
|
@ -2134,6 +2192,7 @@ mod tests {
|
|||
#[test]
|
||||
fn find_environment_pypy_prefers_executable_with_implementation_name() -> Result<()> {
|
||||
let tempdir = TempDir::new()?;
|
||||
let toolchains = InstalledToolchains::temp()?;
|
||||
let cache = Cache::temp()?;
|
||||
|
||||
// We should prefer `pypy` executables over `python` executables even if they are both pypy
|
||||
|
@ -2152,6 +2211,7 @@ mod tests {
|
|||
with_vars(
|
||||
[
|
||||
("UV_TEST_PYTHON_PATH", None::<OsString>),
|
||||
("UV_TOOLCHAIN_DIR", Some(toolchains.root().into())),
|
||||
("PATH", Some(tempdir.path().into())),
|
||||
("PWD", Some(tempdir.path().into())),
|
||||
],
|
||||
|
@ -2209,6 +2269,7 @@ mod tests {
|
|||
|
||||
// We should prefer executables with the version number over those with implementation names
|
||||
let tempdir = TempDir::new()?;
|
||||
let toolchains = InstalledToolchains::temp()?;
|
||||
create_mock_interpreter(
|
||||
&tempdir.path().join("pypy3.10"),
|
||||
&PythonVersion::from_str("3.10.0").unwrap(),
|
||||
|
@ -2224,6 +2285,7 @@ mod tests {
|
|||
with_vars(
|
||||
[
|
||||
("UV_TEST_PYTHON_PATH", None::<OsString>),
|
||||
("UV_TOOLCHAIN_DIR", Some(toolchains.root().into())),
|
||||
("PATH", Some(tempdir.path().into())),
|
||||
("PWD", Some(tempdir.path().into())),
|
||||
],
|
||||
|
@ -2258,6 +2320,7 @@ mod tests {
|
|||
with_vars(
|
||||
[
|
||||
("UV_TEST_PYTHON_PATH", None::<OsString>),
|
||||
("UV_TOOLCHAIN_DIR", Some(toolchains.root().into())),
|
||||
("PATH", Some(tempdir.path().into())),
|
||||
("PWD", Some(tempdir.path().into())),
|
||||
],
|
||||
|
|
|
@ -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)]
|
||||
|
|
|
@ -1,47 +1,152 @@
|
|||
use core::fmt;
|
||||
use fs_err as fs;
|
||||
use std::collections::BTreeSet;
|
||||
use std::ffi::OsStr;
|
||||
use std::path::PathBuf;
|
||||
use std::io::{self, Write};
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::str::FromStr;
|
||||
|
||||
use once_cell::sync::Lazy;
|
||||
use tracing::debug;
|
||||
|
||||
use uv_fs::Simplified;
|
||||
use uv_state::{StateBucket, StateStore};
|
||||
|
||||
use crate::managed::downloads::Error;
|
||||
use crate::platform::{Arch, Libc, Os};
|
||||
use crate::python_version::PythonVersion;
|
||||
|
||||
/// The directory where Python toolchains we install are stored.
|
||||
pub static TOOLCHAIN_DIRECTORY: Lazy<Option<PathBuf>> =
|
||||
Lazy::new(|| std::env::var_os("UV_BOOTSTRAP_DIR").map(PathBuf::from));
|
||||
/// A collection of installed Python toolchains.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct InstalledToolchains {
|
||||
/// The path to the top-level directory of the installed toolchains.
|
||||
root: PathBuf,
|
||||
}
|
||||
|
||||
pub fn toolchains_for_current_platform() -> Result<impl Iterator<Item = Toolchain>, Error> {
|
||||
let platform_key = platform_key_from_env()?;
|
||||
let iter = toolchain_directories()?
|
||||
.into_iter()
|
||||
// Sort "newer" versions of Python first
|
||||
.rev()
|
||||
.filter_map(move |path| {
|
||||
if path
|
||||
.file_name()
|
||||
.map(OsStr::to_string_lossy)
|
||||
.is_some_and(|filename| filename.ends_with(&platform_key))
|
||||
{
|
||||
Toolchain::new(path.clone())
|
||||
.inspect_err(|err| {
|
||||
debug!(
|
||||
"Ignoring invalid toolchain directory {}: {err}",
|
||||
path.user_display()
|
||||
);
|
||||
impl InstalledToolchains {
|
||||
/// A directory for installed toolchains at `root`.
|
||||
pub fn from_path(root: impl Into<PathBuf>) -> Result<Self, io::Error> {
|
||||
Ok(Self { root: root.into() })
|
||||
}
|
||||
|
||||
/// Prefer, in order:
|
||||
/// 1. The specific toolchain directory specified by the user, i.e., `UV_TOOLCHAIN_DIR`
|
||||
/// 2. A bucket in the system-appropriate user-level data directory, e.g., `~/.local/uv/toolchains`
|
||||
/// 3. A bucket in the local data directory, e.g., `./.uv/toolchains`
|
||||
pub fn from_settings() -> Result<Self, io::Error> {
|
||||
if let Some(toolchain_dir) = std::env::var_os("UV_TOOLCHAIN_DIR") {
|
||||
Self::from_path(toolchain_dir)
|
||||
} else {
|
||||
Self::from_path(StateStore::from_settings(None)?.bucket(StateBucket::Toolchains))
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a temporary installed toolchain directory.
|
||||
pub fn temp() -> Result<Self, io::Error> {
|
||||
Self::from_path(StateStore::temp()?.bucket(StateBucket::Toolchains))
|
||||
}
|
||||
|
||||
/// Initialize the installed toolchain directory.
|
||||
///
|
||||
/// Ensures the directory is created.
|
||||
pub fn init(self) -> Result<Self, io::Error> {
|
||||
let root = &self.root;
|
||||
|
||||
// Create the cache directory, if it doesn't exist.
|
||||
fs::create_dir_all(root)?;
|
||||
|
||||
// Add a .gitignore.
|
||||
match fs::OpenOptions::new()
|
||||
.write(true)
|
||||
.create_new(true)
|
||||
.open(root.join(".gitignore"))
|
||||
{
|
||||
Ok(mut file) => file.write_all(b"*")?,
|
||||
Err(err) if err.kind() == io::ErrorKind::AlreadyExists => (),
|
||||
Err(err) => return Err(err),
|
||||
}
|
||||
|
||||
Ok(self)
|
||||
}
|
||||
|
||||
/// Iterate over each installed toolchain in this directory.
|
||||
///
|
||||
/// Toolchains are sorted descending by name, such that we get deterministic
|
||||
/// ordering across platforms. This also results in newer Python versions coming first,
|
||||
/// but should not be relied on — instead the toolchains should be sorted later by
|
||||
/// the parsed Python version.
|
||||
fn find_all(&self) -> Result<impl DoubleEndedIterator<Item = Toolchain>, Error> {
|
||||
let dirs = match fs_err::read_dir(&self.root) {
|
||||
Ok(toolchain_dirs) => {
|
||||
// Collect sorted directory paths; `read_dir` is not stable across platforms
|
||||
let directories: BTreeSet<_> = toolchain_dirs
|
||||
.filter_map(|read_dir| match read_dir {
|
||||
Ok(entry) => match entry.file_type() {
|
||||
Ok(file_type) => file_type.is_dir().then_some(Ok(entry.path())),
|
||||
Err(err) => Some(Err(err)),
|
||||
},
|
||||
Err(err) => Some(Err(err)),
|
||||
})
|
||||
.ok()
|
||||
} else {
|
||||
None
|
||||
.collect::<Result<_, std::io::Error>>()
|
||||
.map_err(|err| Error::ReadError {
|
||||
dir: self.root.clone(),
|
||||
err,
|
||||
})?;
|
||||
directories
|
||||
}
|
||||
});
|
||||
Err(err) if err.kind() == std::io::ErrorKind::NotFound => BTreeSet::default(),
|
||||
Err(err) => {
|
||||
return Err(Error::ReadError {
|
||||
dir: self.root.clone(),
|
||||
err,
|
||||
})
|
||||
}
|
||||
};
|
||||
Ok(dirs
|
||||
.into_iter()
|
||||
.map(|path| Toolchain::new(path).unwrap())
|
||||
.rev())
|
||||
}
|
||||
|
||||
Ok(iter)
|
||||
/// Iterate over toolchains that support the current platform.
|
||||
pub fn find_matching_current_platform(
|
||||
&self,
|
||||
) -> Result<impl DoubleEndedIterator<Item = Toolchain>, Error> {
|
||||
let platform_key = platform_key_from_env()?;
|
||||
|
||||
let iter = InstalledToolchains::from_settings()?
|
||||
.find_all()?
|
||||
.filter(move |toolchain| {
|
||||
toolchain
|
||||
.path
|
||||
.file_name()
|
||||
.map(OsStr::to_string_lossy)
|
||||
.is_some_and(|filename| filename.ends_with(&platform_key))
|
||||
});
|
||||
|
||||
Ok(iter)
|
||||
}
|
||||
|
||||
/// Iterate over toolchains that satisfy the given Python version on this platform.
|
||||
///
|
||||
/// ## Errors
|
||||
///
|
||||
/// - The platform metadata cannot be read
|
||||
/// - A directory in the toolchain directory cannot be read
|
||||
pub fn find_version<'a>(
|
||||
&self,
|
||||
version: &'a PythonVersion,
|
||||
) -> Result<impl DoubleEndedIterator<Item = Toolchain> + 'a, Error> {
|
||||
Ok(self
|
||||
.find_matching_current_platform()?
|
||||
.filter(move |toolchain| {
|
||||
toolchain
|
||||
.path
|
||||
.file_name()
|
||||
.map(OsStr::to_string_lossy)
|
||||
.is_some_and(|filename| filename.starts_with(&format!("cpython-{version}")))
|
||||
}))
|
||||
}
|
||||
|
||||
pub fn root(&self) -> &Path {
|
||||
&self.root
|
||||
}
|
||||
}
|
||||
|
||||
/// An installed Python toolchain.
|
||||
|
@ -72,6 +177,7 @@ impl Toolchain {
|
|||
python_version,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn executable(&self) -> PathBuf {
|
||||
if cfg!(windows) {
|
||||
self.path.join("install").join("python.exe")
|
||||
|
@ -87,84 +193,6 @@ impl Toolchain {
|
|||
}
|
||||
}
|
||||
|
||||
/// Return the directories in the toolchain directory.
|
||||
///
|
||||
/// Toolchain directories are sorted descending by name, such that we get deterministic
|
||||
/// ordering across platforms. This also results in newer Python versions coming first,
|
||||
/// but should not be relied on — instead the toolchains should be sorted later by
|
||||
/// the parsed Python version.
|
||||
fn toolchain_directories() -> Result<BTreeSet<PathBuf>, Error> {
|
||||
let Some(toolchain_dir) = TOOLCHAIN_DIRECTORY.as_ref() else {
|
||||
return Ok(BTreeSet::default());
|
||||
};
|
||||
match fs_err::read_dir(toolchain_dir.clone()) {
|
||||
Ok(toolchain_dirs) => {
|
||||
// Collect sorted directory paths; `read_dir` is not stable across platforms
|
||||
let directories: BTreeSet<_> = toolchain_dirs
|
||||
.filter_map(|read_dir| match read_dir {
|
||||
Ok(entry) => match entry.file_type() {
|
||||
Ok(file_type) => file_type.is_dir().then_some(Ok(entry.path())),
|
||||
Err(err) => Some(Err(err)),
|
||||
},
|
||||
Err(err) => Some(Err(err)),
|
||||
})
|
||||
.collect::<Result<_, std::io::Error>>()
|
||||
.map_err(|err| Error::ReadError {
|
||||
dir: toolchain_dir.clone(),
|
||||
err,
|
||||
})?;
|
||||
Ok(directories)
|
||||
}
|
||||
Err(err) if err.kind() == std::io::ErrorKind::NotFound => Ok(BTreeSet::default()),
|
||||
Err(err) => Err(Error::ReadError {
|
||||
dir: toolchain_dir.clone(),
|
||||
err,
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
/// Return the toolchains that satisfy the given Python version on this platform.
|
||||
///
|
||||
/// ## Errors
|
||||
///
|
||||
/// - The platform metadata cannot be read
|
||||
/// - A directory in the toolchain directory cannot be read
|
||||
pub fn toolchains_for_version(version: &PythonVersion) -> Result<Vec<Toolchain>, Error> {
|
||||
let platform_key = platform_key_from_env()?;
|
||||
|
||||
// TODO(zanieb): Consider returning an iterator instead of a `Vec`
|
||||
// Note we need to collect paths regardless for sorting by version.
|
||||
|
||||
let toolchain_dirs = toolchain_directories()?;
|
||||
|
||||
Ok(toolchain_dirs
|
||||
.into_iter()
|
||||
// Sort "newer" versions of Python first
|
||||
.rev()
|
||||
.filter_map(|path| {
|
||||
if path
|
||||
.file_name()
|
||||
.map(OsStr::to_string_lossy)
|
||||
.is_some_and(|filename| {
|
||||
filename.starts_with(&format!("cpython-{version}"))
|
||||
&& filename.ends_with(&platform_key)
|
||||
})
|
||||
{
|
||||
Toolchain::new(path.clone())
|
||||
.inspect_err(|err| {
|
||||
debug!(
|
||||
"Ignoring invalid toolchain directory {}: {err}",
|
||||
path.user_display()
|
||||
);
|
||||
})
|
||||
.ok()
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>())
|
||||
}
|
||||
|
||||
/// Generate a platform portion of a key from the environment.
|
||||
fn platform_key_from_env() -> Result<String, Error> {
|
||||
let os = Os::from_env()?;
|
||||
|
@ -172,3 +200,16 @@ fn platform_key_from_env() -> Result<String, Error> {
|
|||
let libc = Libc::from_env();
|
||||
Ok(format!("{os}-{arch}-{libc}").to_lowercase())
|
||||
}
|
||||
|
||||
impl fmt::Display for Toolchain {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(
|
||||
f,
|
||||
"{}",
|
||||
self.path
|
||||
.file_name()
|
||||
.unwrap_or(self.path.as_os_str())
|
||||
.to_string_lossy()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
18
crates/uv-state/Cargo.toml
Normal file
18
crates/uv-state/Cargo.toml
Normal 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
108
crates/uv-state/src/lib.rs
Normal 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",
|
||||
}
|
||||
}
|
||||
}
|
|
@ -9,13 +9,14 @@ use regex::Regex;
|
|||
use std::borrow::BorrowMut;
|
||||
use std::env;
|
||||
use std::ffi::OsString;
|
||||
use std::iter::Iterator;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::process::Output;
|
||||
use std::str::FromStr;
|
||||
|
||||
use uv_cache::Cache;
|
||||
use uv_fs::Simplified;
|
||||
use uv_interpreter::managed::toolchains_for_version;
|
||||
use uv_interpreter::managed::InstalledToolchains;
|
||||
use uv_interpreter::{
|
||||
find_interpreter, InterpreterRequest, PythonVersion, SourceSelector, VersionRequest,
|
||||
};
|
||||
|
@ -337,15 +338,22 @@ pub fn create_venv<Parent: assert_fs::prelude::PathChild + AsRef<std::path::Path
|
|||
cache_dir: &assert_fs::TempDir,
|
||||
python: &str,
|
||||
) -> PathBuf {
|
||||
let python = toolchains_for_version(
|
||||
&PythonVersion::from_str(python).expect("Tests should use a valid Python version"),
|
||||
)
|
||||
.expect("Tests are run on a supported platform")
|
||||
.first()
|
||||
.map(uv_interpreter::managed::Toolchain::executable)
|
||||
// We'll search for the request Python on the PATH if not found in the toolchain versions
|
||||
// We hack this into a `PathBuf` to satisfy the compiler but it's just a string
|
||||
.unwrap_or(PathBuf::from(python));
|
||||
let python = InstalledToolchains::from_settings()
|
||||
.map(|installed_toolchains| {
|
||||
installed_toolchains
|
||||
.find_version(
|
||||
&PythonVersion::from_str(python)
|
||||
.expect("Tests should use a valid Python version"),
|
||||
)
|
||||
.expect("Tests are run on a supported platform")
|
||||
.next()
|
||||
.as_ref()
|
||||
.map(uv_interpreter::managed::Toolchain::executable)
|
||||
})
|
||||
// We'll search for the request Python on the PATH if not found in the toolchain versions
|
||||
// We hack this into a `PathBuf` to satisfy the compiler but it's just a string
|
||||
.unwrap_or_default()
|
||||
.unwrap_or(PathBuf::from(python));
|
||||
|
||||
let venv = temp_dir.child(".venv");
|
||||
Command::new(get_bin())
|
||||
|
@ -380,20 +388,24 @@ pub fn python_path_with_versions(
|
|||
let selected_pythons = python_versions
|
||||
.iter()
|
||||
.flat_map(|python_version| {
|
||||
let inner = toolchains_for_version(
|
||||
&PythonVersion::from_str(python_version)
|
||||
.expect("Tests should use a valid Python version"),
|
||||
)
|
||||
.expect("Tests are run on a supported platform")
|
||||
.iter()
|
||||
.map(|toolchain| {
|
||||
toolchain
|
||||
.executable()
|
||||
.parent()
|
||||
.expect("Executables must exist in a directory")
|
||||
.to_path_buf()
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
let inner = InstalledToolchains::from_settings()
|
||||
.map(|toolchains| {
|
||||
toolchains
|
||||
.find_version(
|
||||
&PythonVersion::from_str(python_version)
|
||||
.expect("Tests should use a valid Python version"),
|
||||
)
|
||||
.expect("Tests are run on a supported platform")
|
||||
.map(|toolchain| {
|
||||
toolchain
|
||||
.executable()
|
||||
.parent()
|
||||
.expect("Executables must exist in a directory")
|
||||
.to_path_buf()
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
})
|
||||
.unwrap_or_default();
|
||||
if inner.is_empty() {
|
||||
// Fallback to a system lookup if we failed to find one in the toolchain directory
|
||||
let request = InterpreterRequest::Version(
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue