mirror of https://github.com/astral-sh/uv
Drop trailing arguments when writing shebangs (#14519)
## Summary
You can see in pip that they read the full first line, then replace it
with the rewritten shebang, thereby dropping any trailing arguments on
the shebang:
65da0ff534/src/pip/_internal/operations/install/wheel.py (L94)
In contrast, we currently retain them, but write them _after_ the
shebang, which is wrong.
Closes https://github.com/astral-sh/uv/issues/14470.
This commit is contained in:
parent
4d061a6fc3
commit
57338e558c
|
|
@ -1,6 +1,6 @@
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::io;
|
use std::io;
|
||||||
use std::io::{BufReader, Read, Seek, Write};
|
use std::io::{BufReader, Read, Write};
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
|
|
||||||
use data_encoding::BASE64URL_NOPAD;
|
use data_encoding::BASE64URL_NOPAD;
|
||||||
|
|
@ -144,7 +144,7 @@ fn format_shebang(executable: impl AsRef<Path>, os_name: &str, relocatable: bool
|
||||||
///
|
///
|
||||||
/// <https://github.com/pypa/pip/blob/76e82a43f8fb04695e834810df64f2d9a2ff6020/src/pip/_vendor/distlib/scripts.py#L121-L126>
|
/// <https://github.com/pypa/pip/blob/76e82a43f8fb04695e834810df64f2d9a2ff6020/src/pip/_vendor/distlib/scripts.py#L121-L126>
|
||||||
fn get_script_executable(python_executable: &Path, is_gui: bool) -> PathBuf {
|
fn get_script_executable(python_executable: &Path, is_gui: bool) -> PathBuf {
|
||||||
// Only check for pythonw.exe on Windows
|
// Only check for `pythonw.exe` on Windows.
|
||||||
if cfg!(windows) && is_gui {
|
if cfg!(windows) && is_gui {
|
||||||
python_executable
|
python_executable
|
||||||
.file_name()
|
.file_name()
|
||||||
|
|
@ -431,22 +431,41 @@ fn install_script(
|
||||||
Err(err) => return Err(Error::Io(err)),
|
Err(err) => return Err(Error::Io(err)),
|
||||||
}
|
}
|
||||||
let size_and_encoded_hash = if start == placeholder_python {
|
let size_and_encoded_hash = if start == placeholder_python {
|
||||||
let is_gui = {
|
// Read the rest of the first line, one byte at a time, until we hit a newline.
|
||||||
let mut buf = vec![0; 1];
|
let mut is_gui = false;
|
||||||
script.read_exact(&mut buf)?;
|
let mut first = true;
|
||||||
if buf == b"w" {
|
let mut byte = [0u8; 1];
|
||||||
true
|
loop {
|
||||||
} else {
|
match script.read_exact(&mut byte) {
|
||||||
script.seek_relative(-1)?;
|
Ok(()) => {
|
||||||
false
|
if byte[0] == b'\n' || byte[0] == b'\r' {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if this is a GUI script (starts with 'w').
|
||||||
|
if first {
|
||||||
|
is_gui = byte[0] == b'w';
|
||||||
|
first = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(err) if err.kind() == io::ErrorKind::UnexpectedEof => break,
|
||||||
|
Err(err) => return Err(Error::Io(err)),
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
let executable = get_script_executable(&layout.sys_executable, is_gui);
|
let executable = get_script_executable(&layout.sys_executable, is_gui);
|
||||||
let executable = get_relocatable_executable(executable, layout, relocatable)?;
|
let executable = get_relocatable_executable(executable, layout, relocatable)?;
|
||||||
let start = format_shebang(&executable, &layout.os_name, relocatable)
|
let mut start = format_shebang(&executable, &layout.os_name, relocatable)
|
||||||
.as_bytes()
|
.as_bytes()
|
||||||
.to_vec();
|
.to_vec();
|
||||||
|
|
||||||
|
// Use appropriate line ending for the platform.
|
||||||
|
if layout.os_name == "nt" {
|
||||||
|
start.extend_from_slice(b"\r\n");
|
||||||
|
} else {
|
||||||
|
start.push(b'\n');
|
||||||
|
}
|
||||||
|
|
||||||
let mut target = uv_fs::tempfile_in(&layout.scheme.scripts)?;
|
let mut target = uv_fs::tempfile_in(&layout.scheme.scripts)?;
|
||||||
let size_and_encoded_hash = copy_and_hash(&mut start.chain(script), &mut target)?;
|
let size_and_encoded_hash = copy_and_hash(&mut start.chain(script), &mut target)?;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -11490,3 +11490,110 @@ fn conflicting_flags_clap_bug() {
|
||||||
"
|
"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Test that shebang arguments are stripped when installing scripts
|
||||||
|
#[test]
|
||||||
|
#[cfg(unix)]
|
||||||
|
fn strip_shebang_arguments() -> Result<()> {
|
||||||
|
let context = TestContext::new("3.12");
|
||||||
|
|
||||||
|
let project_dir = context.temp_dir.child("shebang_test");
|
||||||
|
project_dir.create_dir_all()?;
|
||||||
|
|
||||||
|
// Create a package with scripts that have shebang arguments.
|
||||||
|
let pyproject_toml = project_dir.child("pyproject.toml");
|
||||||
|
pyproject_toml.write_str(indoc! {r#"
|
||||||
|
[project]
|
||||||
|
name = "shebang-test"
|
||||||
|
version = "0.1.0"
|
||||||
|
|
||||||
|
[build-system]
|
||||||
|
requires = ["setuptools>=61.0"]
|
||||||
|
build-backend = "setuptools.build_meta"
|
||||||
|
|
||||||
|
[tool.setuptools]
|
||||||
|
packages = ["shebang_test"]
|
||||||
|
|
||||||
|
[tool.setuptools.data-files]
|
||||||
|
"scripts" = ["scripts/custom_script", "scripts/custom_gui_script"]
|
||||||
|
"#})?;
|
||||||
|
|
||||||
|
// Create the package directory.
|
||||||
|
let package_dir = project_dir.child("shebang_test");
|
||||||
|
package_dir.create_dir_all()?;
|
||||||
|
|
||||||
|
// Create an `__init__.py` file in the package directory.
|
||||||
|
let init_file = package_dir.child("__init__.py");
|
||||||
|
init_file.touch()?;
|
||||||
|
|
||||||
|
// Create scripts directory with scripts that have shebangs with arguments
|
||||||
|
let scripts_dir = project_dir.child("scripts");
|
||||||
|
scripts_dir.create_dir_all()?;
|
||||||
|
|
||||||
|
let script_with_args = scripts_dir.child("custom_script");
|
||||||
|
script_with_args.write_str(indoc! {r#"
|
||||||
|
#!python -E -s
|
||||||
|
# This is a test script with shebang arguments
|
||||||
|
import sys
|
||||||
|
print(f"Hello from {sys.executable}")
|
||||||
|
print(f"Arguments: {sys.argv}")
|
||||||
|
"#})?;
|
||||||
|
|
||||||
|
let gui_script_with_args = scripts_dir.child("custom_gui_script");
|
||||||
|
gui_script_with_args.write_str(indoc! {r#"
|
||||||
|
#!pythonw -E
|
||||||
|
# This is a test GUI script with shebang arguments
|
||||||
|
import sys
|
||||||
|
print(f"Hello from GUI script: {sys.executable}")
|
||||||
|
"#})?;
|
||||||
|
|
||||||
|
// Create a `setup.py` that explicitly handles scripts.
|
||||||
|
let setup_py = project_dir.child("setup.py");
|
||||||
|
setup_py.write_str(indoc! {r"
|
||||||
|
from setuptools import setup
|
||||||
|
setup(scripts=['scripts/custom_script', 'scripts/custom_gui_script'])
|
||||||
|
"})?;
|
||||||
|
|
||||||
|
// Install the package.
|
||||||
|
uv_snapshot!(context.filters(), context.pip_install().arg(project_dir.path()), @r###"
|
||||||
|
success: true
|
||||||
|
exit_code: 0
|
||||||
|
----- stdout -----
|
||||||
|
|
||||||
|
----- stderr -----
|
||||||
|
Resolved 1 package in [TIME]
|
||||||
|
Prepared 1 package in [TIME]
|
||||||
|
Installed 1 package in [TIME]
|
||||||
|
+ shebang-test==0.1.0 (from file://[TEMP_DIR]/shebang_test)
|
||||||
|
"###);
|
||||||
|
|
||||||
|
// Check the installed scripts have their shebangs stripped of arguments.
|
||||||
|
let custom_script_path = venv_bin_path(&context.venv).join("custom_script");
|
||||||
|
let script_content = fs::read_to_string(&custom_script_path)?;
|
||||||
|
|
||||||
|
insta::with_settings!({filters => context.filters()
|
||||||
|
}, {
|
||||||
|
insta::assert_snapshot!(script_content, @r#"
|
||||||
|
#![VENV]/bin/python3
|
||||||
|
# This is a test script with shebang arguments
|
||||||
|
import sys
|
||||||
|
print(f"Hello from {sys.executable}")
|
||||||
|
print(f"Arguments: {sys.argv}")
|
||||||
|
"#);
|
||||||
|
});
|
||||||
|
|
||||||
|
let custom_gui_script_path = venv_bin_path(&context.venv).join("custom_gui_script");
|
||||||
|
let gui_script_content = fs::read_to_string(&custom_gui_script_path)?;
|
||||||
|
|
||||||
|
insta::with_settings!({filters => context.filters()
|
||||||
|
}, {
|
||||||
|
insta::assert_snapshot!(gui_script_content, @r#"
|
||||||
|
#![VENV]/bin/python3
|
||||||
|
# This is a test GUI script with shebang arguments
|
||||||
|
import sys
|
||||||
|
print(f"Hello from GUI script: {sys.executable}")
|
||||||
|
"#);
|
||||||
|
});
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue