Skip to content

Commit 6c6eeaa

Browse files
committed
Use an extra layer of ephemeral deps
1 parent 38ee6ec commit 6c6eeaa

File tree

15 files changed

+537
-116
lines changed

15 files changed

+537
-116
lines changed

crates/uv/src/commands/project/environment.rs

Lines changed: 63 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,69 @@ use crate::commands::project::{
1717
use crate::printer::Printer;
1818
use crate::settings::{NetworkSettings, ResolverInstallerSettings};
1919

20+
/// An ephemeral [`PythonEnvironment`] for running an individual command.
21+
#[derive(Debug)]
22+
pub(crate) struct EphemeralEnvironment(PythonEnvironment);
23+
24+
impl From<PythonEnvironment> for EphemeralEnvironment {
25+
fn from(environment: PythonEnvironment) -> Self {
26+
Self(environment)
27+
}
28+
}
29+
30+
impl From<EphemeralEnvironment> for PythonEnvironment {
31+
fn from(environment: EphemeralEnvironment) -> Self {
32+
environment.0
33+
}
34+
}
35+
36+
impl EphemeralEnvironment {
37+
/// Set the ephemeral overlay for a Python environment.
38+
#[allow(clippy::result_large_err)]
39+
pub(crate) fn set_overlay(&self, contents: impl AsRef<[u8]>) -> Result<(), ProjectError> {
40+
let site_packages = self
41+
.0
42+
.site_packages()
43+
.next()
44+
.ok_or(ProjectError::NoSitePackages)?;
45+
let overlay_path = site_packages.join("_uv_ephemeral_overlay.pth");
46+
fs_err::write(overlay_path, contents)?;
47+
Ok(())
48+
}
49+
50+
/// Enable system site packages for a Python environment.
51+
#[allow(clippy::result_large_err)]
52+
pub(crate) fn set_system_site_packages(&self) -> Result<(), ProjectError> {
53+
self.0
54+
.set_pyvenv_cfg("include-system-site-packages", "true")?;
55+
Ok(())
56+
}
57+
58+
/// Set the `extends-environment` key in the `pyvenv.cfg` file to the given path.
59+
///
60+
/// Ephemeral environments created by `uv run --with` extend a parent (virtual or system)
61+
/// environment by adding a `.pth` file to the ephemeral environment's `site-packages`
62+
/// directory. The `pth` file contains Python code to dynamically add the parent
63+
/// environment's `site-packages` directory to Python's import search paths in addition to
64+
/// the ephemeral environment's `site-packages` directory. This works well at runtime, but
65+
/// is too dynamic for static analysis tools like ty to understand. As such, we
66+
/// additionally write the `sys.prefix` of the parent environment to to the
67+
/// `extends-environment` key of the ephemeral environment's `pyvenv.cfg` file, making it
68+
/// easier for these tools to statically and reliably understand the relationship between
69+
/// the two environments.
70+
#[allow(clippy::result_large_err)]
71+
pub(crate) fn set_parent_environment(
72+
&self,
73+
parent_environment_sys_prefix: &Path,
74+
) -> Result<(), ProjectError> {
75+
self.0.set_pyvenv_cfg(
76+
"extends-environment",
77+
&parent_environment_sys_prefix.escape_for_python(),
78+
)?;
79+
Ok(())
80+
}
81+
}
82+
2083
/// A [`PythonEnvironment`] stored in the cache.
2184
#[derive(Debug)]
2285
pub(crate) struct CachedEnvironment(PythonEnvironment);
@@ -150,76 +213,6 @@ impl CachedEnvironment {
150213
Ok(Self(PythonEnvironment::from_root(root, cache)?))
151214
}
152215

153-
/// Set the ephemeral overlay for a Python environment.
154-
#[allow(clippy::result_large_err)]
155-
pub(crate) fn set_overlay(&self, contents: impl AsRef<[u8]>) -> Result<(), ProjectError> {
156-
let site_packages = self
157-
.0
158-
.site_packages()
159-
.next()
160-
.ok_or(ProjectError::NoSitePackages)?;
161-
let overlay_path = site_packages.join("_uv_ephemeral_overlay.pth");
162-
fs_err::write(overlay_path, contents)?;
163-
Ok(())
164-
}
165-
166-
/// Clear the ephemeral overlay for a Python environment, if it exists.
167-
#[allow(clippy::result_large_err)]
168-
pub(crate) fn clear_overlay(&self) -> Result<(), ProjectError> {
169-
let site_packages = self
170-
.0
171-
.site_packages()
172-
.next()
173-
.ok_or(ProjectError::NoSitePackages)?;
174-
let overlay_path = site_packages.join("_uv_ephemeral_overlay.pth");
175-
match fs_err::remove_file(overlay_path) {
176-
Ok(()) => (),
177-
Err(err) if err.kind() == std::io::ErrorKind::NotFound => (),
178-
Err(err) => return Err(ProjectError::OverlayRemoval(err)),
179-
}
180-
Ok(())
181-
}
182-
183-
/// Enable system site packages for a Python environment.
184-
#[allow(clippy::result_large_err)]
185-
pub(crate) fn set_system_site_packages(&self) -> Result<(), ProjectError> {
186-
self.0
187-
.set_pyvenv_cfg("include-system-site-packages", "true")?;
188-
Ok(())
189-
}
190-
191-
/// Disable system site packages for a Python environment.
192-
#[allow(clippy::result_large_err)]
193-
pub(crate) fn clear_system_site_packages(&self) -> Result<(), ProjectError> {
194-
self.0
195-
.set_pyvenv_cfg("include-system-site-packages", "false")?;
196-
Ok(())
197-
}
198-
199-
/// Set the `extends-environment` key in the `pyvenv.cfg` file to the given path.
200-
///
201-
/// Ephemeral environments created by `uv run --with` extend a parent (virtual or system)
202-
/// environment by adding a `.pth` file to the ephemeral environment's `site-packages`
203-
/// directory. The `pth` file contains Python code to dynamically add the parent
204-
/// environment's `site-packages` directory to Python's import search paths in addition to
205-
/// the ephemeral environment's `site-packages` directory. This works well at runtime, but
206-
/// is too dynamic for static analysis tools like ty to understand. As such, we
207-
/// additionally write the `sys.prefix` of the parent environment to to the
208-
/// `extends-environment` key of the ephemeral environment's `pyvenv.cfg` file, making it
209-
/// easier for these tools to statically and reliably understand the relationship between
210-
/// the two environments.
211-
#[allow(clippy::result_large_err)]
212-
pub(crate) fn set_parent_environment(
213-
&self,
214-
parent_environment_sys_prefix: &Path,
215-
) -> Result<(), ProjectError> {
216-
self.0.set_pyvenv_cfg(
217-
"extends-environment",
218-
&parent_environment_sys_prefix.escape_for_python(),
219-
)?;
220-
Ok(())
221-
}
222-
223216
/// Return the [`Interpreter`] to use for the cached environment, based on a given
224217
/// [`Interpreter`].
225218
///

crates/uv/src/commands/project/mod.rs

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -200,9 +200,6 @@ pub(crate) enum ProjectError {
200200
#[error("Failed to parse PEP 723 script metadata")]
201201
Pep723ScriptTomlParse(#[source] toml::de::Error),
202202

203-
#[error("Failed to remove ephemeral overlay")]
204-
OverlayRemoval(#[source] std::io::Error),
205-
206203
#[error("Failed to find `site-packages` directory for environment")]
207204
NoSitePackages,
208205

crates/uv/src/commands/project/run.rs

Lines changed: 77 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ use crate::commands::pip::loggers::{
4444
DefaultInstallLogger, DefaultResolveLogger, SummaryInstallLogger, SummaryResolveLogger,
4545
};
4646
use crate::commands::pip::operations::Modifications;
47-
use crate::commands::project::environment::CachedEnvironment;
47+
use crate::commands::project::environment::{CachedEnvironment, EphemeralEnvironment};
4848
use crate::commands::project::install_target::InstallTarget;
4949
use crate::commands::project::lock::LockMode;
5050
use crate::commands::project::lock_target::LockTarget;
@@ -939,16 +939,15 @@ hint: If you are running a script with `{}` in the shebang, you may need to incl
939939

940940
// If necessary, create an environment for the ephemeral requirements or command.
941941
let base_site_packages = SitePackages::from_interpreter(&base_interpreter)?;
942-
let ephemeral_env = match spec {
942+
let requirements_env = match spec {
943943
None => None,
944944
Some(spec)
945945
if can_skip_ephemeral(&spec, &base_interpreter, &base_site_packages, &settings) =>
946946
{
947947
None
948948
}
949949
Some(spec) => {
950-
debug!("Syncing ephemeral requirements");
951-
950+
debug!("Creating ephemeral environment");
952951
// Read the build constraints from the lock file.
953952
let build_constraints = base_lock
954953
.as_ref()
@@ -1008,54 +1007,87 @@ hint: If you are running a script with `{}` in the shebang, you may need to incl
10081007
Err(err) => return Err(err.into()),
10091008
};
10101009

1011-
Some(environment)
1010+
Some(PythonEnvironment::from(environment))
10121011
}
10131012
};
10141013

1015-
// If we're running in an ephemeral environment, add a path file to enable loading of
1016-
// the base environment's site packages. Setting `PYTHONPATH` is insufficient, as it doesn't
1017-
// resolve `.pth` files in the base environment.
1014+
// If we're layering requirements atop the project environment, run the command in an ephemeral,
1015+
// isolated environment. Otherwise, modifications to the "active virtual environment" would
1016+
// poison the cache.
1017+
let ephemeral_dir = requirements_env
1018+
.as_ref()
1019+
.map(|_| cache.venv_dir())
1020+
.transpose()?;
1021+
1022+
let ephemeral_env = ephemeral_dir
1023+
.as_ref()
1024+
.map(|dir| {
1025+
uv_virtualenv::create_venv(
1026+
dir.path(),
1027+
base_interpreter.clone(),
1028+
uv_virtualenv::Prompt::None,
1029+
false,
1030+
false,
1031+
false,
1032+
false,
1033+
false,
1034+
preview,
1035+
)
1036+
})
1037+
.transpose()?
1038+
.map(EphemeralEnvironment::from);
1039+
1040+
// If we're running in an ephemeral environment, add a path file to enable loading from the
1041+
// `--with` requirements environment and the project environment site packages.
10181042
//
1019-
// `sitecustomize.py` would be an alternative, but it can be shadowed by an existing such
1020-
// module in the python installation.
1043+
// Setting `PYTHONPATH` is insufficient, as it doesn't resolve `.pth` files in the base
1044+
// environment. Adding `sitecustomize.py` would be an alternative, but it can be shadowed by an
1045+
// existing such module in the python installation.
10211046
if let Some(ephemeral_env) = ephemeral_env.as_ref() {
1022-
let site_packages = base_interpreter
1023-
.site_packages()
1024-
.next()
1025-
.ok_or_else(|| ProjectError::NoSitePackages)?;
1026-
ephemeral_env.set_overlay(format!(
1027-
"import site; site.addsitedir(\"{}\")",
1028-
site_packages.escape_for_python()
1029-
))?;
1030-
1031-
// Write the `sys.prefix` of the parent environment to the `extends-environment` key of the `pyvenv.cfg`
1032-
// file. This helps out static-analysis tools such as ty (see docs on
1033-
// `CachedEnvironment::set_parent_environment`).
1034-
//
1035-
// Note that we do this even if the parent environment is not a virtual environment.
1036-
// For ephemeral environments created by `uv run --with`, the parent environment's
1037-
// `site-packages` directory is added to `sys.path` even if the parent environment is not
1038-
// a virtual environment and even if `--system-site-packages` was not explicitly selected.
1039-
ephemeral_env.set_parent_environment(base_interpreter.sys_prefix())?;
1040-
1041-
// If `--system-site-packages` is enabled, add the system site packages to the ephemeral
1042-
// environment.
1043-
if base_interpreter.is_virtualenv()
1044-
&& PyVenvConfiguration::parse(base_interpreter.sys_prefix().join("pyvenv.cfg"))
1045-
.is_ok_and(|cfg| cfg.include_system_site_packages())
1046-
{
1047-
ephemeral_env.set_system_site_packages()?;
1048-
} else {
1049-
ephemeral_env.clear_system_site_packages()?;
1047+
if let Some(requirements_env) = requirements_env.as_ref() {
1048+
let requirements_site_packages =
1049+
requirements_env.site_packages().next().ok_or_else(|| {
1050+
anyhow!("Requirements environment has no site packages directory")
1051+
})?;
1052+
let base_site_packages = base_interpreter
1053+
.site_packages()
1054+
.next()
1055+
.ok_or_else(|| anyhow!("Base environment has no site packages directory"))?;
1056+
1057+
ephemeral_env.set_overlay(format!(
1058+
"import site; site.addsitedir(\"{}\"); site.addsitedir(\"{}\");",
1059+
base_site_packages.escape_for_python(),
1060+
requirements_site_packages.escape_for_python(),
1061+
))?;
1062+
1063+
// Write the `sys.prefix` of the parent environment to the `extends-environment` key of the `pyvenv.cfg`
1064+
// file. This helps out static-analysis tools such as ty (see docs on
1065+
// `CachedEnvironment::set_parent_environment`).
1066+
//
1067+
// Note that we do this even if the parent environment is not a virtual environment.
1068+
// For ephemeral environments created by `uv run --with`, the parent environment's
1069+
// `site-packages` directory is added to `sys.path` even if the parent environment is not
1070+
// a virtual environment and even if `--system-site-packages` was not explicitly selected.
1071+
ephemeral_env.set_parent_environment(base_interpreter.sys_prefix())?;
1072+
1073+
// If `--system-site-packages` is enabled, add the system site packages to the ephemeral
1074+
// environment.
1075+
if base_interpreter.is_virtualenv()
1076+
&& PyVenvConfiguration::parse(base_interpreter.sys_prefix().join("pyvenv.cfg"))
1077+
.is_ok_and(|cfg| cfg.include_system_site_packages())
1078+
{
1079+
ephemeral_env.set_system_site_packages()?;
1080+
}
10501081
}
10511082
}
10521083

1053-
// Cast from `CachedEnvironment` to `PythonEnvironment`.
1084+
// Cast to `PythonEnvironment`.
10541085
let ephemeral_env = ephemeral_env.map(PythonEnvironment::from);
10551086

10561087
// Determine the Python interpreter to use for the command, if necessary.
10571088
let interpreter = ephemeral_env
10581089
.as_ref()
1090+
.or(requirements_env.as_ref())
10591091
.map_or_else(|| &base_interpreter, |env| env.interpreter());
10601092

10611093
// Check if any run command is given.
@@ -1138,6 +1170,12 @@ hint: If you are running a script with `{}` in the shebang, you may need to incl
11381170
.as_ref()
11391171
.map(PythonEnvironment::scripts)
11401172
.into_iter()
1173+
.chain(
1174+
requirements_env
1175+
.as_ref()
1176+
.map(PythonEnvironment::scripts)
1177+
.into_iter(),
1178+
)
11411179
.chain(std::iter::once(base_interpreter.scripts()))
11421180
.chain(
11431181
// On Windows, non-virtual Python distributions put `python.exe` in the top-level

crates/uv/src/commands/tool/run.rs

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1080,9 +1080,5 @@ async fn get_or_create_environment(
10801080
},
10811081
};
10821082

1083-
// Clear any existing overlay.
1084-
environment.clear_overlay()?;
1085-
environment.clear_system_site_packages()?;
1086-
10871083
Ok((from, environment.into()))
10881084
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
{
2+
"cells": [],
3+
"metadata": {},
4+
"nbformat": 4,
5+
"nbformat_minor": 5
6+
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
{
2+
"cells": [],
3+
"metadata": {},
4+
"nbformat": 4,
5+
"nbformat_minor": 5
6+
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
{
2+
"cells": [],
3+
"metadata": {},
4+
"nbformat": 4,
5+
"nbformat_minor": 5
6+
}

foo/.python-version

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
3.12

foo/README.md

Whitespace-only changes.

0 commit comments

Comments
 (0)