Team Ai
Datasetpublic

codekingpro/portable-devtools

sourceHugging Faceupdated 5mo agoView on Hugging Face
1likes14kdownloads
scripts.py453 linesDownload Raw Back to distlib
1# -*- coding: utf-8 -*-2#3# Copyright (C) 2013-2023 Vinay Sajip.4# Licensed to the Python Software Foundation under a contributor agreement.5# See LICENSE.txt and CONTRIBUTORS.txt.6#7from io import BytesIO8import logging9import os10import re11import struct12import sys13import time14from zipfile import ZipInfo15 16from .compat import sysconfig, detect_encoding, ZipFile17from .resources import finder18from .util import (FileOperator, get_export_entry, convert_path,19                   get_executable, get_platform, in_venv)20 21logger = logging.getLogger(__name__)22 23_DEFAULT_MANIFEST = '''24<?xml version="1.0" encoding="UTF-8" standalone="yes"?>25<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0">26 <assemblyIdentity version="1.0.0.0"27 processorArchitecture="X86"28 name="%s"29 type="win32"/>30 31 <!-- Identify the application security requirements. -->32 <trustInfo xmlns="urn:schemas-microsoft-com:asm.v3">33 <security>34 <requestedPrivileges>35 <requestedExecutionLevel level="asInvoker" uiAccess="false"/>36 </requestedPrivileges>37 </security>38 </trustInfo>39</assembly>'''.strip()40 41# check if Python is called on the first line with this expression42FIRST_LINE_RE = re.compile(b'^#!.*pythonw?[0-9.]*([ \t].*)?$')43SCRIPT_TEMPLATE = r'''# -*- coding: utf-8 -*-44import re45import sys46from %(module)s import %(import_name)s47if __name__ == '__main__':48    sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0])49    sys.exit(%(func)s())50'''51 52 53def enquote_executable(executable):54    if ' ' in executable:55        # make sure we quote only the executable in case of env56        # for example /usr/bin/env "/dir with spaces/bin/jython"57        # instead of "/usr/bin/env /dir with spaces/bin/jython"58        # otherwise whole59        if executable.startswith('/usr/bin/env '):60            env, _executable = executable.split(' ', 1)61            if ' ' in _executable and not _executable.startswith('"'):62                executable = '%s "%s"' % (env, _executable)63        else:64            if not executable.startswith('"'):65                executable = '"%s"' % executable66    return executable67 68 69# Keep the old name around (for now), as there is at least one project using it!70_enquote_executable = enquote_executable71 72 73class ScriptMaker(object):74    """75    A class to copy or create scripts from source scripts or callable76    specifications.77    """78    script_template = SCRIPT_TEMPLATE79 80    executable = None  # for shebangs81 82    def __init__(self,83                 source_dir,84                 target_dir,85                 add_launchers=True,86                 dry_run=False,87                 fileop=None):88        self.source_dir = source_dir89        self.target_dir = target_dir90        self.add_launchers = add_launchers91        self.force = False92        self.clobber = False93        # It only makes sense to set mode bits on POSIX.94        self.set_mode = (os.name == 'posix') or (os.name == 'java'95                                                 and os._name == 'posix')96        self.variants = set(('', 'X.Y'))97        self._fileop = fileop or FileOperator(dry_run)98 99        self._is_nt = os.name == 'nt' or (os.name == 'java'100                                          and os._name == 'nt')101        self.version_info = sys.version_info102 103    def _get_alternate_executable(self, executable, options):104        if options.get('gui', False) and self._is_nt:  # pragma: no cover105            dn, fn = os.path.split(executable)106            fn = fn.replace('python', 'pythonw')107            executable = os.path.join(dn, fn)108        return executable109 110    if sys.platform.startswith('java'):  # pragma: no cover111 112        def _is_shell(self, executable):113            """114            Determine if the specified executable is a script115            (contains a #! line)116            """117            try:118                with open(executable) as fp:119                    return fp.read(2) == '#!'120            except (OSError, IOError):121                logger.warning('Failed to open %s', executable)122                return False123 124        def _fix_jython_executable(self, executable):125            if self._is_shell(executable):126                # Workaround for Jython is not needed on Linux systems.127                import java128 129                if java.lang.System.getProperty('os.name') == 'Linux':130                    return executable131            elif executable.lower().endswith('jython.exe'):132                # Use wrapper exe for Jython on Windows133                return executable134            return '/usr/bin/env %s' % executable135 136    def _build_shebang(self, executable, post_interp):137        """138        Build a shebang line. In the simple case (on Windows, or a shebang line139        which is not too long or contains spaces) use a simple formulation for140        the shebang. Otherwise, use /bin/sh as the executable, with a contrived141        shebang which allows the script to run either under Python or sh, using142        suitable quoting. Thanks to Harald Nordgren for his input.143 144        See also: http://www.in-ulm.de/~mascheck/various/shebang/#length145                  https://hg.mozilla.org/mozilla-central/file/tip/mach146        """147        if os.name != 'posix':148            simple_shebang = True149        else:150            # Add 3 for '#!' prefix and newline suffix.151            shebang_length = len(executable) + len(post_interp) + 3152            if sys.platform == 'darwin':153                max_shebang_length = 512154            else:155                max_shebang_length = 127156            simple_shebang = ((b' ' not in executable)157                              and (shebang_length <= max_shebang_length))158 159        if simple_shebang:160            result = b'#!' + executable + post_interp + b'\n'161        else:162            result = b'#!/bin/sh\n'163            result += b"'''exec' " + executable + post_interp + b' "$0" "$@"\n'164            result += b"' '''"165        return result166 167    def _get_shebang(self, encoding, post_interp=b'', options=None):168        enquote = True169        if self.executable:170            executable = self.executable171            enquote = False  # assume this will be taken care of172        elif not sysconfig.is_python_build():173            executable = get_executable()174        elif in_venv():  # pragma: no cover175            executable = os.path.join(176                sysconfig.get_path('scripts'),177                'python%s' % sysconfig.get_config_var('EXE'))178        else:  # pragma: no cover179            if os.name == 'nt':180                # for Python builds from source on Windows, no Python executables with181                # a version suffix are created, so we use python.exe182                executable = os.path.join(183                    sysconfig.get_config_var('BINDIR'),184                    'python%s' % (sysconfig.get_config_var('EXE')))185            else:186                executable = os.path.join(187                    sysconfig.get_config_var('BINDIR'),188                    'python%s%s' % (sysconfig.get_config_var('VERSION'),189                                    sysconfig.get_config_var('EXE')))190        if options:191            executable = self._get_alternate_executable(executable, options)192 193        if sys.platform.startswith('java'):  # pragma: no cover194            executable = self._fix_jython_executable(executable)195 196        # Normalise case for Windows - COMMENTED OUT197        # executable = os.path.normcase(executable)198        # N.B. The normalising operation above has been commented out: See199        # issue #124. Although paths in Windows are generally case-insensitive,200        # they aren't always. For example, a path containing a ẞ (which is a201        # LATIN CAPITAL LETTER SHARP S - U+1E9E) is normcased to ß (which is a202        # LATIN SMALL LETTER SHARP S' - U+00DF). The two are not considered by203        # Windows as equivalent in path names.204 205        # If the user didn't specify an executable, it may be necessary to206        # cater for executable paths with spaces (not uncommon on Windows)207        if enquote:208            executable = enquote_executable(executable)209        # Issue #51: don't use fsencode, since we later try to210        # check that the shebang is decodable using utf-8.211        executable = executable.encode('utf-8')212        # in case of IronPython, play safe and enable frames support213        if (sys.platform == 'cli' and '-X:Frames' not in post_interp214                and '-X:FullFrames' not in post_interp):  # pragma: no cover215            post_interp += b' -X:Frames'216        shebang = self._build_shebang(executable, post_interp)217        # Python parser starts to read a script using UTF-8 until218        # it gets a #coding:xxx cookie. The shebang has to be the219        # first line of a file, the #coding:xxx cookie cannot be220        # written before. So the shebang has to be decodable from221        # UTF-8.222        try:223            shebang.decode('utf-8')224        except UnicodeDecodeError:  # pragma: no cover225            raise ValueError('The shebang (%r) is not decodable from utf-8' %226                             shebang)227        # If the script is encoded to a custom encoding (use a228        # #coding:xxx cookie), the shebang has to be decodable from229        # the script encoding too.230        if encoding != 'utf-8':231            try:232                shebang.decode(encoding)233            except UnicodeDecodeError:  # pragma: no cover234                raise ValueError('The shebang (%r) is not decodable '235                                 'from the script encoding (%r)' %236                                 (shebang, encoding))237        return shebang238 239    def _get_script_text(self, entry):240        return self.script_template % dict(241            module=entry.prefix,242            import_name=entry.suffix.split('.')[0],243            func=entry.suffix)244 245    manifest = _DEFAULT_MANIFEST246 247    def get_manifest(self, exename):248        base = os.path.basename(exename)249        return self.manifest % base250 251    def _write_script(self, names, shebang, script_bytes, filenames, ext):252        use_launcher = self.add_launchers and self._is_nt253        linesep = os.linesep.encode('utf-8')254        if not shebang.endswith(linesep):255            shebang += linesep256        if not use_launcher:257            script_bytes = shebang + script_bytes258        else:  # pragma: no cover259            if ext == 'py':260                launcher = self._get_launcher('t')261            else:262                launcher = self._get_launcher('w')263            stream = BytesIO()264            with ZipFile(stream, 'w') as zf:265                source_date_epoch = os.environ.get('SOURCE_DATE_EPOCH')266                if source_date_epoch:267                    date_time = time.gmtime(int(source_date_epoch))[:6]268                    zinfo = ZipInfo(filename='__main__.py',269                                    date_time=date_time)270                    zf.writestr(zinfo, script_bytes)271                else:272                    zf.writestr('__main__.py', script_bytes)273            zip_data = stream.getvalue()274            script_bytes = launcher + shebang + zip_data275        for name in names:276            outname = os.path.join(self.target_dir, name)277            if use_launcher:  # pragma: no cover278                n, e = os.path.splitext(outname)279                if e.startswith('.py'):280                    outname = n281                outname = '%s.exe' % outname282                try:283                    self._fileop.write_binary_file(outname, script_bytes)284                except Exception:285                    # Failed writing an executable - it might be in use.286                    logger.warning('Failed to write executable - trying to '287                                   'use .deleteme logic')288                    dfname = '%s.deleteme' % outname289                    if os.path.exists(dfname):290                        os.remove(dfname)  # Not allowed to fail here291                    os.rename(outname, dfname)  # nor here292                    self._fileop.write_binary_file(outname, script_bytes)293                    logger.debug('Able to replace executable using '294                                 '.deleteme logic')295                    try:296                        os.remove(dfname)297                    except Exception:298                        pass  # still in use - ignore error299            else:300                if self._is_nt and not outname.endswith(301                        '.' + ext):  # pragma: no cover302                    outname = '%s.%s' % (outname, ext)303                if os.path.exists(outname) and not self.clobber:304                    logger.warning('Skipping existing file %s', outname)305                    continue306                self._fileop.write_binary_file(outname, script_bytes)307                if self.set_mode:308                    self._fileop.set_executable_mode([outname])309            filenames.append(outname)310 311    variant_separator = '-'312 313    def get_script_filenames(self, name):314        result = set()315        if '' in self.variants:316            result.add(name)317        if 'X' in self.variants:318            result.add('%s%s' % (name, self.version_info[0]))319        if 'X.Y' in self.variants:320            result.add('%s%s%s.%s' %321                       (name, self.variant_separator, self.version_info[0],322                        self.version_info[1]))323        return result324 325    def _make_script(self, entry, filenames, options=None):326        post_interp = b''327        if options:328            args = options.get('interpreter_args', [])329            if args:330                args = ' %s' % ' '.join(args)331                post_interp = args.encode('utf-8')332        shebang = self._get_shebang('utf-8', post_interp, options=options)333        script = self._get_script_text(entry).encode('utf-8')334        scriptnames = self.get_script_filenames(entry.name)335        if options and options.get('gui', False):336            ext = 'pyw'337        else:338            ext = 'py'339        self._write_script(scriptnames, shebang, script, filenames, ext)340 341    def _copy_script(self, script, filenames):342        adjust = False343        script = os.path.join(self.source_dir, convert_path(script))344        outname = os.path.join(self.target_dir, os.path.basename(script))345        if not self.force and not self._fileop.newer(script, outname):346            logger.debug('not copying %s (up-to-date)', script)347            return348 349        # Always open the file, but ignore failures in dry-run mode --350        # that way, we'll get accurate feedback if we can read the351        # script.352        try:353            f = open(script, 'rb')354        except IOError:  # pragma: no cover355            if not self.dry_run:356                raise357            f = None358        else:359            first_line = f.readline()360            if not first_line:  # pragma: no cover361                logger.warning('%s is an empty file (skipping)', script)362                return363 364            match = FIRST_LINE_RE.match(first_line.replace(b'\r\n', b'\n'))365            if match:366                adjust = True367                post_interp = match.group(1) or b''368 369        if not adjust:370            if f:371                f.close()372            self._fileop.copy_file(script, outname)373            if self.set_mode:374                self._fileop.set_executable_mode([outname])375            filenames.append(outname)376        else:377            logger.info('copying and adjusting %s -> %s', script,378                        self.target_dir)379            if not self._fileop.dry_run:380                encoding, lines = detect_encoding(f.readline)381                f.seek(0)382                shebang = self._get_shebang(encoding, post_interp)383                if b'pythonw' in first_line:  # pragma: no cover384                    ext = 'pyw'385                else:386                    ext = 'py'387                n = os.path.basename(outname)388                self._write_script([n], shebang, f.read(), filenames, ext)389            if f:390                f.close()391 392    @property393    def dry_run(self):394        return self._fileop.dry_run395 396    @dry_run.setter397    def dry_run(self, value):398        self._fileop.dry_run = value399 400    if os.name == 'nt' or (os.name == 'java'401                           and os._name == 'nt'):  # pragma: no cover402        # Executable launcher support.403        # Launchers are from https://bitbucket.org/vinay.sajip/simple_launcher/404 405        def _get_launcher(self, kind):406            if struct.calcsize('P') == 8:  # 64-bit407                bits = '64'408            else:409                bits = '32'410            platform_suffix = '-arm' if get_platform() == 'win-arm64' else ''411            name = '%s%s%s.exe' % (kind, bits, platform_suffix)412            # Issue 31: don't hardcode an absolute package name, but413            # determine it relative to the current package414            distlib_package = __name__.rsplit('.', 1)[0]415            resource = finder(distlib_package).find(name)416            if not resource:417                msg = ('Unable to find resource %s in package %s' %418                       (name, distlib_package))419                raise ValueError(msg)420            return resource.bytes421 422    # Public API follows423 424    def make(self, specification, options=None):425        """426        Make a script.427 428        :param specification: The specification, which is either a valid export429                              entry specification (to make a script from a430                              callable) or a filename (to make a script by431                              copying from a source location).432        :param options: A dictionary of options controlling script generation.433        :return: A list of all absolute pathnames written to.434        """435        filenames = []436        entry = get_export_entry(specification)437        if entry is None:438            self._copy_script(specification, filenames)439        else:440            self._make_script(entry, filenames, options=options)441        return filenames442 443    def make_multiple(self, specifications, options=None):444        """445        Take a list of specifications and make scripts from them,446        :param specifications: A list of specifications.447        :return: A list of all absolute pathnames written to,448        """449        filenames = []450        for specification in specifications:451            filenames.extend(self.make(specification, options))452        return filenames453 
codekingpro/portable-devtools · Team Ai