Skip to content

Commit 29d68b2

Browse files
committed
Handle non-relocatable environments
1 parent ba35d83 commit 29d68b2

File tree

2 files changed

+114
-3
lines changed

2 files changed

+114
-3
lines changed

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

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1085,6 +1085,7 @@ hint: If you are running a script with `{}` in the shebang, you may need to incl
10851085
copy_entrypoint(
10861086
&entry.path(),
10871087
&ephemeral_env.scripts().join(entry.file_name()),
1088+
&interpreter.sys_executable(),
10881089
ephemeral_env.sys_executable(),
10891090
)?;
10901091
}
@@ -1739,7 +1740,12 @@ fn read_recursion_depth_from_environment_variable() -> anyhow::Result<u32> {
17391740
}
17401741

17411742
#[cfg(unix)]
1742-
fn copy_entrypoint(source: &Path, target: &Path, python_executable: &Path) -> anyhow::Result<()> {
1743+
fn copy_entrypoint(
1744+
source: &Path,
1745+
target: &Path,
1746+
previous_executable: &Path,
1747+
python_executable: &Path,
1748+
) -> anyhow::Result<()> {
17431749
use std::os::unix::fs::PermissionsExt;
17441750

17451751
let contents = fs_err::read_to_string(source)?;
@@ -1749,7 +1755,14 @@ fn copy_entrypoint(source: &Path, target: &Path, python_executable: &Path) -> an
17491755
"#;
17501756

17511757
// Only rewrite entrypoints that use the expected shebang.
1752-
let Some(contents) = contents.strip_prefix(expected) else {
1758+
let Some(contents) = contents
1759+
.strip_prefix(expected)
1760+
.or_else(|| contents.strip_prefix(&format!("#!{}", previous_executable.display())))
1761+
else {
1762+
debug!(
1763+
"Skipping copy of entrypoint at {}: does not start with expected shebang",
1764+
source.user_display()
1765+
);
17531766
return Ok(());
17541767
};
17551768

@@ -1770,7 +1783,12 @@ fn copy_entrypoint(source: &Path, target: &Path, python_executable: &Path) -> an
17701783
}
17711784

17721785
#[cfg(windows)]
1773-
fn copy_entrypoint(source: &Path, target: &Path, python_executable: &Path) -> anyhow::Result<()> {
1786+
fn copy_entrypoint(
1787+
source: &Path,
1788+
target: &Path,
1789+
_previous_executable: &Path,
1790+
python_executable: &Path,
1791+
) -> anyhow::Result<()> {
17741792
use uv_trampoline_builder::Launcher;
17751793

17761794
let Some(launcher) = Launcher::try_from_path(source)? else {

crates/uv/tests/it/run.rs

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1319,6 +1319,99 @@ fn run_with_pyvenv_cfg_file() -> Result<()> {
13191319
Ok(())
13201320
}
13211321

1322+
#[test]
1323+
fn run_with_overlay_interpreter() -> Result<()> {
1324+
let context = TestContext::new("3.12");
1325+
1326+
let pyproject_toml = context.temp_dir.child("pyproject.toml");
1327+
pyproject_toml.write_str(indoc! { r#"
1328+
[project]
1329+
name = "foo"
1330+
version = "1.0.0"
1331+
requires-python = ">=3.8"
1332+
dependencies = []
1333+
1334+
[build-system]
1335+
requires = ["setuptools>=42"]
1336+
build-backend = "setuptools.build_meta"
1337+
1338+
[project.scripts]
1339+
main = "foo:main"
1340+
"#
1341+
})?;
1342+
1343+
let foo = context.temp_dir.child("src").child("foo");
1344+
foo.create_dir_all()?;
1345+
let init_py = foo.child("__init__.py");
1346+
init_py.write_str(indoc! { r#"
1347+
def main():
1348+
import sys
1349+
print(sys.executable)
1350+
"#
1351+
})?;
1352+
1353+
// The project's entrypoint should be rewritten to use the overlay interpreter.
1354+
uv_snapshot!(context.filters(), context.run().arg("--with").arg("iniconfig").arg("main"), @r"
1355+
success: true
1356+
exit_code: 0
1357+
----- stdout -----
1358+
[CACHE_DIR]/builds-v0/[TMP]/python
1359+
1360+
----- stderr -----
1361+
Resolved 1 package in [TIME]
1362+
Prepared 1 package in [TIME]
1363+
Installed 1 package in [TIME]
1364+
+ foo==1.0.0 (from file://[TEMP_DIR]/)
1365+
Resolved 1 package in [TIME]
1366+
Prepared 1 package in [TIME]
1367+
Installed 1 package in [TIME]
1368+
+ iniconfig==2.0.0
1369+
");
1370+
1371+
// When layering the project on top (via `--with`), the overlay interpreter also should be used.
1372+
uv_snapshot!(context.filters(), context.run().arg("--no-project").arg("--with").arg(".").arg("main"), @r"
1373+
success: true
1374+
exit_code: 0
1375+
----- stdout -----
1376+
[CACHE_DIR]/builds-v0/[TMP]/python
1377+
1378+
----- stderr -----
1379+
Resolved 1 package in [TIME]
1380+
Prepared 1 package in [TIME]
1381+
Installed 1 package in [TIME]
1382+
+ foo==1.0.0 (from file://[TEMP_DIR]/)
1383+
");
1384+
1385+
// Switch to a relocatable virtual environment.
1386+
context.venv().arg("--relocatable").assert().success();
1387+
1388+
// The project's entrypoint should be rewritten to use the overlay interpreter.
1389+
uv_snapshot!(context.filters(), context.run().arg("--with").arg("iniconfig").arg("main"), @r"
1390+
success: true
1391+
exit_code: 0
1392+
----- stdout -----
1393+
[CACHE_DIR]/builds-v0/[TMP]/python
1394+
1395+
----- stderr -----
1396+
Resolved 1 package in [TIME]
1397+
Audited 1 package in [TIME]
1398+
Resolved 1 package in [TIME]
1399+
");
1400+
1401+
// When layering the project on top (via `--with`), the overlay interpreter also should be used.
1402+
uv_snapshot!(context.filters(), context.run().arg("--no-project").arg("--with").arg(".").arg("main"), @r"
1403+
success: true
1404+
exit_code: 0
1405+
----- stdout -----
1406+
[CACHE_DIR]/builds-v0/[TMP]/python
1407+
1408+
----- stderr -----
1409+
Resolved 1 package in [TIME]
1410+
");
1411+
1412+
Ok(())
1413+
}
1414+
13221415
#[test]
13231416
fn run_with_build_constraints() -> Result<()> {
13241417
let context = TestContext::new("3.9");

0 commit comments

Comments
 (0)