@@ -17,6 +17,69 @@ use crate::commands::project::{
17
17
use crate :: printer:: Printer ;
18
18
use crate :: settings:: { NetworkSettings , ResolverInstallerSettings } ;
19
19
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
+
20
83
/// A [`PythonEnvironment`] stored in the cache.
21
84
#[ derive( Debug ) ]
22
85
pub ( crate ) struct CachedEnvironment ( PythonEnvironment ) ;
@@ -44,15 +107,13 @@ impl CachedEnvironment {
44
107
printer : Printer ,
45
108
preview : PreviewMode ,
46
109
) -> Result < Self , ProjectError > {
47
- // Resolve the "base" interpreter, which resolves to an underlying parent interpreter if the
48
- // given interpreter is a virtual environment.
49
- let base_interpreter = Self :: base_interpreter ( interpreter, cache) ?;
110
+ let interpreter = Self :: base_interpreter ( interpreter, cache) ?;
50
111
51
112
// Resolve the requirements with the interpreter.
52
113
let resolution = Resolution :: from (
53
114
resolve_environment (
54
115
spec,
55
- & base_interpreter ,
116
+ & interpreter ,
56
117
build_constraints. clone ( ) ,
57
118
& settings. resolver ,
58
119
network_settings,
@@ -80,29 +141,20 @@ impl CachedEnvironment {
80
141
// Use the canonicalized base interpreter path since that's the interpreter we performed the
81
142
// resolution with and the interpreter the environment will be created with.
82
143
//
83
- // We also include the canonicalized `sys.prefix` of the non-base interpreter, that is, the
84
- // virtual environment's path. Originally, we shared cached environments independent of the
85
- // environment they'd be layered on top of. However, this causes collisions as the overlay
86
- // `.pth` file can be overridden by another instance of uv. Including this element in the key
87
- // avoids this problem at the cost of creating separate cached environments for identical
88
- // `--with` invocations across projects. We use `sys.prefix` rather than `sys.executable` so
89
- // we can canonicalize it without invalidating the purpose of the element — it'd probably be
90
- // safe to just use the absolute `sys.executable` as well.
91
- //
92
- // TODO(zanieb): Since we're not sharing these environmments across projects, we should move
93
- // [`CachedEvnvironment::set_overlay`] etc. here since the values there should be constant
94
- // now.
144
+ // We cache environments independent of the environment they'd be layered on top of. The
145
+ // assumption is such that the environment will _not_ be modified by the user or uv;
146
+ // otherwise, we risk cache poisoning. For example, if we were to write a `.pth` file to
147
+ // the cached environment, it would be shared across all projects that use the same
148
+ // interpreter and the same cached dependencies.
95
149
//
96
150
// TODO(zanieb): We should include the version of the base interpreter in the hash, so if
97
151
// the interpreter at the canonicalized path changes versions we construct a new
98
152
// environment.
99
- let environment_hash = cache_digest ( & (
100
- & canonicalize_executable ( base_interpreter. sys_executable ( ) ) ?,
101
- & interpreter. sys_prefix ( ) . canonicalize ( ) ?,
102
- ) ) ;
153
+ let interpreter_hash =
154
+ cache_digest ( & canonicalize_executable ( interpreter. sys_executable ( ) ) ?) ;
103
155
104
156
// Search in the content-addressed cache.
105
- let cache_entry = cache. entry ( CacheBucket :: Environments , environment_hash , resolution_hash) ;
157
+ let cache_entry = cache. entry ( CacheBucket :: Environments , interpreter_hash , resolution_hash) ;
106
158
107
159
if cache. refresh ( ) . is_none ( ) {
108
160
if let Ok ( root) = cache. resolve_link ( cache_entry. path ( ) ) {
@@ -116,7 +168,7 @@ impl CachedEnvironment {
116
168
let temp_dir = cache. venv_dir ( ) ?;
117
169
let venv = uv_virtualenv:: create_venv (
118
170
temp_dir. path ( ) ,
119
- base_interpreter ,
171
+ interpreter ,
120
172
uv_virtualenv:: Prompt :: None ,
121
173
false ,
122
174
false ,
@@ -150,76 +202,6 @@ impl CachedEnvironment {
150
202
Ok ( Self ( PythonEnvironment :: from_root ( root, cache) ?) )
151
203
}
152
204
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 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
-
223
205
/// Return the [`Interpreter`] to use for the cached environment, based on a given
224
206
/// [`Interpreter`].
225
207
///
0 commit comments