diff --git a/src/installer/scripts.py b/src/installer/scripts.py index 7929817..1a0129e 100644 --- a/src/installer/scripts.py +++ b/src/installer/scripts.py @@ -43,6 +43,41 @@ """ +def _is_executable_simple(executable: bytes) -> bool: + if b" " in executable: + return False + shebang_length = len(executable) + 3 # Prefix #! and newline after. + # According to distlib, Darwin can handle up to 512 characters. But I want + # to avoid platform sniffing to make this as platform-agnostic as possible. + # The "complex" script isn't that bad anyway. + return shebang_length <= 127 + + +def _build_shebang(executable: str, forlauncher: bool) -> bytes: + """Build a shebang line. + + The non-launcher cases are taken directly from distlib's implementation, + which tries its best to account for command length, spaces in path, etc. + + https://bitbucket.org/pypa/distlib/src/58cd5c6/distlib/scripts.py#lines-124 + """ + executable_bytes = executable.encode("utf-8") + if forlauncher: # The launcher can just use the command as-is. + return b"#!" + executable_bytes + if _is_executable_simple(executable_bytes): + return b"#!" + executable_bytes + + # Shebang support for an executable with a space in it is under-specified + # and platform-dependent, so we use a clever hack to generate a script to + # run in ``/bin/sh`` that should work on all reasonably modern platforms. + # Read the following message to understand how the hack works: + # https://github.com/pypa/installer/pull/4#issuecomment-623668717 + + quoted = shlex.quote(executable).encode("utf-8") + # I don't understand a lick what this is trying to do. + return b"#!/bin/sh\n'''exec' " + quoted + b' "$0" "$@"\n' + b"' '''" + + class InvalidScript(ValueError): """Raised if the user provides incorrect script section or kind.""" @@ -102,7 +137,7 @@ def generate(self, executable: str, kind: "LauncherKind") -> tuple[str, bytes]: """ launcher = self._get_launcher_data(kind) executable = self._get_alternate_executable(executable, kind) - shebang = self._build_shebang(executable, forlauncher=bool(launcher)) + shebang = _build_shebang(executable, forlauncher=bool(launcher)) code = _SCRIPT_TEMPLATE.format( module=self.module, import_name=self.attr.split(".")[0], @@ -118,37 +153,3 @@ def generate(self, executable: str, kind: "LauncherKind") -> tuple[str, bytes]: name = f"{self.name}.exe" data = launcher + shebang + b"\n" + stream.getvalue() return name, data - - @staticmethod - def _is_executable_simple(executable: bytes) -> bool: - if b" " in executable: - return False - shebang_length = len(executable) + 3 # Prefix #! and newline after. - # According to distlib, Darwin can handle up to 512 characters. But I want - # to avoid platform sniffing to make this as platform-agnostic as possible. - # The "complex" script isn't that bad anyway. - return shebang_length <= 127 - - def _build_shebang(self, executable: str, forlauncher: bool) -> bytes: - """Build a shebang line. - - The non-launcher cases are taken directly from distlib's implementation, - which tries its best to account for command length, spaces in path, etc. - - https://bitbucket.org/pypa/distlib/src/58cd5c6/distlib/scripts.py#lines-124 - """ - executable_bytes = executable.encode("utf-8") - if forlauncher: # The launcher can just use the command as-is. - return b"#!" + executable_bytes - if self._is_executable_simple(executable_bytes): - return b"#!" + executable_bytes - - # Shebang support for an executable with a space in it is under-specified - # and platform-dependent, so we use a clever hack to generate a script to - # run in ``/bin/sh`` that should work on all reasonably modern platforms. - # Read the following message to understand how the hack works: - # https://github.com/pypa/installer/pull/4#issuecomment-623668717 - - quoted = shlex.quote(executable).encode("utf-8") - # I don't understand a lick what this is trying to do. - return b"#!/bin/sh\n'''exec' " + quoted + b' "$0" "$@"\n' + b"' '''" diff --git a/src/installer/utils.py b/src/installer/utils.py index 6a22631..46ee706 100644 --- a/src/installer/utils.py +++ b/src/installer/utils.py @@ -24,6 +24,8 @@ cast, ) +from installer.scripts import _build_shebang + if TYPE_CHECKING: from installer.records import RecordEntry from installer.scripts import LauncherKind, ScriptSection @@ -186,7 +188,7 @@ def fix_shebang(stream: BinaryIO, interpreter: str) -> Iterator[BinaryIO]: if stream.read(8) == b"#!python": new_stream = io.BytesIO() # write our new shebang - new_stream.write(f"#!{interpreter}\n".encode()) + new_stream.write(_build_shebang(interpreter, False) + b"\n") # copy the rest of the stream stream.seek(0) stream.readline() # skip first line