@@ -17,6 +17,69 @@ use crate::commands::project::{
1717use crate :: printer:: Printer ;
1818use 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 ) ]
2285pub ( crate ) struct CachedEnvironment ( PythonEnvironment ) ;
@@ -44,15 +107,13 @@ impl CachedEnvironment {
44107 printer : Printer ,
45108 preview : PreviewMode ,
46109 ) -> 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) ?;
50111
51112 // Resolve the requirements with the interpreter.
52113 let resolution = Resolution :: from (
53114 resolve_environment (
54115 spec,
55- & base_interpreter ,
116+ & interpreter ,
56117 build_constraints. clone ( ) ,
57118 & settings. resolver ,
58119 network_settings,
@@ -80,29 +141,20 @@ impl CachedEnvironment {
80141 // Use the canonicalized base interpreter path since that's the interpreter we performed the
81142 // resolution with and the interpreter the environment will be created with.
82143 //
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.
95149 //
96150 // TODO(zanieb): We should include the version of the base interpreter in the hash, so if
97151 // the interpreter at the canonicalized path changes versions we construct a new
98152 // 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 ( ) ) ?) ;
103155
104156 // 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) ;
106158
107159 if cache. refresh ( ) . is_none ( ) {
108160 if let Ok ( root) = cache. resolve_link ( cache_entry. path ( ) ) {
@@ -116,7 +168,7 @@ impl CachedEnvironment {
116168 let temp_dir = cache. venv_dir ( ) ?;
117169 let venv = uv_virtualenv:: create_venv (
118170 temp_dir. path ( ) ,
119- base_interpreter ,
171+ interpreter ,
120172 uv_virtualenv:: Prompt :: None ,
121173 false ,
122174 false ,
@@ -150,76 +202,6 @@ impl CachedEnvironment {
150202 Ok ( Self ( PythonEnvironment :: from_root ( root, cache) ?) )
151203 }
152204
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-
223205 /// Return the [`Interpreter`] to use for the cached environment, based on a given
224206 /// [`Interpreter`].
225207 ///
0 commit comments