From e0c3a0ab79bf0f62065f488fb154e000a604f7d6 Mon Sep 17 00:00:00 2001 From: Frost Ming Date: Mon, 26 Jun 2023 11:42:20 +0800 Subject: [PATCH] fix: PDM corrupts binary executable (ruff) Fixes #2045 Signed-off-by: Frost Ming --- news/2045.bugfix.md | 1 + src/pdm/environments/local.py | 36 +++++++++++++++++++++++++++++------ 2 files changed, 31 insertions(+), 6 deletions(-) create mode 100644 news/2045.bugfix.md diff --git a/news/2045.bugfix.md b/news/2045.bugfix.md new file mode 100644 index 0000000000..01eb005e68 --- /dev/null +++ b/news/2045.bugfix.md @@ -0,0 +1 @@ +Fix a bug that binary executables are corrupted when replacing shebangs. diff --git a/src/pdm/environments/local.py b/src/pdm/environments/local.py index 081191e53f..82f4d6d41d 100644 --- a/src/pdm/environments/local.py +++ b/src/pdm/environments/local.py @@ -4,15 +4,11 @@ import re import shlex from pathlib import Path -from typing import TYPE_CHECKING from pdm.compat import cached_property from pdm.environments.base import BaseEnvironment from pdm.utils import pdm_scheme -if TYPE_CHECKING: - pass - def _get_shebang_path(executable: str, is_launcher: bool) -> bytes: """Get the interpreter path in the shebang line @@ -27,6 +23,24 @@ def _get_shebang_path(executable: str, is_launcher: bool) -> bytes: return shlex.quote(executable).encode("utf-8") +def _is_console_script(content: bytes) -> bool: + import io + import zipfile + + if os.name == "nt": # Windows .exe should be a zip file. + try: + with zipfile.ZipFile(io.BytesIO(content)): + return True + except zipfile.BadZipFile: + return False + + try: + text = content.decode("utf-8") + return text.startswith("#!") + except UnicodeDecodeError: + return False + + def _replace_shebang(path: Path, new_executable: bytes) -> None: """Replace the python executable from the shebeng line, which can be in two forms: @@ -38,12 +52,22 @@ def _replace_shebang(path: Path, new_executable: bytes) -> None: _complex_shebang_re = rb"^'''exec' ('.+?') \"\$0\"" _simple_shebang_re = rb"^#!(.+?)\s*$" contents = path.read_bytes() - match = re.search(_complex_shebang_re, contents, flags=re.M) + + if not _is_console_script(contents): + return + + if os.name == "nt": + match = re.search(_simple_shebang_re, contents, flags=re.M) + if match: + path.write_bytes(contents.replace(match.group(1), new_executable, 1)) + return + + match = re.search(_complex_shebang_re, contents) if match: path.write_bytes(contents.replace(match.group(1), new_executable, 1)) return - match = re.search(_simple_shebang_re, contents, flags=re.M) + match = re.search(_simple_shebang_re, contents) if match: path.write_bytes(contents.replace(match.group(1), new_executable, 1))