Allow symlinks to files in scripts directory (#5380)

## Summary

Closes https://github.com/astral-sh/uv/issues/5359.

## Test Plan

Unfortunately, the only packages I know of that use this are Ruff and
uv, and both are too heavy to install in a recurring test, so:

`uv tool install hatch==1.12.0 --with uv==0.2.27 --force
--link-mode=symlink`
This commit is contained in:
Charlie Marsh 2024-07-23 18:24:43 -04:00 committed by GitHub
parent 8942ec36c0
commit ad4a15d0c2
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 82 additions and 11 deletions

View file

@ -441,13 +441,31 @@ fn install_script(
record: &mut [RecordEntry],
file: &DirEntry,
) -> Result<(), Error> {
if !file.file_type()?.is_file() {
let file_type = file.file_type()?;
if file_type.is_dir() {
return Err(Error::InvalidWheel(format!(
"Wheel contains entry in scripts directory that is not a file: {}",
file.path().display()
"Wheel contains an invalid entry (directory) in the `scripts` directory: {}",
file.path().simplified_display()
)));
}
if file_type.is_symlink() {
let Ok(target) = file.path().canonicalize() else {
return Err(Error::InvalidWheel(format!(
"Wheel contains an invalid entry (broken symlink) in the `scripts` directory: {}",
file.path().simplified_display(),
)));
};
if target.is_dir() {
return Err(Error::InvalidWheel(format!(
"Wheel contains an invalid entry (directory symlink) in the `scripts` directory: {} ({})",
file.path().simplified_display(),
target.simplified_display()
)));
}
}
let script_absolute = layout.scheme.scripts.join(file.file_name());
let script_relative =
pathdiff::diff_paths(&script_absolute, site_packages).ok_or_else(|| {

View file

@ -105,12 +105,16 @@ fn install() -> Result<()> {
.join("__init__.cpython-312.pyc")
.exists());
context.assert_command("import markupsafe").success();
context
.assert_command("from markupsafe import Markup")
.success();
// Removing the cache shouldn't invalidate the virtual environment.
fs::remove_dir_all(context.cache_dir.path())?;
context.assert_command("import markupsafe").success();
context
.assert_command("from markupsafe import Markup")
.success();
Ok(())
}
@ -140,12 +144,16 @@ fn install_copy() -> Result<()> {
"###
);
context.assert_command("import markupsafe").success();
context
.assert_command("from markupsafe import Markup")
.success();
// Removing the cache shouldn't invalidate the virtual environment.
fs::remove_dir_all(context.cache_dir.path())?;
context.assert_command("import markupsafe").success();
context
.assert_command("from markupsafe import Markup")
.success();
Ok(())
}
@ -175,12 +183,55 @@ fn install_hardlink() -> Result<()> {
"###
);
context.assert_command("import markupsafe").success();
context
.assert_command("from markupsafe import Markup")
.success();
// Removing the cache shouldn't invalidate the virtual environment.
fs::remove_dir_all(context.cache_dir.path())?;
context.assert_command("import markupsafe").success();
context
.assert_command("from markupsafe import Markup")
.success();
Ok(())
}
/// Install a package into a virtual environment using symlink semantics.
#[test]
fn install_symlink() -> Result<()> {
let context = TestContext::new("3.12");
let requirements_txt = context.temp_dir.child("requirements.txt");
requirements_txt.write_str("MarkupSafe==2.1.3")?;
uv_snapshot!(context.pip_sync()
.arg("requirements.txt")
.arg("--link-mode")
.arg("symlink")
.arg("--strict"), @r###"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
Resolved 1 package in [TIME]
Prepared 1 package in [TIME]
Installed 1 package in [TIME]
+ markupsafe==2.1.3
"###
);
context
.assert_command("from markupsafe import Markup")
.success();
// Removing the cache _should_ invalidate the virtual environment.
fs::remove_dir_all(context.cache_dir.path())?;
context
.assert_command("from markupsafe import Markup")
.failure();
Ok(())
}
@ -210,7 +261,7 @@ fn install_many() -> Result<()> {
);
context
.assert_command("import markupsafe; import tomli")
.assert_command("from markupsafe import Markup; import tomli")
.success();
Ok(())
@ -244,7 +295,9 @@ fn noop() -> Result<()> {
"###
);
context.assert_command("import markupsafe").success();
context
.assert_command("from markupsafe import Markup")
.success();
Ok(())
}