You cannot select more than 25 topics
			Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
		
		
		
		
		
			
		
			
				
	
	
		
			362 lines
		
	
	
		
			11 KiB
		
	
	
	
		
			Python
		
	
			
		
		
	
	
			362 lines
		
	
	
		
			11 KiB
		
	
	
	
		
			Python
		
	
from __future__ import annotations
 | 
						|
 | 
						|
import os
 | 
						|
import re
 | 
						|
import shlex
 | 
						|
import shutil
 | 
						|
import struct
 | 
						|
import subprocess
 | 
						|
import sys
 | 
						|
import textwrap
 | 
						|
from collections.abc import Iterable
 | 
						|
from typing import TYPE_CHECKING, TypedDict
 | 
						|
 | 
						|
from ._importlib import metadata, resources
 | 
						|
 | 
						|
if TYPE_CHECKING:
 | 
						|
    from typing_extensions import Self
 | 
						|
 | 
						|
from .warnings import SetuptoolsWarning
 | 
						|
 | 
						|
from distutils.command.build_scripts import first_line_re
 | 
						|
from distutils.util import get_platform
 | 
						|
 | 
						|
 | 
						|
class _SplitArgs(TypedDict, total=False):
 | 
						|
    comments: bool
 | 
						|
    posix: bool
 | 
						|
 | 
						|
 | 
						|
class CommandSpec(list):
 | 
						|
    """
 | 
						|
    A command spec for a #! header, specified as a list of arguments akin to
 | 
						|
    those passed to Popen.
 | 
						|
    """
 | 
						|
 | 
						|
    options: list[str] = []
 | 
						|
    split_args = _SplitArgs()
 | 
						|
 | 
						|
    @classmethod
 | 
						|
    def best(cls):
 | 
						|
        """
 | 
						|
        Choose the best CommandSpec class based on environmental conditions.
 | 
						|
        """
 | 
						|
        return cls
 | 
						|
 | 
						|
    @classmethod
 | 
						|
    def _sys_executable(cls):
 | 
						|
        _default = os.path.normpath(sys.executable)
 | 
						|
        return os.environ.get('__PYVENV_LAUNCHER__', _default)
 | 
						|
 | 
						|
    @classmethod
 | 
						|
    def from_param(cls, param: Self | str | Iterable[str] | None) -> Self:
 | 
						|
        """
 | 
						|
        Construct a CommandSpec from a parameter to build_scripts, which may
 | 
						|
        be None.
 | 
						|
        """
 | 
						|
        if isinstance(param, cls):
 | 
						|
            return param
 | 
						|
        if isinstance(param, str):
 | 
						|
            return cls.from_string(param)
 | 
						|
        if isinstance(param, Iterable):
 | 
						|
            return cls(param)
 | 
						|
        if param is None:
 | 
						|
            return cls.from_environment()
 | 
						|
        raise TypeError(f"Argument has an unsupported type {type(param)}")
 | 
						|
 | 
						|
    @classmethod
 | 
						|
    def from_environment(cls):
 | 
						|
        return cls([cls._sys_executable()])
 | 
						|
 | 
						|
    @classmethod
 | 
						|
    def from_string(cls, string: str) -> Self:
 | 
						|
        """
 | 
						|
        Construct a command spec from a simple string representing a command
 | 
						|
        line parseable by shlex.split.
 | 
						|
        """
 | 
						|
        items = shlex.split(string, **cls.split_args)
 | 
						|
        return cls(items)
 | 
						|
 | 
						|
    def install_options(self, script_text: str):
 | 
						|
        self.options = shlex.split(self._extract_options(script_text))
 | 
						|
        cmdline = subprocess.list2cmdline(self)
 | 
						|
        if not isascii(cmdline):
 | 
						|
            self.options[:0] = ['-x']
 | 
						|
 | 
						|
    @staticmethod
 | 
						|
    def _extract_options(orig_script):
 | 
						|
        """
 | 
						|
        Extract any options from the first line of the script.
 | 
						|
        """
 | 
						|
        first = (orig_script + '\n').splitlines()[0]
 | 
						|
        match = _first_line_re().match(first)
 | 
						|
        options = match.group(1) or '' if match else ''
 | 
						|
        return options.strip()
 | 
						|
 | 
						|
    def as_header(self):
 | 
						|
        return self._render(self + list(self.options))
 | 
						|
 | 
						|
    @staticmethod
 | 
						|
    def _strip_quotes(item):
 | 
						|
        _QUOTES = '"\''
 | 
						|
        for q in _QUOTES:
 | 
						|
            if item.startswith(q) and item.endswith(q):
 | 
						|
                return item[1:-1]
 | 
						|
        return item
 | 
						|
 | 
						|
    @staticmethod
 | 
						|
    def _render(items):
 | 
						|
        cmdline = subprocess.list2cmdline(
 | 
						|
            CommandSpec._strip_quotes(item.strip()) for item in items
 | 
						|
        )
 | 
						|
        return '#!' + cmdline + '\n'
 | 
						|
 | 
						|
 | 
						|
class WindowsCommandSpec(CommandSpec):
 | 
						|
    split_args = _SplitArgs(posix=False)
 | 
						|
 | 
						|
 | 
						|
class ScriptWriter:
 | 
						|
    """
 | 
						|
    Encapsulates behavior around writing entry point scripts for console and
 | 
						|
    gui apps.
 | 
						|
    """
 | 
						|
 | 
						|
    template = textwrap.dedent(
 | 
						|
        r"""
 | 
						|
        # EASY-INSTALL-ENTRY-SCRIPT: %(spec)r,%(group)r,%(name)r
 | 
						|
        import re
 | 
						|
        import sys
 | 
						|
 | 
						|
        # for compatibility with easy_install; see #2198
 | 
						|
        __requires__ = %(spec)r
 | 
						|
 | 
						|
        try:
 | 
						|
            from importlib.metadata import distribution
 | 
						|
        except ImportError:
 | 
						|
            try:
 | 
						|
                from importlib_metadata import distribution
 | 
						|
            except ImportError:
 | 
						|
                from pkg_resources import load_entry_point
 | 
						|
 | 
						|
 | 
						|
        def importlib_load_entry_point(spec, group, name):
 | 
						|
            dist_name, _, _ = spec.partition('==')
 | 
						|
            matches = (
 | 
						|
                entry_point
 | 
						|
                for entry_point in distribution(dist_name).entry_points
 | 
						|
                if entry_point.group == group and entry_point.name == name
 | 
						|
            )
 | 
						|
            return next(matches).load()
 | 
						|
 | 
						|
 | 
						|
        globals().setdefault('load_entry_point', importlib_load_entry_point)
 | 
						|
 | 
						|
 | 
						|
        if __name__ == '__main__':
 | 
						|
            sys.argv[0] = re.sub(r'(-script\.pyw?|\.exe)?$', '', sys.argv[0])
 | 
						|
            sys.exit(load_entry_point(%(spec)r, %(group)r, %(name)r)())
 | 
						|
        """
 | 
						|
    ).lstrip()
 | 
						|
 | 
						|
    command_spec_class = CommandSpec
 | 
						|
 | 
						|
    @classmethod
 | 
						|
    def get_args(cls, dist, header=None):
 | 
						|
        """
 | 
						|
        Yield write_script() argument tuples for a distribution's
 | 
						|
        console_scripts and gui_scripts entry points.
 | 
						|
        """
 | 
						|
 | 
						|
        # If distribution is not an importlib.metadata.Distribution, assume
 | 
						|
        # it's a pkg_resources.Distribution and transform it.
 | 
						|
        if not hasattr(dist, 'entry_points'):
 | 
						|
            SetuptoolsWarning.emit("Unsupported distribution encountered.")
 | 
						|
            dist = metadata.Distribution.at(dist.egg_info)
 | 
						|
 | 
						|
        if header is None:
 | 
						|
            header = cls.get_header()
 | 
						|
        spec = f'{dist.name}=={dist.version}'
 | 
						|
        for type_ in 'console', 'gui':
 | 
						|
            group = f'{type_}_scripts'
 | 
						|
            for ep in dist.entry_points.select(group=group):
 | 
						|
                name = ep.name
 | 
						|
                cls._ensure_safe_name(ep.name)
 | 
						|
                script_text = cls.template % locals()
 | 
						|
                args = cls._get_script_args(type_, ep.name, header, script_text)
 | 
						|
                yield from args
 | 
						|
 | 
						|
    @staticmethod
 | 
						|
    def _ensure_safe_name(name):
 | 
						|
        """
 | 
						|
        Prevent paths in *_scripts entry point names.
 | 
						|
        """
 | 
						|
        has_path_sep = re.search(r'[\\/]', name)
 | 
						|
        if has_path_sep:
 | 
						|
            raise ValueError("Path separators not allowed in script names")
 | 
						|
 | 
						|
    @classmethod
 | 
						|
    def best(cls):
 | 
						|
        """
 | 
						|
        Select the best ScriptWriter for this environment.
 | 
						|
        """
 | 
						|
        if sys.platform == 'win32' or (os.name == 'java' and os._name == 'nt'):
 | 
						|
            return WindowsScriptWriter.best()
 | 
						|
        else:
 | 
						|
            return cls
 | 
						|
 | 
						|
    @classmethod
 | 
						|
    def _get_script_args(cls, type_, name, header, script_text):
 | 
						|
        # Simply write the stub with no extension.
 | 
						|
        yield (name, header + script_text)
 | 
						|
 | 
						|
    @classmethod
 | 
						|
    def get_header(
 | 
						|
        cls,
 | 
						|
        script_text: str = "",
 | 
						|
        executable: str | CommandSpec | Iterable[str] | None = None,
 | 
						|
    ) -> str:
 | 
						|
        """Create a #! line, getting options (if any) from script_text"""
 | 
						|
        cmd = cls.command_spec_class.best().from_param(executable)
 | 
						|
        cmd.install_options(script_text)
 | 
						|
        return cmd.as_header()
 | 
						|
 | 
						|
 | 
						|
class WindowsScriptWriter(ScriptWriter):
 | 
						|
    command_spec_class = WindowsCommandSpec
 | 
						|
 | 
						|
    @classmethod
 | 
						|
    def best(cls):
 | 
						|
        """
 | 
						|
        Select the best ScriptWriter suitable for Windows
 | 
						|
        """
 | 
						|
        writer_lookup = dict(
 | 
						|
            executable=WindowsExecutableLauncherWriter,
 | 
						|
            natural=cls,
 | 
						|
        )
 | 
						|
        # for compatibility, use the executable launcher by default
 | 
						|
        launcher = os.environ.get('SETUPTOOLS_LAUNCHER', 'executable')
 | 
						|
        return writer_lookup[launcher]
 | 
						|
 | 
						|
    @classmethod
 | 
						|
    def _get_script_args(cls, type_, name, header, script_text):
 | 
						|
        "For Windows, add a .py extension"
 | 
						|
        ext = dict(console='.pya', gui='.pyw')[type_]
 | 
						|
        if ext not in os.environ['PATHEXT'].lower().split(';'):
 | 
						|
            msg = (
 | 
						|
                "{ext} not listed in PATHEXT; scripts will not be "
 | 
						|
                "recognized as executables."
 | 
						|
            ).format(**locals())
 | 
						|
            SetuptoolsWarning.emit(msg)
 | 
						|
        old = ['.pya', '.py', '-script.py', '.pyc', '.pyo', '.pyw', '.exe']
 | 
						|
        old.remove(ext)
 | 
						|
        header = cls._adjust_header(type_, header)
 | 
						|
        blockers = [name + x for x in old]
 | 
						|
        yield name + ext, header + script_text, 't', blockers
 | 
						|
 | 
						|
    @classmethod
 | 
						|
    def _adjust_header(cls, type_, orig_header):
 | 
						|
        """
 | 
						|
        Make sure 'pythonw' is used for gui and 'python' is used for
 | 
						|
        console (regardless of what sys.executable is).
 | 
						|
        """
 | 
						|
        pattern = 'pythonw.exe'
 | 
						|
        repl = 'python.exe'
 | 
						|
        if type_ == 'gui':
 | 
						|
            pattern, repl = repl, pattern
 | 
						|
        pattern_ob = re.compile(re.escape(pattern), re.IGNORECASE)
 | 
						|
        new_header = pattern_ob.sub(string=orig_header, repl=repl)
 | 
						|
        return new_header if cls._use_header(new_header) else orig_header
 | 
						|
 | 
						|
    @staticmethod
 | 
						|
    def _use_header(new_header):
 | 
						|
        """
 | 
						|
        Should _adjust_header use the replaced header?
 | 
						|
 | 
						|
        On non-windows systems, always use. On
 | 
						|
        Windows systems, only use the replaced header if it resolves
 | 
						|
        to an executable on the system.
 | 
						|
        """
 | 
						|
        clean_header = new_header[2:-1].strip('"')
 | 
						|
        return sys.platform != 'win32' or shutil.which(clean_header)
 | 
						|
 | 
						|
 | 
						|
class WindowsExecutableLauncherWriter(WindowsScriptWriter):
 | 
						|
    @classmethod
 | 
						|
    def _get_script_args(cls, type_, name, header, script_text):
 | 
						|
        """
 | 
						|
        For Windows, add a .py extension and an .exe launcher
 | 
						|
        """
 | 
						|
        if type_ == 'gui':
 | 
						|
            launcher_type = 'gui'
 | 
						|
            ext = '-script.pyw'
 | 
						|
            old = ['.pyw']
 | 
						|
        else:
 | 
						|
            launcher_type = 'cli'
 | 
						|
            ext = '-script.py'
 | 
						|
            old = ['.py', '.pyc', '.pyo']
 | 
						|
        hdr = cls._adjust_header(type_, header)
 | 
						|
        blockers = [name + x for x in old]
 | 
						|
        yield (name + ext, hdr + script_text, 't', blockers)
 | 
						|
        yield (
 | 
						|
            name + '.exe',
 | 
						|
            get_win_launcher(launcher_type),
 | 
						|
            'b',  # write in binary mode
 | 
						|
        )
 | 
						|
        if not is_64bit():
 | 
						|
            # install a manifest for the launcher to prevent Windows
 | 
						|
            # from detecting it as an installer (which it will for
 | 
						|
            #  launchers like easy_install.exe). Consider only
 | 
						|
            #  adding a manifest for launchers detected as installers.
 | 
						|
            #  See Distribute #143 for details.
 | 
						|
            m_name = name + '.exe.manifest'
 | 
						|
            yield (m_name, load_launcher_manifest(name), 't')
 | 
						|
 | 
						|
 | 
						|
def get_win_launcher(type):
 | 
						|
    """
 | 
						|
    Load the Windows launcher (executable) suitable for launching a script.
 | 
						|
 | 
						|
    `type` should be either 'cli' or 'gui'
 | 
						|
 | 
						|
    Returns the executable as a byte string.
 | 
						|
    """
 | 
						|
    launcher_fn = f'{type}.exe'
 | 
						|
    if is_64bit():
 | 
						|
        if get_platform() == "win-arm64":
 | 
						|
            launcher_fn = launcher_fn.replace(".", "-arm64.")
 | 
						|
        else:
 | 
						|
            launcher_fn = launcher_fn.replace(".", "-64.")
 | 
						|
    else:
 | 
						|
        launcher_fn = launcher_fn.replace(".", "-32.")
 | 
						|
    return resources.files('setuptools').joinpath(launcher_fn).read_bytes()
 | 
						|
 | 
						|
 | 
						|
def load_launcher_manifest(name):
 | 
						|
    res = resources.files(__name__).joinpath('launcher manifest.xml')
 | 
						|
    return res.read_text(encoding='utf-8') % vars()
 | 
						|
 | 
						|
 | 
						|
def _first_line_re():
 | 
						|
    """
 | 
						|
    Return a regular expression based on first_line_re suitable for matching
 | 
						|
    strings.
 | 
						|
    """
 | 
						|
    if isinstance(first_line_re.pattern, str):
 | 
						|
        return first_line_re
 | 
						|
 | 
						|
    # first_line_re in Python >=3.1.4 and >=3.2.1 is a bytes pattern.
 | 
						|
    return re.compile(first_line_re.pattern.decode())
 | 
						|
 | 
						|
 | 
						|
def is_64bit():
 | 
						|
    return struct.calcsize("P") == 8
 | 
						|
 | 
						|
 | 
						|
def isascii(s):
 | 
						|
    try:
 | 
						|
        s.encode('ascii')
 | 
						|
    except UnicodeError:
 | 
						|
        return False
 | 
						|
    return True
 |