Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
89 changes: 39 additions & 50 deletions src/installer/scripts.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,20 +3,10 @@
import io
import os
import shlex
import sys
import zipfile
from dataclasses import dataclass, field
from types import ModuleType
from typing import TYPE_CHECKING, Mapping, Optional, Tuple, Union

if sys.version_info >= (3, 9): # pragma: no cover
from importlib.resources import files

def read_binary(package: Union[str, ModuleType], file_path: str) -> bytes:
return (files(package) / file_path).read_bytes()

else: # pragma: no cover
from importlib.resources import read_binary
from importlib.resources import read_binary
from typing import TYPE_CHECKING, Mapping, Optional, Tuple

from installer import _scripts

Expand Down Expand Up @@ -52,41 +42,6 @@ def read_binary(package: Union[str, ModuleType], file_path: str) -> bytes:
"""


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."""

Expand Down Expand Up @@ -146,19 +101,53 @@ def generate(self, executable: str, kind: "LauncherKind") -> Tuple[str, bytes]:
"""
launcher = self._get_launcher_data(kind)
executable = self._get_alternate_executable(executable, kind)
shebang = _build_shebang(executable, forlauncher=bool(launcher))
shebang = self._build_shebang(executable, forlauncher=bool(launcher))
code = _SCRIPT_TEMPLATE.format(
module=self.module,
import_name=self.attr.split(".")[0],
func_path=self.attr,
).encode("utf-8")

if launcher is None:
return (self.name, shebang + b"\n" + code)
return self.name, shebang + b"\n" + code

stream = io.BytesIO()
with zipfile.ZipFile(stream, "w") as zf:
zf.writestr("__main__.py", code)
name = f"{self.name}.exe"
data = launcher + shebang + b"\n" + stream.getvalue()
return (name, data)
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"' '''"