python (3.12.0)

(root)/
lib/
python3.12/
site-packages/
pip/
_vendor/
distlib/
wheel.py
       1  # -*- coding: utf-8 -*-
       2  #
       3  # Copyright (C) 2013-2020 Vinay Sajip.
       4  # Licensed to the Python Software Foundation under a contributor agreement.
       5  # See LICENSE.txt and CONTRIBUTORS.txt.
       6  #
       7  from __future__ import unicode_literals
       8  
       9  import base64
      10  import codecs
      11  import datetime
      12  from email import message_from_file
      13  import hashlib
      14  import json
      15  import logging
      16  import os
      17  import posixpath
      18  import re
      19  import shutil
      20  import sys
      21  import tempfile
      22  import zipfile
      23  
      24  from . import __version__, DistlibException
      25  from .compat import sysconfig, ZipFile, fsdecode, text_type, filter
      26  from .database import InstalledDistribution
      27  from .metadata import (Metadata, METADATA_FILENAME, WHEEL_METADATA_FILENAME,
      28                         LEGACY_METADATA_FILENAME)
      29  from .util import (FileOperator, convert_path, CSVReader, CSVWriter, Cache,
      30                     cached_property, get_cache_base, read_exports, tempdir,
      31                     get_platform)
      32  from .version import NormalizedVersion, UnsupportedVersionError
      33  
      34  logger = logging.getLogger(__name__)
      35  
      36  cache = None    # created when needed
      37  
      38  if hasattr(sys, 'pypy_version_info'):  # pragma: no cover
      39      IMP_PREFIX = 'pp'
      40  elif sys.platform.startswith('java'):  # pragma: no cover
      41      IMP_PREFIX = 'jy'
      42  elif sys.platform == 'cli':  # pragma: no cover
      43      IMP_PREFIX = 'ip'
      44  else:
      45      IMP_PREFIX = 'cp'
      46  
      47  VER_SUFFIX = sysconfig.get_config_var('py_version_nodot')
      48  if not VER_SUFFIX:   # pragma: no cover
      49      VER_SUFFIX = '%s%s' % sys.version_info[:2]
      50  PYVER = 'py' + VER_SUFFIX
      51  IMPVER = IMP_PREFIX + VER_SUFFIX
      52  
      53  ARCH = get_platform().replace('-', '_').replace('.', '_')
      54  
      55  ABI = sysconfig.get_config_var('SOABI')
      56  if ABI and ABI.startswith('cpython-'):
      57      ABI = ABI.replace('cpython-', 'cp').split('-')[0]
      58  else:
      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 = True
      69                  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      ABI = _derive_abi()
      77      del _derive_abi
      78  
      79  FILENAME_RE = re.compile(r'''
      80  (?P<nm>[^-]+)
      81  -(?P<vn>\d+[^-]*)
      82  (-(?P<bn>\d+[^-]*))?
      83  -(?P<py>\w+\d+(\.\w+\d+)*)
      84  -(?P<bi>\w+)
      85  -(?P<ar>\w+(\.\w+)*)
      86  \.whl$
      87  ''', re.IGNORECASE | re.VERBOSE)
      88  
      89  NAME_VERSION_RE = re.compile(r'''
      90  (?P<nm>[^-]+)
      91  -(?P<vn>\d+[^-]*)
      92  (-(?P<bn>\d+[^-]*))?$
      93  ''', re.IGNORECASE | re.VERBOSE)
      94  
      95  SHEBANG_RE = re.compile(br'\s*#![^\r\n]*')
      96  SHEBANG_DETAIL_RE = re.compile(br'^(\s*#!("[^"]+"|\S+))\s+(.*)$')
      97  SHEBANG_PYTHON = b'#!python'
      98  SHEBANG_PYTHONW = b'#!pythonw'
      99  
     100  if os.sep == '/':
     101      to_posix = lambda o: o
     102  else:
     103      to_posix = lambda o: o.replace(os.sep, '/')
     104  
     105  if sys.version_info[0] < 3:
     106      import imp
     107  else:
     108      imp = None
     109      import importlib.machinery
     110      import importlib.util
     111  
     112  def _get_suffixes():
     113      if imp:
     114          return [s[0] for s in imp.get_suffixes()]
     115      else:
     116          return importlib.machinery.EXTENSION_SUFFIXES
     117  
     118  def _load_dynamic(name, path):
     119      # https://docs.python.org/3/library/importlib.html#importing-a-source-file-directly
     120      if imp:
     121          return imp.load_dynamic(name, path)
     122      else:
     123          spec = importlib.util.spec_from_file_location(name, path)
     124          module = importlib.util.module_from_spec(spec)
     125          sys.modules[name] = module
     126          spec.loader.exec_module(module)
     127          return module
     128  
     129  class ESC[4;38;5;81mMounter(ESC[4;38;5;149mobject):
     130      def __init__(self):
     131          self.impure_wheels = {}
     132          self.libs = {}
     133  
     134      def add(self, pathname, extensions):
     135          self.impure_wheels[pathname] = extensions
     136          self.libs.update(extensions)
     137  
     138      def remove(self, pathname):
     139          extensions = self.impure_wheels.pop(pathname)
     140          for k, v in extensions:
     141              if k in self.libs:
     142                  del self.libs[k]
     143  
     144      def find_module(self, fullname, path=None):
     145          if fullname in self.libs:
     146              result = self
     147          else:
     148              result = None
     149          return result
     150  
     151      def load_module(self, fullname):
     152          if fullname in sys.modules:
     153              result = sys.modules[fullname]
     154          else:
     155              if fullname not in self.libs:
     156                  raise ImportError('unable to find extension for %s' % fullname)
     157              result = _load_dynamic(fullname, self.libs[fullname])
     158              result.__loader__ = self
     159              parts = fullname.rsplit('.', 1)
     160              if len(parts) > 1:
     161                  result.__package__ = parts[0]
     162          return result
     163  
     164  _hook = Mounter()
     165  
     166  
     167  class ESC[4;38;5;81mWheel(ESC[4;38;5;149mobject):
     168      """
     169      Class to build and install from Wheel files (PEP 427).
     170      """
     171  
     172      wheel_version = (1, 1)
     173      hash_kind = 'sha256'
     174  
     175      def __init__(self, filename=None, sign=False, verify=False):
     176          """
     177          Initialise an instance using a (valid) filename.
     178          """
     179          self.sign = sign
     180          self.should_verify = verify
     181          self.buildver = ''
     182          self.pyver = [PYVER]
     183          self.abi = ['none']
     184          self.arch = ['any']
     185          self.dirname = os.getcwd()
     186          if filename is None:
     187              self.name = 'dummy'
     188              self.version = '0.1'
     189              self._filename = self.filename
     190          else:
     191              m = NAME_VERSION_RE.match(filename)
     192              if m:
     193                  info = m.groupdict('')
     194                  self.name = info['nm']
     195                  # Reinstate the local version separator
     196                  self.version = info['vn'].replace('_', '-')
     197                  self.buildver = info['bn']
     198                  self._filename = self.filename
     199              else:
     200                  dirname, filename = os.path.split(filename)
     201                  m = FILENAME_RE.match(filename)
     202                  if not m:
     203                      raise DistlibException('Invalid name or '
     204                                             'filename: %r' % filename)
     205                  if dirname:
     206                      self.dirname = os.path.abspath(dirname)
     207                  self._filename = filename
     208                  info = m.groupdict('')
     209                  self.name = info['nm']
     210                  self.version = info['vn']
     211                  self.buildver = info['bn']
     212                  self.pyver = info['py'].split('.')
     213                  self.abi = info['bi'].split('.')
     214                  self.arch = info['ar'].split('.')
     215  
     216      @property
     217      def filename(self):
     218          """
     219          Build and return a filename from the various components.
     220          """
     221          if self.buildver:
     222              buildver = '-' + self.buildver
     223          else:
     224              buildver = ''
     225          pyver = '.'.join(self.pyver)
     226          abi = '.'.join(self.abi)
     227          arch = '.'.join(self.arch)
     228          # replace - with _ as a local version separator
     229          version = self.version.replace('-', '_')
     230          return '%s-%s%s-%s-%s-%s.whl' % (self.name, version, buildver,
     231                                           pyver, abi, arch)
     232  
     233      @property
     234      def exists(self):
     235          path = os.path.join(self.dirname, self.filename)
     236          return os.path.isfile(path)
     237  
     238      @property
     239      def tags(self):
     240          for pyver in self.pyver:
     241              for abi in self.abi:
     242                  for arch in self.arch:
     243                      yield pyver, abi, arch
     244  
     245      @cached_property
     246      def metadata(self):
     247          pathname = os.path.join(self.dirname, self.filename)
     248          name_ver = '%s-%s' % (self.name, self.version)
     249          info_dir = '%s.dist-info' % name_ver
     250          wrapper = codecs.getreader('utf-8')
     251          with ZipFile(pathname, 'r') as zf:
     252              wheel_metadata = self.get_wheel_metadata(zf)
     253              wv = wheel_metadata['Wheel-Version'].split('.', 1)
     254              file_version = tuple([int(i) for i in wv])
     255              # if file_version < (1, 1):
     256                  # fns = [WHEEL_METADATA_FILENAME, METADATA_FILENAME,
     257                         # LEGACY_METADATA_FILENAME]
     258              # else:
     259                  # fns = [WHEEL_METADATA_FILENAME, METADATA_FILENAME]
     260              fns = [WHEEL_METADATA_FILENAME, LEGACY_METADATA_FILENAME]
     261              result = None
     262              for fn in fns:
     263                  try:
     264                      metadata_filename = posixpath.join(info_dir, fn)
     265                      with zf.open(metadata_filename) as bf:
     266                          wf = wrapper(bf)
     267                          result = Metadata(fileobj=wf)
     268                          if result:
     269                              break
     270                  except KeyError:
     271                      pass
     272              if not result:
     273                  raise ValueError('Invalid wheel, because metadata is '
     274                                   'missing: looked in %s' % ', '.join(fns))
     275          return result
     276  
     277      def get_wheel_metadata(self, zf):
     278          name_ver = '%s-%s' % (self.name, self.version)
     279          info_dir = '%s.dist-info' % name_ver
     280          metadata_filename = posixpath.join(info_dir, 'WHEEL')
     281          with zf.open(metadata_filename) as bf:
     282              wf = codecs.getreader('utf-8')(bf)
     283              message = message_from_file(wf)
     284          return dict(message)
     285  
     286      @cached_property
     287      def info(self):
     288          pathname = os.path.join(self.dirname, self.filename)
     289          with ZipFile(pathname, 'r') as zf:
     290              result = self.get_wheel_metadata(zf)
     291          return result
     292  
     293      def process_shebang(self, data):
     294          m = SHEBANG_RE.match(data)
     295          if m:
     296              end = m.end()
     297              shebang, data_after_shebang = data[:end], data[end:]
     298              # Preserve any arguments after the interpreter
     299              if b'pythonw' in shebang.lower():
     300                  shebang_python = SHEBANG_PYTHONW
     301              else:
     302                  shebang_python = SHEBANG_PYTHON
     303              m = SHEBANG_DETAIL_RE.match(shebang)
     304              if m:
     305                  args = b' ' + m.groups()[-1]
     306              else:
     307                  args = b''
     308              shebang = shebang_python + args
     309              data = shebang + data_after_shebang
     310          else:
     311              cr = data.find(b'\r')
     312              lf = data.find(b'\n')
     313              if cr < 0 or cr > lf:
     314                  term = b'\n'
     315              else:
     316                  if data[cr:cr + 2] == b'\r\n':
     317                      term = b'\r\n'
     318                  else:
     319                      term = b'\r'
     320              data = SHEBANG_PYTHON + term + data
     321          return data
     322  
     323      def get_hash(self, data, hash_kind=None):
     324          if hash_kind is None:
     325              hash_kind = self.hash_kind
     326          try:
     327              hasher = getattr(hashlib, hash_kind)
     328          except AttributeError:
     329              raise DistlibException('Unsupported hash algorithm: %r' % hash_kind)
     330          result = hasher(data).digest()
     331          result = base64.urlsafe_b64encode(result).rstrip(b'=').decode('ascii')
     332          return hash_kind, result
     333  
     334      def write_record(self, records, record_path, archive_record_path):
     335          records = list(records) # make a copy, as mutated
     336          records.append((archive_record_path, '', ''))
     337          with CSVWriter(record_path) as writer:
     338              for row in records:
     339                  writer.writerow(row)
     340  
     341      def write_records(self, info, libdir, archive_paths):
     342          records = []
     343          distinfo, info_dir = info
     344          hasher = getattr(hashlib, self.hash_kind)
     345          for ap, p in archive_paths:
     346              with open(p, 'rb') as f:
     347                  data = f.read()
     348              digest = '%s=%s' % self.get_hash(data)
     349              size = os.path.getsize(p)
     350              records.append((ap, digest, size))
     351  
     352          p = os.path.join(distinfo, 'RECORD')
     353          ap = to_posix(os.path.join(info_dir, 'RECORD'))
     354          self.write_record(records, p, ap)
     355          archive_paths.append((ap, p))
     356  
     357      def build_zip(self, pathname, archive_paths):
     358          with ZipFile(pathname, 'w', zipfile.ZIP_DEFLATED) as zf:
     359              for ap, p in archive_paths:
     360                  logger.debug('Wrote %s to %s in wheel', p, ap)
     361                  zf.write(p, ap)
     362  
     363      def build(self, paths, tags=None, wheel_version=None):
     364          """
     365          Build a wheel from files in specified paths, and use any specified tags
     366          when determining the name of the wheel.
     367          """
     368          if tags is None:
     369              tags = {}
     370  
     371          libkey = list(filter(lambda o: o in paths, ('purelib', 'platlib')))[0]
     372          if libkey == 'platlib':
     373              is_pure = 'false'
     374              default_pyver = [IMPVER]
     375              default_abi = [ABI]
     376              default_arch = [ARCH]
     377          else:
     378              is_pure = 'true'
     379              default_pyver = [PYVER]
     380              default_abi = ['none']
     381              default_arch = ['any']
     382  
     383          self.pyver = tags.get('pyver', default_pyver)
     384          self.abi = tags.get('abi', default_abi)
     385          self.arch = tags.get('arch', default_arch)
     386  
     387          libdir = paths[libkey]
     388  
     389          name_ver = '%s-%s' % (self.name, self.version)
     390          data_dir = '%s.data' % name_ver
     391          info_dir = '%s.dist-info' % name_ver
     392  
     393          archive_paths = []
     394  
     395          # First, stuff which is not in site-packages
     396          for key in ('data', 'headers', 'scripts'):
     397              if key not in paths:
     398                  continue
     399              path = paths[key]
     400              if os.path.isdir(path):
     401                  for root, dirs, files in os.walk(path):
     402                      for fn in files:
     403                          p = fsdecode(os.path.join(root, fn))
     404                          rp = os.path.relpath(p, path)
     405                          ap = to_posix(os.path.join(data_dir, key, rp))
     406                          archive_paths.append((ap, p))
     407                          if key == 'scripts' and not p.endswith('.exe'):
     408                              with open(p, 'rb') as f:
     409                                  data = f.read()
     410                              data = self.process_shebang(data)
     411                              with open(p, 'wb') as f:
     412                                  f.write(data)
     413  
     414          # Now, stuff which is in site-packages, other than the
     415          # distinfo stuff.
     416          path = libdir
     417          distinfo = None
     418          for root, dirs, files in os.walk(path):
     419              if root == path:
     420                  # At the top level only, save distinfo for later
     421                  # and skip it for now
     422                  for i, dn in enumerate(dirs):
     423                      dn = fsdecode(dn)
     424                      if dn.endswith('.dist-info'):
     425                          distinfo = os.path.join(root, dn)
     426                          del dirs[i]
     427                          break
     428                  assert distinfo, '.dist-info directory expected, not found'
     429  
     430              for fn in files:
     431                  # comment out next suite to leave .pyc files in
     432                  if fsdecode(fn).endswith(('.pyc', '.pyo')):
     433                      continue
     434                  p = os.path.join(root, fn)
     435                  rp = to_posix(os.path.relpath(p, path))
     436                  archive_paths.append((rp, p))
     437  
     438          # Now distinfo. Assumed to be flat, i.e. os.listdir is enough.
     439          files = os.listdir(distinfo)
     440          for fn in files:
     441              if fn not in ('RECORD', 'INSTALLER', 'SHARED', 'WHEEL'):
     442                  p = fsdecode(os.path.join(distinfo, fn))
     443                  ap = to_posix(os.path.join(info_dir, fn))
     444                  archive_paths.append((ap, p))
     445  
     446          wheel_metadata = [
     447              'Wheel-Version: %d.%d' % (wheel_version or self.wheel_version),
     448              'Generator: distlib %s' % __version__,
     449              'Root-Is-Purelib: %s' % is_pure,
     450          ]
     451          for pyver, abi, arch in self.tags:
     452              wheel_metadata.append('Tag: %s-%s-%s' % (pyver, abi, arch))
     453          p = os.path.join(distinfo, 'WHEEL')
     454          with open(p, 'w') as f:
     455              f.write('\n'.join(wheel_metadata))
     456          ap = to_posix(os.path.join(info_dir, 'WHEEL'))
     457          archive_paths.append((ap, p))
     458  
     459          # sort the entries by archive path. Not needed by any spec, but it
     460          # keeps the archive listing and RECORD tidier than they would otherwise
     461          # be. Use the number of path segments to keep directory entries together,
     462          # and keep the dist-info stuff at the end.
     463          def sorter(t):
     464              ap = t[0]
     465              n = ap.count('/')
     466              if '.dist-info' in ap:
     467                  n += 10000
     468              return (n, ap)
     469          archive_paths = sorted(archive_paths, key=sorter)
     470  
     471          # Now, at last, RECORD.
     472          # Paths in here are archive paths - nothing else makes sense.
     473          self.write_records((distinfo, info_dir), libdir, archive_paths)
     474          # Now, ready to build the zip file
     475          pathname = os.path.join(self.dirname, self.filename)
     476          self.build_zip(pathname, archive_paths)
     477          return pathname
     478  
     479      def skip_entry(self, arcname):
     480          """
     481          Determine whether an archive entry should be skipped when verifying
     482          or installing.
     483          """
     484          # The signature file won't be in RECORD,
     485          # and we  don't currently don't do anything with it
     486          # We also skip directories, as they won't be in RECORD
     487          # either. See:
     488          #
     489          # https://github.com/pypa/wheel/issues/294
     490          # https://github.com/pypa/wheel/issues/287
     491          # https://github.com/pypa/wheel/pull/289
     492          #
     493          return arcname.endswith(('/', '/RECORD.jws'))
     494  
     495      def install(self, paths, maker, **kwargs):
     496          """
     497          Install a wheel to the specified paths. If kwarg ``warner`` is
     498          specified, it should be a callable, which will be called with two
     499          tuples indicating the wheel version of this software and the wheel
     500          version in the file, if there is a discrepancy in the versions.
     501          This can be used to issue any warnings to raise any exceptions.
     502          If kwarg ``lib_only`` is True, only the purelib/platlib files are
     503          installed, and the headers, scripts, data and dist-info metadata are
     504          not written. If kwarg ``bytecode_hashed_invalidation`` is True, written
     505          bytecode will try to use file-hash based invalidation (PEP-552) on
     506          supported interpreter versions (CPython 2.7+).
     507  
     508          The return value is a :class:`InstalledDistribution` instance unless
     509          ``options.lib_only`` is True, in which case the return value is ``None``.
     510          """
     511  
     512          dry_run = maker.dry_run
     513          warner = kwargs.get('warner')
     514          lib_only = kwargs.get('lib_only', False)
     515          bc_hashed_invalidation = kwargs.get('bytecode_hashed_invalidation', False)
     516  
     517          pathname = os.path.join(self.dirname, self.filename)
     518          name_ver = '%s-%s' % (self.name, self.version)
     519          data_dir = '%s.data' % name_ver
     520          info_dir = '%s.dist-info' % name_ver
     521  
     522          metadata_name = posixpath.join(info_dir, LEGACY_METADATA_FILENAME)
     523          wheel_metadata_name = posixpath.join(info_dir, 'WHEEL')
     524          record_name = posixpath.join(info_dir, 'RECORD')
     525  
     526          wrapper = codecs.getreader('utf-8')
     527  
     528          with ZipFile(pathname, 'r') as zf:
     529              with zf.open(wheel_metadata_name) as bwf:
     530                  wf = wrapper(bwf)
     531                  message = message_from_file(wf)
     532              wv = message['Wheel-Version'].split('.', 1)
     533              file_version = tuple([int(i) for i in wv])
     534              if (file_version != self.wheel_version) and warner:
     535                  warner(self.wheel_version, file_version)
     536  
     537              if message['Root-Is-Purelib'] == 'true':
     538                  libdir = paths['purelib']
     539              else:
     540                  libdir = paths['platlib']
     541  
     542              records = {}
     543              with zf.open(record_name) as bf:
     544                  with CSVReader(stream=bf) as reader:
     545                      for row in reader:
     546                          p = row[0]
     547                          records[p] = row
     548  
     549              data_pfx = posixpath.join(data_dir, '')
     550              info_pfx = posixpath.join(info_dir, '')
     551              script_pfx = posixpath.join(data_dir, 'scripts', '')
     552  
     553              # make a new instance rather than a copy of maker's,
     554              # as we mutate it
     555              fileop = FileOperator(dry_run=dry_run)
     556              fileop.record = True    # so we can rollback if needed
     557  
     558              bc = not sys.dont_write_bytecode    # Double negatives. Lovely!
     559  
     560              outfiles = []   # for RECORD writing
     561  
     562              # for script copying/shebang processing
     563              workdir = tempfile.mkdtemp()
     564              # set target dir later
     565              # we default add_launchers to False, as the
     566              # Python Launcher should be used instead
     567              maker.source_dir = workdir
     568              maker.target_dir = None
     569              try:
     570                  for zinfo in zf.infolist():
     571                      arcname = zinfo.filename
     572                      if isinstance(arcname, text_type):
     573                          u_arcname = arcname
     574                      else:
     575                          u_arcname = arcname.decode('utf-8')
     576                      if self.skip_entry(u_arcname):
     577                          continue
     578                      row = records[u_arcname]
     579                      if row[2] and str(zinfo.file_size) != row[2]:
     580                          raise DistlibException('size mismatch for '
     581                                                 '%s' % u_arcname)
     582                      if row[1]:
     583                          kind, value = row[1].split('=', 1)
     584                          with zf.open(arcname) as bf:
     585                              data = bf.read()
     586                          _, digest = self.get_hash(data, kind)
     587                          if digest != value:
     588                              raise DistlibException('digest mismatch for '
     589                                                     '%s' % arcname)
     590  
     591                      if lib_only and u_arcname.startswith((info_pfx, data_pfx)):
     592                          logger.debug('lib_only: skipping %s', u_arcname)
     593                          continue
     594                      is_script = (u_arcname.startswith(script_pfx)
     595                                   and not u_arcname.endswith('.exe'))
     596  
     597                      if u_arcname.startswith(data_pfx):
     598                          _, where, rp = u_arcname.split('/', 2)
     599                          outfile = os.path.join(paths[where], convert_path(rp))
     600                      else:
     601                          # meant for site-packages.
     602                          if u_arcname in (wheel_metadata_name, record_name):
     603                              continue
     604                          outfile = os.path.join(libdir, convert_path(u_arcname))
     605                      if not is_script:
     606                          with zf.open(arcname) as bf:
     607                              fileop.copy_stream(bf, outfile)
     608                          # Issue #147: permission bits aren't preserved. Using
     609                          # zf.extract(zinfo, libdir) should have worked, but didn't,
     610                          # see https://www.thetopsites.net/article/53834422.shtml
     611                          # So ... manually preserve permission bits as given in zinfo
     612                          if os.name == 'posix':
     613                              # just set the normal permission bits
     614                              os.chmod(outfile, (zinfo.external_attr >> 16) & 0x1FF)
     615                          outfiles.append(outfile)
     616                          # Double check the digest of the written file
     617                          if not dry_run and row[1]:
     618                              with open(outfile, 'rb') as bf:
     619                                  data = bf.read()
     620                                  _, newdigest = self.get_hash(data, kind)
     621                                  if newdigest != digest:
     622                                      raise DistlibException('digest mismatch '
     623                                                             'on write for '
     624                                                             '%s' % outfile)
     625                          if bc and outfile.endswith('.py'):
     626                              try:
     627                                  pyc = fileop.byte_compile(outfile,
     628                                                            hashed_invalidation=bc_hashed_invalidation)
     629                                  outfiles.append(pyc)
     630                              except Exception:
     631                                  # Don't give up if byte-compilation fails,
     632                                  # but log it and perhaps warn the user
     633                                  logger.warning('Byte-compilation failed',
     634                                                 exc_info=True)
     635                      else:
     636                          fn = os.path.basename(convert_path(arcname))
     637                          workname = os.path.join(workdir, fn)
     638                          with zf.open(arcname) as bf:
     639                              fileop.copy_stream(bf, workname)
     640  
     641                          dn, fn = os.path.split(outfile)
     642                          maker.target_dir = dn
     643                          filenames = maker.make(fn)
     644                          fileop.set_executable_mode(filenames)
     645                          outfiles.extend(filenames)
     646  
     647                  if lib_only:
     648                      logger.debug('lib_only: returning None')
     649                      dist = None
     650                  else:
     651                      # Generate scripts
     652  
     653                      # Try to get pydist.json so we can see if there are
     654                      # any commands to generate. If this fails (e.g. because
     655                      # of a legacy wheel), log a warning but don't give up.
     656                      commands = None
     657                      file_version = self.info['Wheel-Version']
     658                      if file_version == '1.0':
     659                          # Use legacy info
     660                          ep = posixpath.join(info_dir, 'entry_points.txt')
     661                          try:
     662                              with zf.open(ep) as bwf:
     663                                  epdata = read_exports(bwf)
     664                              commands = {}
     665                              for key in ('console', 'gui'):
     666                                  k = '%s_scripts' % key
     667                                  if k in epdata:
     668                                      commands['wrap_%s' % key] = d = {}
     669                                      for v in epdata[k].values():
     670                                          s = '%s:%s' % (v.prefix, v.suffix)
     671                                          if v.flags:
     672                                              s += ' [%s]' % ','.join(v.flags)
     673                                          d[v.name] = s
     674                          except Exception:
     675                              logger.warning('Unable to read legacy script '
     676                                             'metadata, so cannot generate '
     677                                             'scripts')
     678                      else:
     679                          try:
     680                              with zf.open(metadata_name) as bwf:
     681                                  wf = wrapper(bwf)
     682                                  commands = json.load(wf).get('extensions')
     683                                  if commands:
     684                                      commands = commands.get('python.commands')
     685                          except Exception:
     686                              logger.warning('Unable to read JSON metadata, so '
     687                                             'cannot generate scripts')
     688                      if commands:
     689                          console_scripts = commands.get('wrap_console', {})
     690                          gui_scripts = commands.get('wrap_gui', {})
     691                          if console_scripts or gui_scripts:
     692                              script_dir = paths.get('scripts', '')
     693                              if not os.path.isdir(script_dir):
     694                                  raise ValueError('Valid script path not '
     695                                                   'specified')
     696                              maker.target_dir = script_dir
     697                              for k, v in console_scripts.items():
     698                                  script = '%s = %s' % (k, v)
     699                                  filenames = maker.make(script)
     700                                  fileop.set_executable_mode(filenames)
     701  
     702                              if gui_scripts:
     703                                  options = {'gui': True }
     704                                  for k, v in gui_scripts.items():
     705                                      script = '%s = %s' % (k, v)
     706                                      filenames = maker.make(script, options)
     707                                      fileop.set_executable_mode(filenames)
     708  
     709                      p = os.path.join(libdir, info_dir)
     710                      dist = InstalledDistribution(p)
     711  
     712                      # Write SHARED
     713                      paths = dict(paths)     # don't change passed in dict
     714                      del paths['purelib']
     715                      del paths['platlib']
     716                      paths['lib'] = libdir
     717                      p = dist.write_shared_locations(paths, dry_run)
     718                      if p:
     719                          outfiles.append(p)
     720  
     721                      # Write RECORD
     722                      dist.write_installed_files(outfiles, paths['prefix'],
     723                                                 dry_run)
     724                  return dist
     725              except Exception:  # pragma: no cover
     726                  logger.exception('installation failed.')
     727                  fileop.rollback()
     728                  raise
     729              finally:
     730                  shutil.rmtree(workdir)
     731  
     732      def _get_dylib_cache(self):
     733          global cache
     734          if cache is None:
     735              # Use native string to avoid issues on 2.x: see Python #20140.
     736              base = os.path.join(get_cache_base(), str('dylib-cache'),
     737                                  '%s.%s' % sys.version_info[:2])
     738              cache = Cache(base)
     739          return cache
     740  
     741      def _get_extensions(self):
     742          pathname = os.path.join(self.dirname, self.filename)
     743          name_ver = '%s-%s' % (self.name, self.version)
     744          info_dir = '%s.dist-info' % name_ver
     745          arcname = posixpath.join(info_dir, 'EXTENSIONS')
     746          wrapper = codecs.getreader('utf-8')
     747          result = []
     748          with ZipFile(pathname, 'r') as zf:
     749              try:
     750                  with zf.open(arcname) as bf:
     751                      wf = wrapper(bf)
     752                      extensions = json.load(wf)
     753                      cache = self._get_dylib_cache()
     754                      prefix = cache.prefix_to_dir(pathname)
     755                      cache_base = os.path.join(cache.base, prefix)
     756                      if not os.path.isdir(cache_base):
     757                          os.makedirs(cache_base)
     758                      for name, relpath in extensions.items():
     759                          dest = os.path.join(cache_base, convert_path(relpath))
     760                          if not os.path.exists(dest):
     761                              extract = True
     762                          else:
     763                              file_time = os.stat(dest).st_mtime
     764                              file_time = datetime.datetime.fromtimestamp(file_time)
     765                              info = zf.getinfo(relpath)
     766                              wheel_time = datetime.datetime(*info.date_time)
     767                              extract = wheel_time > file_time
     768                          if extract:
     769                              zf.extract(relpath, cache_base)
     770                          result.append((name, dest))
     771              except KeyError:
     772                  pass
     773          return result
     774  
     775      def is_compatible(self):
     776          """
     777          Determine if a wheel is compatible with the running system.
     778          """
     779          return is_compatible(self)
     780  
     781      def is_mountable(self):
     782          """
     783          Determine if a wheel is asserted as mountable by its metadata.
     784          """
     785          return True # for now - metadata details TBD
     786  
     787      def mount(self, append=False):
     788          pathname = os.path.abspath(os.path.join(self.dirname, self.filename))
     789          if not self.is_compatible():
     790              msg = 'Wheel %s not compatible with this Python.' % pathname
     791              raise DistlibException(msg)
     792          if not self.is_mountable():
     793              msg = 'Wheel %s is marked as not mountable.' % pathname
     794              raise DistlibException(msg)
     795          if pathname in sys.path:
     796              logger.debug('%s already in path', pathname)
     797          else:
     798              if append:
     799                  sys.path.append(pathname)
     800              else:
     801                  sys.path.insert(0, pathname)
     802              extensions = self._get_extensions()
     803              if extensions:
     804                  if _hook not in sys.meta_path:
     805                      sys.meta_path.append(_hook)
     806                  _hook.add(pathname, extensions)
     807  
     808      def unmount(self):
     809          pathname = os.path.abspath(os.path.join(self.dirname, self.filename))
     810          if pathname not in sys.path:
     811              logger.debug('%s not in path', pathname)
     812          else:
     813              sys.path.remove(pathname)
     814              if pathname in _hook.impure_wheels:
     815                  _hook.remove(pathname)
     816              if not _hook.impure_wheels:
     817                  if _hook in sys.meta_path:
     818                      sys.meta_path.remove(_hook)
     819  
     820      def verify(self):
     821          pathname = os.path.join(self.dirname, self.filename)
     822          name_ver = '%s-%s' % (self.name, self.version)
     823          data_dir = '%s.data' % name_ver
     824          info_dir = '%s.dist-info' % name_ver
     825  
     826          metadata_name = posixpath.join(info_dir, LEGACY_METADATA_FILENAME)
     827          wheel_metadata_name = posixpath.join(info_dir, 'WHEEL')
     828          record_name = posixpath.join(info_dir, 'RECORD')
     829  
     830          wrapper = codecs.getreader('utf-8')
     831  
     832          with ZipFile(pathname, 'r') as zf:
     833              with zf.open(wheel_metadata_name) as bwf:
     834                  wf = wrapper(bwf)
     835                  message = message_from_file(wf)
     836              wv = message['Wheel-Version'].split('.', 1)
     837              file_version = tuple([int(i) for i in wv])
     838              # TODO version verification
     839  
     840              records = {}
     841              with zf.open(record_name) as bf:
     842                  with CSVReader(stream=bf) as reader:
     843                      for row in reader:
     844                          p = row[0]
     845                          records[p] = row
     846  
     847              for zinfo in zf.infolist():
     848                  arcname = zinfo.filename
     849                  if isinstance(arcname, text_type):
     850                      u_arcname = arcname
     851                  else:
     852                      u_arcname = arcname.decode('utf-8')
     853                  # See issue #115: some wheels have .. in their entries, but
     854                  # in the filename ... e.g. __main__..py ! So the check is
     855                  # updated to look for .. in the directory portions
     856                  p = u_arcname.split('/')
     857                  if '..' in p:
     858                      raise DistlibException('invalid entry in '
     859                                             'wheel: %r' % u_arcname)
     860  
     861                  if self.skip_entry(u_arcname):
     862                      continue
     863                  row = records[u_arcname]
     864                  if row[2] and str(zinfo.file_size) != row[2]:
     865                      raise DistlibException('size mismatch for '
     866                                             '%s' % u_arcname)
     867                  if row[1]:
     868                      kind, value = row[1].split('=', 1)
     869                      with zf.open(arcname) as bf:
     870                          data = bf.read()
     871                      _, digest = self.get_hash(data, kind)
     872                      if digest != value:
     873                          raise DistlibException('digest mismatch for '
     874                                                 '%s' % arcname)
     875  
     876      def update(self, modifier, dest_dir=None, **kwargs):
     877          """
     878          Update the contents of a wheel in a generic way. The modifier should
     879          be a callable which expects a dictionary argument: its keys are
     880          archive-entry paths, and its values are absolute filesystem paths
     881          where the contents the corresponding archive entries can be found. The
     882          modifier is free to change the contents of the files pointed to, add
     883          new entries and remove entries, before returning. This method will
     884          extract the entire contents of the wheel to a temporary location, call
     885          the modifier, and then use the passed (and possibly updated)
     886          dictionary to write a new wheel. If ``dest_dir`` is specified, the new
     887          wheel is written there -- otherwise, the original wheel is overwritten.
     888  
     889          The modifier should return True if it updated the wheel, else False.
     890          This method returns the same value the modifier returns.
     891          """
     892  
     893          def get_version(path_map, info_dir):
     894              version = path = None
     895              key = '%s/%s' % (info_dir, LEGACY_METADATA_FILENAME)
     896              if key not in path_map:
     897                  key = '%s/PKG-INFO' % info_dir
     898              if key in path_map:
     899                  path = path_map[key]
     900                  version = Metadata(path=path).version
     901              return version, path
     902  
     903          def update_version(version, path):
     904              updated = None
     905              try:
     906                  v = NormalizedVersion(version)
     907                  i = version.find('-')
     908                  if i < 0:
     909                      updated = '%s+1' % version
     910                  else:
     911                      parts = [int(s) for s in version[i + 1:].split('.')]
     912                      parts[-1] += 1
     913                      updated = '%s+%s' % (version[:i],
     914                                           '.'.join(str(i) for i in parts))
     915              except UnsupportedVersionError:
     916                  logger.debug('Cannot update non-compliant (PEP-440) '
     917                               'version %r', version)
     918              if updated:
     919                  md = Metadata(path=path)
     920                  md.version = updated
     921                  legacy = path.endswith(LEGACY_METADATA_FILENAME)
     922                  md.write(path=path, legacy=legacy)
     923                  logger.debug('Version updated from %r to %r', version,
     924                               updated)
     925  
     926          pathname = os.path.join(self.dirname, self.filename)
     927          name_ver = '%s-%s' % (self.name, self.version)
     928          info_dir = '%s.dist-info' % name_ver
     929          record_name = posixpath.join(info_dir, 'RECORD')
     930          with tempdir() as workdir:
     931              with ZipFile(pathname, 'r') as zf:
     932                  path_map = {}
     933                  for zinfo in zf.infolist():
     934                      arcname = zinfo.filename
     935                      if isinstance(arcname, text_type):
     936                          u_arcname = arcname
     937                      else:
     938                          u_arcname = arcname.decode('utf-8')
     939                      if u_arcname == record_name:
     940                          continue
     941                      if '..' in u_arcname:
     942                          raise DistlibException('invalid entry in '
     943                                                 'wheel: %r' % u_arcname)
     944                      zf.extract(zinfo, workdir)
     945                      path = os.path.join(workdir, convert_path(u_arcname))
     946                      path_map[u_arcname] = path
     947  
     948              # Remember the version.
     949              original_version, _ = get_version(path_map, info_dir)
     950              # Files extracted. Call the modifier.
     951              modified = modifier(path_map, **kwargs)
     952              if modified:
     953                  # Something changed - need to build a new wheel.
     954                  current_version, path = get_version(path_map, info_dir)
     955                  if current_version and (current_version == original_version):
     956                      # Add or update local version to signify changes.
     957                      update_version(current_version, path)
     958                  # Decide where the new wheel goes.
     959                  if dest_dir is None:
     960                      fd, newpath = tempfile.mkstemp(suffix='.whl',
     961                                                     prefix='wheel-update-',
     962                                                     dir=workdir)
     963                      os.close(fd)
     964                  else:
     965                      if not os.path.isdir(dest_dir):
     966                          raise DistlibException('Not a directory: %r' % dest_dir)
     967                      newpath = os.path.join(dest_dir, self.filename)
     968                  archive_paths = list(path_map.items())
     969                  distinfo = os.path.join(workdir, info_dir)
     970                  info = distinfo, info_dir
     971                  self.write_records(info, workdir, archive_paths)
     972                  self.build_zip(newpath, archive_paths)
     973                  if dest_dir is None:
     974                      shutil.copyfile(newpath, pathname)
     975          return modified
     976  
     977  def _get_glibc_version():
     978      import platform
     979      ver = platform.libc_ver()
     980      result = []
     981      if ver[0] == 'glibc':
     982          for s in ver[1].split('.'):
     983              result.append(int(s) if s.isdigit() else 0)
     984          result = tuple(result)
     985      return result
     986  
     987  def compatible_tags():
     988      """
     989      Return (pyver, abi, arch) tuples compatible with this Python.
     990      """
     991      versions = [VER_SUFFIX]
     992      major = VER_SUFFIX[0]
     993      for minor in range(sys.version_info[1] - 1, - 1, -1):
     994          versions.append(''.join([major, str(minor)]))
     995  
     996      abis = []
     997      for suffix in _get_suffixes():
     998          if suffix.startswith('.abi'):
     999              abis.append(suffix.split('.', 2)[1])
    1000      abis.sort()
    1001      if ABI != 'none':
    1002          abis.insert(0, ABI)
    1003      abis.append('none')
    1004      result = []
    1005  
    1006      arches = [ARCH]
    1007      if sys.platform == 'darwin':
    1008          m = re.match(r'(\w+)_(\d+)_(\d+)_(\w+)$', ARCH)
    1009          if m:
    1010              name, major, minor, arch = m.groups()
    1011              minor = int(minor)
    1012              matches = [arch]
    1013              if arch in ('i386', 'ppc'):
    1014                  matches.append('fat')
    1015              if arch in ('i386', 'ppc', 'x86_64'):
    1016                  matches.append('fat3')
    1017              if arch in ('ppc64', 'x86_64'):
    1018                  matches.append('fat64')
    1019              if arch in ('i386', 'x86_64'):
    1020                  matches.append('intel')
    1021              if arch in ('i386', 'x86_64', 'intel', 'ppc', 'ppc64'):
    1022                  matches.append('universal')
    1023              while minor >= 0:
    1024                  for match in matches:
    1025                      s = '%s_%s_%s_%s' % (name, major, minor, match)
    1026                      if s != ARCH:   # already there
    1027                          arches.append(s)
    1028                  minor -= 1
    1029  
    1030      # Most specific - our Python version, ABI and arch
    1031      for abi in abis:
    1032          for arch in arches:
    1033              result.append((''.join((IMP_PREFIX, versions[0])), abi, arch))
    1034              # manylinux
    1035              if abi != 'none' and sys.platform.startswith('linux'):
    1036                  arch = arch.replace('linux_', '')
    1037                  parts = _get_glibc_version()
    1038                  if len(parts) == 2:
    1039                      if parts >= (2, 5):
    1040                          result.append((''.join((IMP_PREFIX, versions[0])), abi,
    1041                                         'manylinux1_%s' % arch))
    1042                      if parts >= (2, 12):
    1043                          result.append((''.join((IMP_PREFIX, versions[0])), abi,
    1044                                         'manylinux2010_%s' % arch))
    1045                      if parts >= (2, 17):
    1046                          result.append((''.join((IMP_PREFIX, versions[0])), abi,
    1047                                         'manylinux2014_%s' % arch))
    1048                      result.append((''.join((IMP_PREFIX, versions[0])), abi,
    1049                                     'manylinux_%s_%s_%s' % (parts[0], parts[1],
    1050                                                             arch)))
    1051  
    1052      # where no ABI / arch dependency, but IMP_PREFIX dependency
    1053      for i, version in enumerate(versions):
    1054          result.append((''.join((IMP_PREFIX, version)), 'none', 'any'))
    1055          if i == 0:
    1056              result.append((''.join((IMP_PREFIX, version[0])), 'none', 'any'))
    1057  
    1058      # no IMP_PREFIX, ABI or arch dependency
    1059      for i, version in enumerate(versions):
    1060          result.append((''.join(('py', version)), 'none', 'any'))
    1061          if i == 0:
    1062              result.append((''.join(('py', version[0])), 'none', 'any'))
    1063  
    1064      return set(result)
    1065  
    1066  
    1067  COMPATIBLE_TAGS = compatible_tags()
    1068  
    1069  del compatible_tags
    1070  
    1071  
    1072  def is_compatible(wheel, tags=None):
    1073      if not isinstance(wheel, Wheel):
    1074          wheel = Wheel(wheel)    # assume it's a filename
    1075      result = False
    1076      if tags is None:
    1077          tags = COMPATIBLE_TAGS
    1078      for ver, abi, arch in tags:
    1079          if ver in wheel.pyver and abi in wheel.abi and arch in wheel.arch:
    1080              result = True
    1081              break
    1082      return result