Team Ai
Datasetpublic

codekingpro/portable-devtools

sourceHugging Faceupdated 5mo agoView on Hugging Face
1likes14kdownloads
wheel.py1100 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 __future__ import unicode_literals8 9import base6410import codecs11import datetime12from email import message_from_file13import hashlib14import json15import logging16import os17import posixpath18import re19import shutil20import sys21import tempfile22import zipfile23 24from . import __version__, DistlibException25from .compat import sysconfig, ZipFile, fsdecode, text_type, filter26from .database import InstalledDistribution27from .metadata import Metadata, WHEEL_METADATA_FILENAME, LEGACY_METADATA_FILENAME28from .util import (FileOperator, convert_path, CSVReader, CSVWriter, Cache,29                   cached_property, get_cache_base, read_exports, tempdir,30                   get_platform)31from .version import NormalizedVersion, UnsupportedVersionError32 33logger = logging.getLogger(__name__)34 35cache = None  # created when needed36 37if hasattr(sys, 'pypy_version_info'):  # pragma: no cover38    IMP_PREFIX = 'pp'39elif sys.platform.startswith('java'):  # pragma: no cover40    IMP_PREFIX = 'jy'41elif sys.platform == 'cli':  # pragma: no cover42    IMP_PREFIX = 'ip'43else:44    IMP_PREFIX = 'cp'45 46VER_SUFFIX = sysconfig.get_config_var('py_version_nodot')47if not VER_SUFFIX:  # pragma: no cover48    VER_SUFFIX = '%s%s' % sys.version_info[:2]49PYVER = 'py' + VER_SUFFIX50IMPVER = IMP_PREFIX + VER_SUFFIX51 52ARCH = get_platform().replace('-', '_').replace('.', '_')53 54ABI = sysconfig.get_config_var('SOABI')55if ABI and ABI.startswith('cpython-'):56    ABI = ABI.replace('cpython-', 'cp').split('-')[0]57else:58 59    def _derive_abi():60        parts = ['cp', VER_SUFFIX]61        if sysconfig.get_config_var('Py_DEBUG'):62            parts.append('d')63        if IMP_PREFIX == 'cp':64            vi = sys.version_info[:2]65            if vi < (3, 8):66                wpm = sysconfig.get_config_var('WITH_PYMALLOC')67                if wpm is None:68                    wpm = True69                if wpm:70                    parts.append('m')71                if vi < (3, 3):72                    us = sysconfig.get_config_var('Py_UNICODE_SIZE')73                    if us == 4 or (us is None and sys.maxunicode == 0x10FFFF):74                        parts.append('u')75        return ''.join(parts)76 77    ABI = _derive_abi()78    del _derive_abi79 80FILENAME_RE = re.compile(81    r'''82(?P<nm>[^-]+)83-(?P<vn>\d+[^-]*)84(-(?P<bn>\d+[^-]*))?85-(?P<py>\w+\d+(\.\w+\d+)*)86-(?P<bi>\w+)87-(?P<ar>\w+(\.\w+)*)88\.whl$89''', re.IGNORECASE | re.VERBOSE)90 91NAME_VERSION_RE = re.compile(92    r'''93(?P<nm>[^-]+)94-(?P<vn>\d+[^-]*)95(-(?P<bn>\d+[^-]*))?$96''', re.IGNORECASE | re.VERBOSE)97 98SHEBANG_RE = re.compile(br'\s*#![^\r\n]*')99SHEBANG_DETAIL_RE = re.compile(br'^(\s*#!("[^"]+"|\S+))\s+(.*)$')100SHEBANG_PYTHON = b'#!python'101SHEBANG_PYTHONW = b'#!pythonw'102 103if os.sep == '/':104    to_posix = lambda o: o105else:106    to_posix = lambda o: o.replace(os.sep, '/')107 108if sys.version_info[0] < 3:109    import imp110else:111    imp = None112    import importlib.machinery113    import importlib.util114 115 116def _get_suffixes():117    if imp:118        return [s[0] for s in imp.get_suffixes()]119    else:120        return importlib.machinery.EXTENSION_SUFFIXES121 122 123def _load_dynamic(name, path):124    # https://docs.python.org/3/library/importlib.html#importing-a-source-file-directly125    if imp:126        return imp.load_dynamic(name, path)127    else:128        spec = importlib.util.spec_from_file_location(name, path)129        module = importlib.util.module_from_spec(spec)130        sys.modules[name] = module131        spec.loader.exec_module(module)132        return module133 134 135class Mounter(object):136 137    def __init__(self):138        self.impure_wheels = {}139        self.libs = {}140 141    def add(self, pathname, extensions):142        self.impure_wheels[pathname] = extensions143        self.libs.update(extensions)144 145    def remove(self, pathname):146        extensions = self.impure_wheels.pop(pathname)147        for k, v in extensions:148            if k in self.libs:149                del self.libs[k]150 151    def find_module(self, fullname, path=None):152        if fullname in self.libs:153            result = self154        else:155            result = None156        return result157 158    def load_module(self, fullname):159        if fullname in sys.modules:160            result = sys.modules[fullname]161        else:162            if fullname not in self.libs:163                raise ImportError('unable to find extension for %s' % fullname)164            result = _load_dynamic(fullname, self.libs[fullname])165            result.__loader__ = self166            parts = fullname.rsplit('.', 1)167            if len(parts) > 1:168                result.__package__ = parts[0]169        return result170 171 172_hook = Mounter()173 174 175class Wheel(object):176    """177    Class to build and install from Wheel files (PEP 427).178    """179 180    wheel_version = (1, 1)181    hash_kind = 'sha256'182 183    def __init__(self, filename=None, sign=False, verify=False):184        """185        Initialise an instance using a (valid) filename.186        """187        self.sign = sign188        self.should_verify = verify189        self.buildver = ''190        self.pyver = [PYVER]191        self.abi = ['none']192        self.arch = ['any']193        self.dirname = os.getcwd()194        if filename is None:195            self.name = 'dummy'196            self.version = '0.1'197            self._filename = self.filename198        else:199            m = NAME_VERSION_RE.match(filename)200            if m:201                info = m.groupdict('')202                self.name = info['nm']203                # Reinstate the local version separator204                self.version = info['vn'].replace('_', '-')205                self.buildver = info['bn']206                self._filename = self.filename207            else:208                dirname, filename = os.path.split(filename)209                m = FILENAME_RE.match(filename)210                if not m:211                    raise DistlibException('Invalid name or '212                                           'filename: %r' % filename)213                if dirname:214                    self.dirname = os.path.abspath(dirname)215                self._filename = filename216                info = m.groupdict('')217                self.name = info['nm']218                self.version = info['vn']219                self.buildver = info['bn']220                self.pyver = info['py'].split('.')221                self.abi = info['bi'].split('.')222                self.arch = info['ar'].split('.')223 224    @property225    def filename(self):226        """227        Build and return a filename from the various components.228        """229        if self.buildver:230            buildver = '-' + self.buildver231        else:232            buildver = ''233        pyver = '.'.join(self.pyver)234        abi = '.'.join(self.abi)235        arch = '.'.join(self.arch)236        # replace - with _ as a local version separator237        version = self.version.replace('-', '_')238        return '%s-%s%s-%s-%s-%s.whl' % (self.name, version, buildver, pyver,239                                         abi, arch)240 241    @property242    def exists(self):243        path = os.path.join(self.dirname, self.filename)244        return os.path.isfile(path)245 246    @property247    def tags(self):248        for pyver in self.pyver:249            for abi in self.abi:250                for arch in self.arch:251                    yield pyver, abi, arch252 253    @cached_property254    def metadata(self):255        pathname = os.path.join(self.dirname, self.filename)256        name_ver = '%s-%s' % (self.name, self.version)257        info_dir = '%s.dist-info' % name_ver258        wrapper = codecs.getreader('utf-8')259        with ZipFile(pathname, 'r') as zf:260            self.get_wheel_metadata(zf)261            # wv = wheel_metadata['Wheel-Version'].split('.', 1)262            # file_version = tuple([int(i) for i in wv])263            # if file_version < (1, 1):264            # fns = [WHEEL_METADATA_FILENAME, METADATA_FILENAME,265            # LEGACY_METADATA_FILENAME]266            # else:267            # fns = [WHEEL_METADATA_FILENAME, METADATA_FILENAME]268            fns = [WHEEL_METADATA_FILENAME, LEGACY_METADATA_FILENAME]269            result = None270            for fn in fns:271                try:272                    metadata_filename = posixpath.join(info_dir, fn)273                    with zf.open(metadata_filename) as bf:274                        wf = wrapper(bf)275                        result = Metadata(fileobj=wf)276                        if result:277                            break278                except KeyError:279                    pass280            if not result:281                raise ValueError('Invalid wheel, because metadata is '282                                 'missing: looked in %s' % ', '.join(fns))283        return result284 285    def get_wheel_metadata(self, zf):286        name_ver = '%s-%s' % (self.name, self.version)287        info_dir = '%s.dist-info' % name_ver288        metadata_filename = posixpath.join(info_dir, 'WHEEL')289        with zf.open(metadata_filename) as bf:290            wf = codecs.getreader('utf-8')(bf)291            message = message_from_file(wf)292        return dict(message)293 294    @cached_property295    def info(self):296        pathname = os.path.join(self.dirname, self.filename)297        with ZipFile(pathname, 'r') as zf:298            result = self.get_wheel_metadata(zf)299        return result300 301    def process_shebang(self, data):302        m = SHEBANG_RE.match(data)303        if m:304            end = m.end()305            shebang, data_after_shebang = data[:end], data[end:]306            # Preserve any arguments after the interpreter307            if b'pythonw' in shebang.lower():308                shebang_python = SHEBANG_PYTHONW309            else:310                shebang_python = SHEBANG_PYTHON311            m = SHEBANG_DETAIL_RE.match(shebang)312            if m:313                args = b' ' + m.groups()[-1]314            else:315                args = b''316            shebang = shebang_python + args317            data = shebang + data_after_shebang318        else:319            cr = data.find(b'\r')320            lf = data.find(b'\n')321            if cr < 0 or cr > lf:322                term = b'\n'323            else:324                if data[cr:cr + 2] == b'\r\n':325                    term = b'\r\n'326                else:327                    term = b'\r'328            data = SHEBANG_PYTHON + term + data329        return data330 331    def get_hash(self, data, hash_kind=None):332        if hash_kind is None:333            hash_kind = self.hash_kind334        try:335            hasher = getattr(hashlib, hash_kind)336        except AttributeError:337            raise DistlibException('Unsupported hash algorithm: %r' %338                                   hash_kind)339        result = hasher(data).digest()340        result = base64.urlsafe_b64encode(result).rstrip(b'=').decode('ascii')341        return hash_kind, result342 343    def write_record(self, records, record_path, archive_record_path):344        records = list(records)  # make a copy, as mutated345        records.append((archive_record_path, '', ''))346        with CSVWriter(record_path) as writer:347            for row in records:348                writer.writerow(row)349 350    def write_records(self, info, libdir, archive_paths):351        records = []352        distinfo, info_dir = info353        # hasher = getattr(hashlib, self.hash_kind)354        for ap, p in archive_paths:355            with open(p, 'rb') as f:356                data = f.read()357            digest = '%s=%s' % self.get_hash(data)358            size = os.path.getsize(p)359            records.append((ap, digest, size))360 361        p = os.path.join(distinfo, 'RECORD')362        ap = to_posix(os.path.join(info_dir, 'RECORD'))363        self.write_record(records, p, ap)364        archive_paths.append((ap, p))365 366    def build_zip(self, pathname, archive_paths):367        with ZipFile(pathname, 'w', zipfile.ZIP_DEFLATED) as zf:368            for ap, p in archive_paths:369                logger.debug('Wrote %s to %s in wheel', p, ap)370                zf.write(p, ap)371 372    def build(self, paths, tags=None, wheel_version=None):373        """374        Build a wheel from files in specified paths, and use any specified tags375        when determining the name of the wheel.376        """377        if tags is None:378            tags = {}379 380        libkey = list(filter(lambda o: o in paths, ('purelib', 'platlib')))[0]381        if libkey == 'platlib':382            is_pure = 'false'383            default_pyver = [IMPVER]384            default_abi = [ABI]385            default_arch = [ARCH]386        else:387            is_pure = 'true'388            default_pyver = [PYVER]389            default_abi = ['none']390            default_arch = ['any']391 392        self.pyver = tags.get('pyver', default_pyver)393        self.abi = tags.get('abi', default_abi)394        self.arch = tags.get('arch', default_arch)395 396        libdir = paths[libkey]397 398        name_ver = '%s-%s' % (self.name, self.version)399        data_dir = '%s.data' % name_ver400        info_dir = '%s.dist-info' % name_ver401 402        archive_paths = []403 404        # First, stuff which is not in site-packages405        for key in ('data', 'headers', 'scripts'):406            if key not in paths:407                continue408            path = paths[key]409            if os.path.isdir(path):410                for root, dirs, files in os.walk(path):411                    for fn in files:412                        p = fsdecode(os.path.join(root, fn))413                        rp = os.path.relpath(p, path)414                        ap = to_posix(os.path.join(data_dir, key, rp))415                        archive_paths.append((ap, p))416                        if key == 'scripts' and not p.endswith('.exe'):417                            with open(p, 'rb') as f:418                                data = f.read()419                            data = self.process_shebang(data)420                            with open(p, 'wb') as f:421                                f.write(data)422 423        # Now, stuff which is in site-packages, other than the424        # distinfo stuff.425        path = libdir426        distinfo = None427        for root, dirs, files in os.walk(path):428            if root == path:429                # At the top level only, save distinfo for later430                # and skip it for now431                for i, dn in enumerate(dirs):432                    dn = fsdecode(dn)433                    if dn.endswith('.dist-info'):434                        distinfo = os.path.join(root, dn)435                        del dirs[i]436                        break437                assert distinfo, '.dist-info directory expected, not found'438 439            for fn in files:440                # comment out next suite to leave .pyc files in441                if fsdecode(fn).endswith(('.pyc', '.pyo')):442                    continue443                p = os.path.join(root, fn)444                rp = to_posix(os.path.relpath(p, path))445                archive_paths.append((rp, p))446 447        # Now distinfo. Assumed to be flat, i.e. os.listdir is enough.448        files = os.listdir(distinfo)449        for fn in files:450            if fn not in ('RECORD', 'INSTALLER', 'SHARED', 'WHEEL'):451                p = fsdecode(os.path.join(distinfo, fn))452                ap = to_posix(os.path.join(info_dir, fn))453                archive_paths.append((ap, p))454 455        wheel_metadata = [456            'Wheel-Version: %d.%d' % (wheel_version or self.wheel_version),457            'Generator: distlib %s' % __version__,458            'Root-Is-Purelib: %s' % is_pure,459        ]460        for pyver, abi, arch in self.tags:461            wheel_metadata.append('Tag: %s-%s-%s' % (pyver, abi, arch))462        p = os.path.join(distinfo, 'WHEEL')463        with open(p, 'w') as f:464            f.write('\n'.join(wheel_metadata))465        ap = to_posix(os.path.join(info_dir, 'WHEEL'))466        archive_paths.append((ap, p))467 468        # sort the entries by archive path. Not needed by any spec, but it469        # keeps the archive listing and RECORD tidier than they would otherwise470        # be. Use the number of path segments to keep directory entries together,471        # and keep the dist-info stuff at the end.472        def sorter(t):473            ap = t[0]474            n = ap.count('/')475            if '.dist-info' in ap:476                n += 10000477            return (n, ap)478 479        archive_paths = sorted(archive_paths, key=sorter)480 481        # Now, at last, RECORD.482        # Paths in here are archive paths - nothing else makes sense.483        self.write_records((distinfo, info_dir), libdir, archive_paths)484        # Now, ready to build the zip file485        pathname = os.path.join(self.dirname, self.filename)486        self.build_zip(pathname, archive_paths)487        return pathname488 489    def skip_entry(self, arcname):490        """491        Determine whether an archive entry should be skipped when verifying492        or installing.493        """494        # The signature file won't be in RECORD,495        # and we  don't currently don't do anything with it496        # We also skip directories, as they won't be in RECORD497        # either. See:498        #499        # https://github.com/pypa/wheel/issues/294500        # https://github.com/pypa/wheel/issues/287501        # https://github.com/pypa/wheel/pull/289502        #503        return arcname.endswith(('/', '/RECORD.jws'))504 505    def install(self, paths, maker, **kwargs):506        """507        Install a wheel to the specified paths. If kwarg ``warner`` is508        specified, it should be a callable, which will be called with two509        tuples indicating the wheel version of this software and the wheel510        version in the file, if there is a discrepancy in the versions.511        This can be used to issue any warnings to raise any exceptions.512        If kwarg ``lib_only`` is True, only the purelib/platlib files are513        installed, and the headers, scripts, data and dist-info metadata are514        not written. If kwarg ``bytecode_hashed_invalidation`` is True, written515        bytecode will try to use file-hash based invalidation (PEP-552) on516        supported interpreter versions (CPython 2.7+).517 518        The return value is a :class:`InstalledDistribution` instance unless519        ``options.lib_only`` is True, in which case the return value is ``None``.520        """521 522        dry_run = maker.dry_run523        warner = kwargs.get('warner')524        lib_only = kwargs.get('lib_only', False)525        bc_hashed_invalidation = kwargs.get('bytecode_hashed_invalidation',526                                            False)527 528        pathname = os.path.join(self.dirname, self.filename)529        name_ver = '%s-%s' % (self.name, self.version)530        data_dir = '%s.data' % name_ver531        info_dir = '%s.dist-info' % name_ver532 533        metadata_name = posixpath.join(info_dir, LEGACY_METADATA_FILENAME)534        wheel_metadata_name = posixpath.join(info_dir, 'WHEEL')535        record_name = posixpath.join(info_dir, 'RECORD')536 537        wrapper = codecs.getreader('utf-8')538 539        with ZipFile(pathname, 'r') as zf:540            with zf.open(wheel_metadata_name) as bwf:541                wf = wrapper(bwf)542                message = message_from_file(wf)543            wv = message['Wheel-Version'].split('.', 1)544            file_version = tuple([int(i) for i in wv])545            if (file_version != self.wheel_version) and warner:546                warner(self.wheel_version, file_version)547 548            if message['Root-Is-Purelib'] == 'true':549                libdir = paths['purelib']550            else:551                libdir = paths['platlib']552 553            records = {}554            with zf.open(record_name) as bf:555                with CSVReader(stream=bf) as reader:556                    for row in reader:557                        p = row[0]558                        records[p] = row559 560            data_pfx = posixpath.join(data_dir, '')561            info_pfx = posixpath.join(info_dir, '')562            script_pfx = posixpath.join(data_dir, 'scripts', '')563 564            # make a new instance rather than a copy of maker's,565            # as we mutate it566            fileop = FileOperator(dry_run=dry_run)567            fileop.record = True  # so we can rollback if needed568 569            bc = not sys.dont_write_bytecode  # Double negatives. Lovely!570 571            outfiles = []  # for RECORD writing572 573            # for script copying/shebang processing574            workdir = tempfile.mkdtemp()575            # set target dir later576            # we default add_launchers to False, as the577            # Python Launcher should be used instead578            maker.source_dir = workdir579            maker.target_dir = None580            try:581                for zinfo in zf.infolist():582                    arcname = zinfo.filename583                    if isinstance(arcname, text_type):584                        u_arcname = arcname585                    else:586                        u_arcname = arcname.decode('utf-8')587                    if self.skip_entry(u_arcname):588                        continue589                    row = records[u_arcname]590                    if row[2] and str(zinfo.file_size) != row[2]:591                        raise DistlibException('size mismatch for '592                                               '%s' % u_arcname)593                    if row[1]:594                        kind, value = row[1].split('=', 1)595                        with zf.open(arcname) as bf:596                            data = bf.read()597                        _, digest = self.get_hash(data, kind)598                        if digest != value:599                            raise DistlibException('digest mismatch for '600                                                   '%s' % arcname)601 602                    if lib_only and u_arcname.startswith((info_pfx, data_pfx)):603                        logger.debug('lib_only: skipping %s', u_arcname)604                        continue605                    is_script = (u_arcname.startswith(script_pfx)606                                 and not u_arcname.endswith('.exe'))607 608                    if u_arcname.startswith(data_pfx):609                        _, where, rp = u_arcname.split('/', 2)610                        outfile = os.path.join(paths[where], convert_path(rp))611                    else:612                        # meant for site-packages.613                        if u_arcname in (wheel_metadata_name, record_name):614                            continue615                        outfile = os.path.join(libdir, convert_path(u_arcname))616                    if not is_script:617                        with zf.open(arcname) as bf:618                            fileop.copy_stream(bf, outfile)619                        # Issue #147: permission bits aren't preserved. Using620                        # zf.extract(zinfo, libdir) should have worked, but didn't,621                        # see https://www.thetopsites.net/article/53834422.shtml622                        # So ... manually preserve permission bits as given in zinfo623                        if os.name == 'posix':624                            # just set the normal permission bits625                            os.chmod(outfile,626                                     (zinfo.external_attr >> 16) & 0x1FF)627                        outfiles.append(outfile)628                        # Double check the digest of the written file629                        if not dry_run and row[1]:630                            with open(outfile, 'rb') as bf:631                                data = bf.read()632                                _, newdigest = self.get_hash(data, kind)633                                if newdigest != digest:634                                    raise DistlibException('digest mismatch '635                                                           'on write for '636                                                           '%s' % outfile)637                        if bc and outfile.endswith('.py'):638                            try:639                                pyc = fileop.byte_compile(640                                    outfile,641                                    hashed_invalidation=bc_hashed_invalidation)642                                outfiles.append(pyc)643                            except Exception:644                                # Don't give up if byte-compilation fails,645                                # but log it and perhaps warn the user646                                logger.warning('Byte-compilation failed',647                                               exc_info=True)648                    else:649                        fn = os.path.basename(convert_path(arcname))650                        workname = os.path.join(workdir, fn)651                        with zf.open(arcname) as bf:652                            fileop.copy_stream(bf, workname)653 654                        dn, fn = os.path.split(outfile)655                        maker.target_dir = dn656                        filenames = maker.make(fn)657                        fileop.set_executable_mode(filenames)658                        outfiles.extend(filenames)659 660                if lib_only:661                    logger.debug('lib_only: returning None')662                    dist = None663                else:664                    # Generate scripts665 666                    # Try to get pydist.json so we can see if there are667                    # any commands to generate. If this fails (e.g. because668                    # of a legacy wheel), log a warning but don't give up.669                    commands = None670                    file_version = self.info['Wheel-Version']671                    if file_version == '1.0':672                        # Use legacy info673                        ep = posixpath.join(info_dir, 'entry_points.txt')674                        try:675                            with zf.open(ep) as bwf:676                                epdata = read_exports(bwf)677                            commands = {}678                            for key in ('console', 'gui'):679                                k = '%s_scripts' % key680                                if k in epdata:681                                    commands['wrap_%s' % key] = d = {}682                                    for v in epdata[k].values():683                                        s = '%s:%s' % (v.prefix, v.suffix)684                                        if v.flags:685                                            s += ' [%s]' % ','.join(v.flags)686                                        d[v.name] = s687                        except Exception:688                            logger.warning('Unable to read legacy script '689                                           'metadata, so cannot generate '690                                           'scripts')691                    else:692                        try:693                            with zf.open(metadata_name) as bwf:694                                wf = wrapper(bwf)695                                commands = json.load(wf).get('extensions')696                                if commands:697                                    commands = commands.get('python.commands')698                        except Exception:699                            logger.warning('Unable to read JSON metadata, so '700                                           'cannot generate scripts')701                    if commands:702                        console_scripts = commands.get('wrap_console', {})703                        gui_scripts = commands.get('wrap_gui', {})704                        if console_scripts or gui_scripts:705                            script_dir = paths.get('scripts', '')706                            if not os.path.isdir(script_dir):707                                raise ValueError('Valid script path not '708                                                 'specified')709                            maker.target_dir = script_dir710                            for k, v in console_scripts.items():711                                script = '%s = %s' % (k, v)712                                filenames = maker.make(script)713                                fileop.set_executable_mode(filenames)714 715                            if gui_scripts:716                                options = {'gui': True}717                                for k, v in gui_scripts.items():718                                    script = '%s = %s' % (k, v)719                                    filenames = maker.make(script, options)720                                    fileop.set_executable_mode(filenames)721 722                    p = os.path.join(libdir, info_dir)723                    dist = InstalledDistribution(p)724 725                    # Write SHARED726                    paths = dict(paths)  # don't change passed in dict727                    del paths['purelib']728                    del paths['platlib']729                    paths['lib'] = libdir730                    p = dist.write_shared_locations(paths, dry_run)731                    if p:732                        outfiles.append(p)733 734                    # Write RECORD735                    dist.write_installed_files(outfiles, paths['prefix'],736                                               dry_run)737                return dist738            except Exception:  # pragma: no cover739                logger.exception('installation failed.')740                fileop.rollback()741                raise742            finally:743                shutil.rmtree(workdir)744 745    def _get_dylib_cache(self):746        global cache747        if cache is None:748            # Use native string to avoid issues on 2.x: see Python #20140.749            base = os.path.join(get_cache_base(), str('dylib-cache'),750                                '%s.%s' % sys.version_info[:2])751            cache = Cache(base)752        return cache753 754    def _get_extensions(self):755        pathname = os.path.join(self.dirname, self.filename)756        name_ver = '%s-%s' % (self.name, self.version)757        info_dir = '%s.dist-info' % name_ver758        arcname = posixpath.join(info_dir, 'EXTENSIONS')759        wrapper = codecs.getreader('utf-8')760        result = []761        with ZipFile(pathname, 'r') as zf:762            try:763                with zf.open(arcname) as bf:764                    wf = wrapper(bf)765                    extensions = json.load(wf)766                    cache = self._get_dylib_cache()767                    prefix = cache.prefix_to_dir(pathname)768                    cache_base = os.path.join(cache.base, prefix)769                    if not os.path.isdir(cache_base):770                        os.makedirs(cache_base)771                    for name, relpath in extensions.items():772                        dest = os.path.join(cache_base, convert_path(relpath))773                        if not os.path.exists(dest):774                            extract = True775                        else:776                            file_time = os.stat(dest).st_mtime777                            file_time = datetime.datetime.fromtimestamp(778                                file_time)779                            info = zf.getinfo(relpath)780                            wheel_time = datetime.datetime(*info.date_time)781                            extract = wheel_time > file_time782                        if extract:783                            zf.extract(relpath, cache_base)784                        result.append((name, dest))785            except KeyError:786                pass787        return result788 789    def is_compatible(self):790        """791        Determine if a wheel is compatible with the running system.792        """793        return is_compatible(self)794 795    def is_mountable(self):796        """797        Determine if a wheel is asserted as mountable by its metadata.798        """799        return True  # for now - metadata details TBD800 801    def mount(self, append=False):802        pathname = os.path.abspath(os.path.join(self.dirname, self.filename))803        if not self.is_compatible():804            msg = 'Wheel %s not compatible with this Python.' % pathname805            raise DistlibException(msg)806        if not self.is_mountable():807            msg = 'Wheel %s is marked as not mountable.' % pathname808            raise DistlibException(msg)809        if pathname in sys.path:810            logger.debug('%s already in path', pathname)811        else:812            if append:813                sys.path.append(pathname)814            else:815                sys.path.insert(0, pathname)816            extensions = self._get_extensions()817            if extensions:818                if _hook not in sys.meta_path:819                    sys.meta_path.append(_hook)820                _hook.add(pathname, extensions)821 822    def unmount(self):823        pathname = os.path.abspath(os.path.join(self.dirname, self.filename))824        if pathname not in sys.path:825            logger.debug('%s not in path', pathname)826        else:827            sys.path.remove(pathname)828            if pathname in _hook.impure_wheels:829                _hook.remove(pathname)830            if not _hook.impure_wheels:831                if _hook in sys.meta_path:832                    sys.meta_path.remove(_hook)833 834    def verify(self):835        pathname = os.path.join(self.dirname, self.filename)836        name_ver = '%s-%s' % (self.name, self.version)837        # data_dir = '%s.data' % name_ver838        info_dir = '%s.dist-info' % name_ver839 840        # metadata_name = posixpath.join(info_dir, LEGACY_METADATA_FILENAME)841        wheel_metadata_name = posixpath.join(info_dir, 'WHEEL')842        record_name = posixpath.join(info_dir, 'RECORD')843 844        wrapper = codecs.getreader('utf-8')845 846        with ZipFile(pathname, 'r') as zf:847            with zf.open(wheel_metadata_name) as bwf:848                wf = wrapper(bwf)849                message_from_file(wf)850            # wv = message['Wheel-Version'].split('.', 1)851            # file_version = tuple([int(i) for i in wv])852            # TODO version verification853 854            records = {}855            with zf.open(record_name) as bf:856                with CSVReader(stream=bf) as reader:857                    for row in reader:858                        p = row[0]859                        records[p] = row860 861            for zinfo in zf.infolist():862                arcname = zinfo.filename863                if isinstance(arcname, text_type):864                    u_arcname = arcname865                else:866                    u_arcname = arcname.decode('utf-8')867                # See issue #115: some wheels have .. in their entries, but868                # in the filename ... e.g. __main__..py ! So the check is869                # updated to look for .. in the directory portions870                p = u_arcname.split('/')871                if '..' in p:872                    raise DistlibException('invalid entry in '873                                           'wheel: %r' % u_arcname)874 875                if self.skip_entry(u_arcname):876                    continue877                row = records[u_arcname]878                if row[2] and str(zinfo.file_size) != row[2]:879                    raise DistlibException('size mismatch for '880                                           '%s' % u_arcname)881                if row[1]:882                    kind, value = row[1].split('=', 1)883                    with zf.open(arcname) as bf:884                        data = bf.read()885                    _, digest = self.get_hash(data, kind)886                    if digest != value:887                        raise DistlibException('digest mismatch for '888                                               '%s' % arcname)889 890    def update(self, modifier, dest_dir=None, **kwargs):891        """892        Update the contents of a wheel in a generic way. The modifier should893        be a callable which expects a dictionary argument: its keys are894        archive-entry paths, and its values are absolute filesystem paths895        where the contents the corresponding archive entries can be found. The896        modifier is free to change the contents of the files pointed to, add897        new entries and remove entries, before returning. This method will898        extract the entire contents of the wheel to a temporary location, call899        the modifier, and then use the passed (and possibly updated)900        dictionary to write a new wheel. If ``dest_dir`` is specified, the new901        wheel is written there -- otherwise, the original wheel is overwritten.902 903        The modifier should return True if it updated the wheel, else False.904        This method returns the same value the modifier returns.905        """906 907        def get_version(path_map, info_dir):908            version = path = None909            key = '%s/%s' % (info_dir, LEGACY_METADATA_FILENAME)910            if key not in path_map:911                key = '%s/PKG-INFO' % info_dir912            if key in path_map:913                path = path_map[key]914                version = Metadata(path=path).version915            return version, path916 917        def update_version(version, path):918            updated = None919            try:920                NormalizedVersion(version)921                i = version.find('-')922                if i < 0:923                    updated = '%s+1' % version924                else:925                    parts = [int(s) for s in version[i + 1:].split('.')]926                    parts[-1] += 1927                    updated = '%s+%s' % (version[:i], '.'.join(928                        str(i) for i in parts))929            except UnsupportedVersionError:930                logger.debug(931                    'Cannot update non-compliant (PEP-440) '932                    'version %r', version)933            if updated:934                md = Metadata(path=path)935                md.version = updated936                legacy = path.endswith(LEGACY_METADATA_FILENAME)937                md.write(path=path, legacy=legacy)938                logger.debug('Version updated from %r to %r', version, updated)939 940        pathname = os.path.join(self.dirname, self.filename)941        name_ver = '%s-%s' % (self.name, self.version)942        info_dir = '%s.dist-info' % name_ver943        record_name = posixpath.join(info_dir, 'RECORD')944        with tempdir() as workdir:945            with ZipFile(pathname, 'r') as zf:946                path_map = {}947                for zinfo in zf.infolist():948                    arcname = zinfo.filename949                    if isinstance(arcname, text_type):950                        u_arcname = arcname951                    else:952                        u_arcname = arcname.decode('utf-8')953                    if u_arcname == record_name:954                        continue955                    if '..' in u_arcname:956                        raise DistlibException('invalid entry in '957                                               'wheel: %r' % u_arcname)958                    zf.extract(zinfo, workdir)959                    path = os.path.join(workdir, convert_path(u_arcname))960                    path_map[u_arcname] = path961 962            # Remember the version.963            original_version, _ = get_version(path_map, info_dir)964            # Files extracted. Call the modifier.965            modified = modifier(path_map, **kwargs)966            if modified:967                # Something changed - need to build a new wheel.968                current_version, path = get_version(path_map, info_dir)969                if current_version and (current_version == original_version):970                    # Add or update local version to signify changes.971                    update_version(current_version, path)972                # Decide where the new wheel goes.973                if dest_dir is None:974                    fd, newpath = tempfile.mkstemp(suffix='.whl',975                                                   prefix='wheel-update-',976                                                   dir=workdir)977                    os.close(fd)978                else:979                    if not os.path.isdir(dest_dir):980                        raise DistlibException('Not a directory: %r' %981                                               dest_dir)982                    newpath = os.path.join(dest_dir, self.filename)983                archive_paths = list(path_map.items())984                distinfo = os.path.join(workdir, info_dir)985                info = distinfo, info_dir986                self.write_records(info, workdir, archive_paths)987                self.build_zip(newpath, archive_paths)988                if dest_dir is None:989                    shutil.copyfile(newpath, pathname)990        return modified991 992 993def _get_glibc_version():994    import platform995    ver = platform.libc_ver()996    result = []997    if ver[0] == 'glibc':998        for s in ver[1].split('.'):999            result.append(int(s) if s.isdigit() else 0)1000        result = tuple(result)1001    return result1002 1003 1004def compatible_tags():1005    """1006    Return (pyver, abi, arch) tuples compatible with this Python.1007    """1008    versions = [VER_SUFFIX]1009    major = VER_SUFFIX[0]1010    for minor in range(sys.version_info[1] - 1, -1, -1):1011        versions.append(''.join([major, str(minor)]))1012 1013    abis = []1014    for suffix in _get_suffixes():1015        if suffix.startswith('.abi'):1016            abis.append(suffix.split('.', 2)[1])1017    abis.sort()1018    if ABI != 'none':1019        abis.insert(0, ABI)1020    abis.append('none')1021    result = []1022 1023    arches = [ARCH]1024    if sys.platform == 'darwin':1025        m = re.match(r'(\w+)_(\d+)_(\d+)_(\w+)$', ARCH)1026        if m:1027            name, major, minor, arch = m.groups()1028            minor = int(minor)1029            matches = [arch]1030            if arch in ('i386', 'ppc'):1031                matches.append('fat')1032            if arch in ('i386', 'ppc', 'x86_64'):1033                matches.append('fat3')1034            if arch in ('ppc64', 'x86_64'):1035                matches.append('fat64')1036            if arch in ('i386', 'x86_64'):1037                matches.append('intel')1038            if arch in ('i386', 'x86_64', 'intel', 'ppc', 'ppc64'):1039                matches.append('universal')1040            while minor >= 0:1041                for match in matches:1042                    s = '%s_%s_%s_%s' % (name, major, minor, match)1043                    if s != ARCH:  # already there1044                        arches.append(s)1045                minor -= 11046 1047    # Most specific - our Python version, ABI and arch1048    for abi in abis:1049        for arch in arches:1050            result.append((''.join((IMP_PREFIX, versions[0])), abi, arch))1051            # manylinux1052            if abi != 'none' and sys.platform.startswith('linux'):1053                arch = arch.replace('linux_', '')1054                parts = _get_glibc_version()1055                if len(parts) == 2:1056                    if parts >= (2, 5):1057                        result.append((''.join((IMP_PREFIX, versions[0])), abi,1058                                       'manylinux1_%s' % arch))1059                    if parts >= (2, 12):1060                        result.append((''.join((IMP_PREFIX, versions[0])), abi,1061                                       'manylinux2010_%s' % arch))1062                    if parts >= (2, 17):1063                        result.append((''.join((IMP_PREFIX, versions[0])), abi,1064                                       'manylinux2014_%s' % arch))1065                    result.append(1066                        (''.join((IMP_PREFIX, versions[0])), abi,1067                         'manylinux_%s_%s_%s' % (parts[0], parts[1], arch)))1068 1069    # where no ABI / arch dependency, but IMP_PREFIX dependency1070    for i, version in enumerate(versions):1071        result.append((''.join((IMP_PREFIX, version)), 'none', 'any'))1072        if i == 0:1073            result.append((''.join((IMP_PREFIX, version[0])), 'none', 'any'))1074 1075    # no IMP_PREFIX, ABI or arch dependency1076    for i, version in enumerate(versions):1077        result.append((''.join(('py', version)), 'none', 'any'))1078        if i == 0:1079            result.append((''.join(('py', version[0])), 'none', 'any'))1080 1081    return set(result)1082 1083 1084COMPATIBLE_TAGS = compatible_tags()1085 1086del compatible_tags1087 1088 1089def is_compatible(wheel, tags=None):1090    if not isinstance(wheel, Wheel):1091        wheel = Wheel(wheel)  # assume it's a filename1092    result = False1093    if tags is None:1094        tags = COMPATIBLE_TAGS1095    for ver, abi, arch in tags:1096        if ver in wheel.pyver and abi in wheel.abi and arch in wheel.arch:1097            result = True1098            break1099    return result1100 
codekingpro/portable-devtools · Team Ai