@@ -44,7 +44,7 @@ use crate::commands::pip::loggers::{
4444 DefaultInstallLogger , DefaultResolveLogger , SummaryInstallLogger , SummaryResolveLogger ,
4545} ;
4646use crate :: commands:: pip:: operations:: Modifications ;
47- use crate :: commands:: project:: environment:: CachedEnvironment ;
47+ use crate :: commands:: project:: environment:: { CachedEnvironment , EphemeralEnvironment } ;
4848use crate :: commands:: project:: install_target:: InstallTarget ;
4949use crate :: commands:: project:: lock:: LockMode ;
5050use 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
0 commit comments