@@ -10,7 +10,7 @@ use futures::StreamExt;
1010use itertools:: Itertools ;
1111use owo_colors:: OwoColorize ;
1212use tokio:: process:: Command ;
13- use tracing:: { debug, warn} ;
13+ use tracing:: { debug, trace , warn} ;
1414use url:: Url ;
1515
1616use uv_cache:: Cache ;
@@ -22,7 +22,7 @@ use uv_configuration::{
2222} ;
2323use uv_distribution_types:: Requirement ;
2424use uv_fs:: which:: is_executable;
25- use uv_fs:: { PythonExt , Simplified } ;
25+ use uv_fs:: { PythonExt , Simplified , create_symlink , symlink_or_copy_file } ;
2626use uv_installer:: { SatisfiesResult , SitePackages } ;
2727use uv_normalize:: { DefaultExtras , DefaultGroups , PackageName } ;
2828use uv_python:: {
@@ -1071,47 +1071,95 @@ hint: If you are running a script with `{}` in the shebang, you may need to incl
10711071 requirements_site_packages. escape_for_python( ) ,
10721072 ) ) ?;
10731073
1074- for interpreter in [ & base_interpreter, requirements_env. interpreter ( ) ] {
1075- // Copy every binary from the base environment to the ephemeral environment.
1074+ // N.B. The order here matters — earlier interpreters take precedence over the
1075+ // later ones.
1076+ for interpreter in [ requirements_env. interpreter ( ) , & base_interpreter] {
1077+ // Copy each entrypoint from the base environments to the ephemeral environment.
10761078 for entry in fs_err:: read_dir ( interpreter. scripts ( ) ) ? {
10771079 let entry = entry?;
10781080
10791081 if !entry. file_type ( ) ?. is_file ( ) {
10801082 continue ;
10811083 }
10821084
1083- // Read the whole file.
1084- let contents = fs_err:: read_to_string ( & entry. path ( ) ) ?;
1085-
1085+ let contents = fs_err:: read_to_string ( entry. path ( ) ) ?;
10861086 let expected = r#"#!/bin/sh
10871087'''exec' "$(dirname -- "$(realpath -- "$0")")"/'python' "$0" "$@"
10881088' '''
10891089"# ;
1090- // let expected = format!("#!{}\n", interpreter.sys_executable().display());
1091- // println!("Expected shebang: {expected}");
1092-
1093- // Must start with a shebang.
1094- if let Some ( contents) = contents. strip_prefix ( & expected) {
1095- let contents = format ! (
1096- "#!{}\n {}" ,
1097- ephemeral_env. sys_executable( ) . display( ) ,
1098- contents
1099- ) ;
1100- // Write the file to the ephemeral environment's scripts directory.
1101- let target_path = ephemeral_env. scripts ( ) . join ( entry. file_name ( ) ) ;
1102- fs_err:: write ( & target_path, & contents) ?;
1103-
1104- // Set the permissions to be executable.
1105- #[ cfg( unix) ]
1106- {
1107- use std:: os:: unix:: fs:: PermissionsExt ;
1108- let mut perms = fs_err:: metadata ( & target_path) ?. permissions ( ) ;
1109- perms. set_mode ( 0o755 ) ;
1110- fs_err:: set_permissions ( & target_path, perms) ?;
1111- }
11121090
1113- println ! ( "Writing to: {}" , target_path. display( ) ) ;
1114- // println!("{contents}");
1091+ // Only rewrite entrypoints that use the expected shebang.
1092+ let Some ( contents) = contents. strip_prefix ( expected) else {
1093+ continue ;
1094+ } ;
1095+
1096+ let contents = format ! (
1097+ "#!{}\n {}" ,
1098+ ephemeral_env. sys_executable( ) . display( ) ,
1099+ contents
1100+ ) ;
1101+ // Write the file to the ephemeral environment's scripts directory.
1102+ let target_path = ephemeral_env. scripts ( ) . join ( entry. file_name ( ) ) ;
1103+ fs_err:: write ( & target_path, & contents) ?;
1104+
1105+ match fs_err:: write ( & target_path, & contents) {
1106+ Ok ( ( ) ) => trace ! ( "Updated entrypoint at {}" , target_path. user_display( ) ) ,
1107+ Err ( err) if err. kind ( ) == std:: io:: ErrorKind :: AlreadyExists => { }
1108+ Err ( err) => return Err ( err. into ( ) ) ,
1109+ }
1110+
1111+ // Set the permissions to be executable.
1112+ #[ cfg( unix) ]
1113+ {
1114+ use std:: os:: unix:: fs:: PermissionsExt ;
1115+ let mut perms = fs_err:: metadata ( & target_path) ?. permissions ( ) ;
1116+ perms. set_mode ( 0o755 ) ;
1117+ fs_err:: set_permissions ( & target_path, perms) ?;
1118+ }
1119+ }
1120+
1121+ // Link common data directories from the base environment to the ephemeral
1122+ // environment. This is critical for Jupyter Lab, which cannot operate without the
1123+ // files it writes to `<prefix>/share`.
1124+ //
1125+ // We only perform a shallow merge here, so `<prefix>/share/<name>` will only be
1126+ // written once and contents beyond the first level, e.g.,
1127+ // `<prefix>/share/<name>/foo` will not be merged across parent interpreters.
1128+ for dir in & [ "etc" , "share" ] {
1129+ let entries = match fs_err:: read_dir ( interpreter. sys_prefix ( ) . join ( dir) ) {
1130+ Ok ( entries) => entries,
1131+ // Skip missing directories
1132+ Err ( err) if err. kind ( ) == std:: io:: ErrorKind :: NotFound => continue ,
1133+ Err ( err) => return Err ( err. into ( ) ) ,
1134+ } ;
1135+ fs_err:: create_dir_all ( ephemeral_env. sys_prefix ( ) . join ( dir) ) ?;
1136+ for entry in entries {
1137+ let entry = entry?;
1138+ let target = ephemeral_env. sys_prefix ( ) . join ( dir) . join ( entry. file_name ( ) ) ;
1139+
1140+ if entry. file_type ( ) ?. is_file ( ) {
1141+ match symlink_or_copy_file ( entry. path ( ) , & target) {
1142+ Ok ( ( ) ) => trace ! (
1143+ "Created link for {} -> {}" ,
1144+ target. user_display( ) ,
1145+ entry. path( ) . user_display( )
1146+ ) ,
1147+ Err ( err) if err. kind ( ) == std:: io:: ErrorKind :: AlreadyExists => { }
1148+ Err ( err) => return Err ( err. into ( ) ) ,
1149+ }
1150+ } else if entry. file_type ( ) ?. is_dir ( ) {
1151+ match create_symlink ( entry. path ( ) , & target) {
1152+ Ok ( ( ) ) => trace ! (
1153+ "Created link for {} -> {}" ,
1154+ target. user_display( ) ,
1155+ entry. path( ) . user_display( )
1156+ ) ,
1157+ Err ( err) if err. kind ( ) == std:: io:: ErrorKind :: AlreadyExists => { }
1158+ Err ( err) => return Err ( err. into ( ) ) ,
1159+ }
1160+ } else {
1161+ trace ! ( "Skipping link of entry: {}" , entry. path( ) . user_display( ) ) ;
1162+ }
11151163 }
11161164 }
11171165 }
@@ -1141,7 +1189,7 @@ hint: If you are running a script with `{}` in the shebang, you may need to incl
11411189 let ephemeral_env = ephemeral_env. map ( PythonEnvironment :: from) ;
11421190
11431191 if let Some ( e) = ephemeral_env. as_ref ( ) {
1144- println ! ( "Using ephemeral environment at: {}" , e. scripts( ) . display( ) ) ;
1192+ debug ! ( "Using ephemeral environment at: {}" , e. scripts( ) . display( ) ) ;
11451193 }
11461194
11471195 // Determine the Python interpreter to use for the command, if necessary.
@@ -1230,13 +1278,13 @@ hint: If you are running a script with `{}` in the shebang, you may need to incl
12301278 . as_ref ( )
12311279 . map ( PythonEnvironment :: scripts)
12321280 . into_iter ( )
1233- // .chain(
1234- // requirements_env
1235- // .as_ref()
1236- // .map(PythonEnvironment::scripts)
1237- // .into_iter(),
1238- // )
1239- // .chain(std::iter::once(base_interpreter.scripts()))
1281+ . chain (
1282+ requirements_env
1283+ . as_ref ( )
1284+ . map ( PythonEnvironment :: scripts)
1285+ . into_iter ( ) ,
1286+ )
1287+ . chain ( std:: iter:: once ( base_interpreter. scripts ( ) ) )
12401288 . chain (
12411289 // On Windows, non-virtual Python distributions put `python.exe` in the top-level
12421290 // directory, rather than in the `Scripts` subdirectory.
@@ -1254,7 +1302,6 @@ hint: If you are running a script with `{}` in the shebang, you may need to incl
12541302 . flat_map ( std:: env:: split_paths) ,
12551303 ) ,
12561304 ) ?;
1257- println ! ( "New PATH: {}" , new_path. display( ) ) ;
12581305 process. env ( EnvVars :: PATH , new_path) ;
12591306
12601307 // Increment recursion depth counter.
0 commit comments