Skip to content

Commit 52dec14

Browse files
committed
Copy etc and share into ephemeral environments
1 parent 66f483f commit 52dec14

File tree

3 files changed

+121
-40
lines changed

3 files changed

+121
-40
lines changed

crates/uv-fs/src/lib.rs

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,36 @@ pub fn replace_symlink(src: impl AsRef<Path>, dst: impl AsRef<Path>) -> std::io:
138138
}
139139
}
140140

141+
/// Create a symlink at `dst` pointing to `src`.
142+
///
143+
/// On Windows, this uses the `junction` crate to create a junction point.
144+
///
145+
/// Note that because junctions are used, the source must be a directory.
146+
#[cfg(windows)]
147+
pub fn create_symlink(src: impl AsRef<Path>, dst: impl AsRef<Path>) -> std::io::Result<()> {
148+
// If the source is a file, we can't create a junction
149+
if src.as_ref().is_file() {
150+
return Err(std::io::Error::new(
151+
std::io::ErrorKind::InvalidInput,
152+
format!(
153+
"Cannot create a junction for {}: is not a directory",
154+
src.as_ref().display()
155+
),
156+
));
157+
}
158+
159+
junction::create(
160+
dunce::simplified(src.as_ref()),
161+
dunce::simplified(dst.as_ref()),
162+
)
163+
}
164+
165+
/// Create a symlink at `dst` pointing to `src`.
166+
#[cfg(unix)]
167+
pub fn create_symlink(src: impl AsRef<Path>, dst: impl AsRef<Path>) -> std::io::Result<()> {
168+
fs_err::os::unix::fs::symlink(src.as_ref(), dst.as_ref())
169+
}
170+
141171
#[cfg(unix)]
142172
pub fn remove_symlink(path: impl AsRef<Path>) -> std::io::Result<()> {
143173
fs_err::remove_file(path.as_ref())

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,10 @@ impl EphemeralEnvironment {
8888
pub(crate) fn sys_executable(&self) -> &Path {
8989
self.0.interpreter().sys_executable()
9090
}
91+
92+
pub(crate) fn sys_prefix(&self) -> &Path {
93+
self.0.interpreter().sys_prefix()
94+
}
9195
}
9296

9397
/// A [`PythonEnvironment`] stored in the cache.

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

Lines changed: 87 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ use futures::StreamExt;
1010
use itertools::Itertools;
1111
use owo_colors::OwoColorize;
1212
use tokio::process::Command;
13-
use tracing::{debug, warn};
13+
use tracing::{debug, trace, warn};
1414
use url::Url;
1515

1616
use uv_cache::Cache;
@@ -22,7 +22,7 @@ use uv_configuration::{
2222
};
2323
use uv_distribution_types::Requirement;
2424
use uv_fs::which::is_executable;
25-
use uv_fs::{PythonExt, Simplified};
25+
use uv_fs::{PythonExt, Simplified, create_symlink, symlink_or_copy_file};
2626
use uv_installer::{SatisfiesResult, SitePackages};
2727
use uv_normalize::{DefaultExtras, DefaultGroups, PackageName};
2828
use 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

Comments
 (0)