|
|
|
|
|
|
|
|
|
|
|
|
|
from __future__ import unicode_literals |
|
|
|
import base64 |
|
import codecs |
|
import datetime |
|
from email import message_from_file |
|
import hashlib |
|
import json |
|
import logging |
|
import os |
|
import posixpath |
|
import re |
|
import shutil |
|
import sys |
|
import tempfile |
|
import zipfile |
|
|
|
from . import __version__, DistlibException |
|
from .compat import sysconfig, ZipFile, fsdecode, text_type, filter |
|
from .database import InstalledDistribution |
|
from .metadata import (Metadata, METADATA_FILENAME, WHEEL_METADATA_FILENAME, |
|
LEGACY_METADATA_FILENAME) |
|
from .util import (FileOperator, convert_path, CSVReader, CSVWriter, Cache, |
|
cached_property, get_cache_base, read_exports, tempdir, |
|
get_platform) |
|
from .version import NormalizedVersion, UnsupportedVersionError |
|
|
|
logger = logging.getLogger(__name__) |
|
|
|
cache = None |
|
|
|
if hasattr(sys, 'pypy_version_info'): |
|
IMP_PREFIX = 'pp' |
|
elif sys.platform.startswith('java'): |
|
IMP_PREFIX = 'jy' |
|
elif sys.platform == 'cli': |
|
IMP_PREFIX = 'ip' |
|
else: |
|
IMP_PREFIX = 'cp' |
|
|
|
VER_SUFFIX = sysconfig.get_config_var('py_version_nodot') |
|
if not VER_SUFFIX: |
|
VER_SUFFIX = '%s%s' % sys.version_info[:2] |
|
PYVER = 'py' + VER_SUFFIX |
|
IMPVER = IMP_PREFIX + VER_SUFFIX |
|
|
|
ARCH = get_platform().replace('-', '_').replace('.', '_') |
|
|
|
ABI = sysconfig.get_config_var('SOABI') |
|
if ABI and ABI.startswith('cpython-'): |
|
ABI = ABI.replace('cpython-', 'cp').split('-')[0] |
|
else: |
|
def _derive_abi(): |
|
parts = ['cp', VER_SUFFIX] |
|
if sysconfig.get_config_var('Py_DEBUG'): |
|
parts.append('d') |
|
if IMP_PREFIX == 'cp': |
|
vi = sys.version_info[:2] |
|
if vi < (3, 8): |
|
wpm = sysconfig.get_config_var('WITH_PYMALLOC') |
|
if wpm is None: |
|
wpm = True |
|
if wpm: |
|
parts.append('m') |
|
if vi < (3, 3): |
|
us = sysconfig.get_config_var('Py_UNICODE_SIZE') |
|
if us == 4 or (us is None and sys.maxunicode == 0x10FFFF): |
|
parts.append('u') |
|
return ''.join(parts) |
|
ABI = _derive_abi() |
|
del _derive_abi |
|
|
|
FILENAME_RE = re.compile(r''' |
|
(?P<nm>[^-]+) |
|
-(?P<vn>\d+[^-]*) |
|
(-(?P<bn>\d+[^-]*))? |
|
-(?P<py>\w+\d+(\.\w+\d+)*) |
|
-(?P<bi>\w+) |
|
-(?P<ar>\w+(\.\w+)*) |
|
\.whl$ |
|
''', re.IGNORECASE | re.VERBOSE) |
|
|
|
NAME_VERSION_RE = re.compile(r''' |
|
(?P<nm>[^-]+) |
|
-(?P<vn>\d+[^-]*) |
|
(-(?P<bn>\d+[^-]*))?$ |
|
''', re.IGNORECASE | re.VERBOSE) |
|
|
|
SHEBANG_RE = re.compile(br'\s*#![^\r\n]*') |
|
SHEBANG_DETAIL_RE = re.compile(br'^(\s*#!("[^"]+"|\S+))\s+(.*)$') |
|
SHEBANG_PYTHON = b'#!python' |
|
SHEBANG_PYTHONW = b'#!pythonw' |
|
|
|
if os.sep == '/': |
|
to_posix = lambda o: o |
|
else: |
|
to_posix = lambda o: o.replace(os.sep, '/') |
|
|
|
if sys.version_info[0] < 3: |
|
import imp |
|
else: |
|
imp = None |
|
import importlib.machinery |
|
import importlib.util |
|
|
|
def _get_suffixes(): |
|
if imp: |
|
return [s[0] for s in imp.get_suffixes()] |
|
else: |
|
return importlib.machinery.EXTENSION_SUFFIXES |
|
|
|
def _load_dynamic(name, path): |
|
|
|
if imp: |
|
return imp.load_dynamic(name, path) |
|
else: |
|
spec = importlib.util.spec_from_file_location(name, path) |
|
module = importlib.util.module_from_spec(spec) |
|
sys.modules[name] = module |
|
spec.loader.exec_module(module) |
|
return module |
|
|
|
class Mounter(object): |
|
def __init__(self): |
|
self.impure_wheels = {} |
|
self.libs = {} |
|
|
|
def add(self, pathname, extensions): |
|
self.impure_wheels[pathname] = extensions |
|
self.libs.update(extensions) |
|
|
|
def remove(self, pathname): |
|
extensions = self.impure_wheels.pop(pathname) |
|
for k, v in extensions: |
|
if k in self.libs: |
|
del self.libs[k] |
|
|
|
def find_module(self, fullname, path=None): |
|
if fullname in self.libs: |
|
result = self |
|
else: |
|
result = None |
|
return result |
|
|
|
def load_module(self, fullname): |
|
if fullname in sys.modules: |
|
result = sys.modules[fullname] |
|
else: |
|
if fullname not in self.libs: |
|
raise ImportError('unable to find extension for %s' % fullname) |
|
result = _load_dynamic(fullname, self.libs[fullname]) |
|
result.__loader__ = self |
|
parts = fullname.rsplit('.', 1) |
|
if len(parts) > 1: |
|
result.__package__ = parts[0] |
|
return result |
|
|
|
_hook = Mounter() |
|
|
|
|
|
class Wheel(object): |
|
""" |
|
Class to build and install from Wheel files (PEP 427). |
|
""" |
|
|
|
wheel_version = (1, 1) |
|
hash_kind = 'sha256' |
|
|
|
def __init__(self, filename=None, sign=False, verify=False): |
|
""" |
|
Initialise an instance using a (valid) filename. |
|
""" |
|
self.sign = sign |
|
self.should_verify = verify |
|
self.buildver = '' |
|
self.pyver = [PYVER] |
|
self.abi = ['none'] |
|
self.arch = ['any'] |
|
self.dirname = os.getcwd() |
|
if filename is None: |
|
self.name = 'dummy' |
|
self.version = '0.1' |
|
self._filename = self.filename |
|
else: |
|
m = NAME_VERSION_RE.match(filename) |
|
if m: |
|
info = m.groupdict('') |
|
self.name = info['nm'] |
|
|
|
self.version = info['vn'].replace('_', '-') |
|
self.buildver = info['bn'] |
|
self._filename = self.filename |
|
else: |
|
dirname, filename = os.path.split(filename) |
|
m = FILENAME_RE.match(filename) |
|
if not m: |
|
raise DistlibException('Invalid name or ' |
|
'filename: %r' % filename) |
|
if dirname: |
|
self.dirname = os.path.abspath(dirname) |
|
self._filename = filename |
|
info = m.groupdict('') |
|
self.name = info['nm'] |
|
self.version = info['vn'] |
|
self.buildver = info['bn'] |
|
self.pyver = info['py'].split('.') |
|
self.abi = info['bi'].split('.') |
|
self.arch = info['ar'].split('.') |
|
|
|
@property |
|
def filename(self): |
|
""" |
|
Build and return a filename from the various components. |
|
""" |
|
if self.buildver: |
|
buildver = '-' + self.buildver |
|
else: |
|
buildver = '' |
|
pyver = '.'.join(self.pyver) |
|
abi = '.'.join(self.abi) |
|
arch = '.'.join(self.arch) |
|
|
|
version = self.version.replace('-', '_') |
|
return '%s-%s%s-%s-%s-%s.whl' % (self.name, version, buildver, |
|
pyver, abi, arch) |
|
|
|
@property |
|
def exists(self): |
|
path = os.path.join(self.dirname, self.filename) |
|
return os.path.isfile(path) |
|
|
|
@property |
|
def tags(self): |
|
for pyver in self.pyver: |
|
for abi in self.abi: |
|
for arch in self.arch: |
|
yield pyver, abi, arch |
|
|
|
@cached_property |
|
def metadata(self): |
|
pathname = os.path.join(self.dirname, self.filename) |
|
name_ver = '%s-%s' % (self.name, self.version) |
|
info_dir = '%s.dist-info' % name_ver |
|
wrapper = codecs.getreader('utf-8') |
|
with ZipFile(pathname, 'r') as zf: |
|
wheel_metadata = self.get_wheel_metadata(zf) |
|
wv = wheel_metadata['Wheel-Version'].split('.', 1) |
|
file_version = tuple([int(i) for i in wv]) |
|
|
|
|
|
|
|
|
|
|
|
fns = [WHEEL_METADATA_FILENAME, LEGACY_METADATA_FILENAME] |
|
result = None |
|
for fn in fns: |
|
try: |
|
metadata_filename = posixpath.join(info_dir, fn) |
|
with zf.open(metadata_filename) as bf: |
|
wf = wrapper(bf) |
|
result = Metadata(fileobj=wf) |
|
if result: |
|
break |
|
except KeyError: |
|
pass |
|
if not result: |
|
raise ValueError('Invalid wheel, because metadata is ' |
|
'missing: looked in %s' % ', '.join(fns)) |
|
return result |
|
|
|
def get_wheel_metadata(self, zf): |
|
name_ver = '%s-%s' % (self.name, self.version) |
|
info_dir = '%s.dist-info' % name_ver |
|
metadata_filename = posixpath.join(info_dir, 'WHEEL') |
|
with zf.open(metadata_filename) as bf: |
|
wf = codecs.getreader('utf-8')(bf) |
|
message = message_from_file(wf) |
|
return dict(message) |
|
|
|
@cached_property |
|
def info(self): |
|
pathname = os.path.join(self.dirname, self.filename) |
|
with ZipFile(pathname, 'r') as zf: |
|
result = self.get_wheel_metadata(zf) |
|
return result |
|
|
|
def process_shebang(self, data): |
|
m = SHEBANG_RE.match(data) |
|
if m: |
|
end = m.end() |
|
shebang, data_after_shebang = data[:end], data[end:] |
|
|
|
if b'pythonw' in shebang.lower(): |
|
shebang_python = SHEBANG_PYTHONW |
|
else: |
|
shebang_python = SHEBANG_PYTHON |
|
m = SHEBANG_DETAIL_RE.match(shebang) |
|
if m: |
|
args = b' ' + m.groups()[-1] |
|
else: |
|
args = b'' |
|
shebang = shebang_python + args |
|
data = shebang + data_after_shebang |
|
else: |
|
cr = data.find(b'\r') |
|
lf = data.find(b'\n') |
|
if cr < 0 or cr > lf: |
|
term = b'\n' |
|
else: |
|
if data[cr:cr + 2] == b'\r\n': |
|
term = b'\r\n' |
|
else: |
|
term = b'\r' |
|
data = SHEBANG_PYTHON + term + data |
|
return data |
|
|
|
def get_hash(self, data, hash_kind=None): |
|
if hash_kind is None: |
|
hash_kind = self.hash_kind |
|
try: |
|
hasher = getattr(hashlib, hash_kind) |
|
except AttributeError: |
|
raise DistlibException('Unsupported hash algorithm: %r' % hash_kind) |
|
result = hasher(data).digest() |
|
result = base64.urlsafe_b64encode(result).rstrip(b'=').decode('ascii') |
|
return hash_kind, result |
|
|
|
def write_record(self, records, record_path, archive_record_path): |
|
records = list(records) |
|
records.append((archive_record_path, '', '')) |
|
with CSVWriter(record_path) as writer: |
|
for row in records: |
|
writer.writerow(row) |
|
|
|
def write_records(self, info, libdir, archive_paths): |
|
records = [] |
|
distinfo, info_dir = info |
|
hasher = getattr(hashlib, self.hash_kind) |
|
for ap, p in archive_paths: |
|
with open(p, 'rb') as f: |
|
data = f.read() |
|
digest = '%s=%s' % self.get_hash(data) |
|
size = os.path.getsize(p) |
|
records.append((ap, digest, size)) |
|
|
|
p = os.path.join(distinfo, 'RECORD') |
|
ap = to_posix(os.path.join(info_dir, 'RECORD')) |
|
self.write_record(records, p, ap) |
|
archive_paths.append((ap, p)) |
|
|
|
def build_zip(self, pathname, archive_paths): |
|
with ZipFile(pathname, 'w', zipfile.ZIP_DEFLATED) as zf: |
|
for ap, p in archive_paths: |
|
logger.debug('Wrote %s to %s in wheel', p, ap) |
|
zf.write(p, ap) |
|
|
|
def build(self, paths, tags=None, wheel_version=None): |
|
""" |
|
Build a wheel from files in specified paths, and use any specified tags |
|
when determining the name of the wheel. |
|
""" |
|
if tags is None: |
|
tags = {} |
|
|
|
libkey = list(filter(lambda o: o in paths, ('purelib', 'platlib')))[0] |
|
if libkey == 'platlib': |
|
is_pure = 'false' |
|
default_pyver = [IMPVER] |
|
default_abi = [ABI] |
|
default_arch = [ARCH] |
|
else: |
|
is_pure = 'true' |
|
default_pyver = [PYVER] |
|
default_abi = ['none'] |
|
default_arch = ['any'] |
|
|
|
self.pyver = tags.get('pyver', default_pyver) |
|
self.abi = tags.get('abi', default_abi) |
|
self.arch = tags.get('arch', default_arch) |
|
|
|
libdir = paths[libkey] |
|
|
|
name_ver = '%s-%s' % (self.name, self.version) |
|
data_dir = '%s.data' % name_ver |
|
info_dir = '%s.dist-info' % name_ver |
|
|
|
archive_paths = [] |
|
|
|
|
|
for key in ('data', 'headers', 'scripts'): |
|
if key not in paths: |
|
continue |
|
path = paths[key] |
|
if os.path.isdir(path): |
|
for root, dirs, files in os.walk(path): |
|
for fn in files: |
|
p = fsdecode(os.path.join(root, fn)) |
|
rp = os.path.relpath(p, path) |
|
ap = to_posix(os.path.join(data_dir, key, rp)) |
|
archive_paths.append((ap, p)) |
|
if key == 'scripts' and not p.endswith('.exe'): |
|
with open(p, 'rb') as f: |
|
data = f.read() |
|
data = self.process_shebang(data) |
|
with open(p, 'wb') as f: |
|
f.write(data) |
|
|
|
|
|
|
|
path = libdir |
|
distinfo = None |
|
for root, dirs, files in os.walk(path): |
|
if root == path: |
|
|
|
|
|
for i, dn in enumerate(dirs): |
|
dn = fsdecode(dn) |
|
if dn.endswith('.dist-info'): |
|
distinfo = os.path.join(root, dn) |
|
del dirs[i] |
|
break |
|
assert distinfo, '.dist-info directory expected, not found' |
|
|
|
for fn in files: |
|
|
|
if fsdecode(fn).endswith(('.pyc', '.pyo')): |
|
continue |
|
p = os.path.join(root, fn) |
|
rp = to_posix(os.path.relpath(p, path)) |
|
archive_paths.append((rp, p)) |
|
|
|
|
|
files = os.listdir(distinfo) |
|
for fn in files: |
|
if fn not in ('RECORD', 'INSTALLER', 'SHARED', 'WHEEL'): |
|
p = fsdecode(os.path.join(distinfo, fn)) |
|
ap = to_posix(os.path.join(info_dir, fn)) |
|
archive_paths.append((ap, p)) |
|
|
|
wheel_metadata = [ |
|
'Wheel-Version: %d.%d' % (wheel_version or self.wheel_version), |
|
'Generator: distlib %s' % __version__, |
|
'Root-Is-Purelib: %s' % is_pure, |
|
] |
|
for pyver, abi, arch in self.tags: |
|
wheel_metadata.append('Tag: %s-%s-%s' % (pyver, abi, arch)) |
|
p = os.path.join(distinfo, 'WHEEL') |
|
with open(p, 'w') as f: |
|
f.write('\n'.join(wheel_metadata)) |
|
ap = to_posix(os.path.join(info_dir, 'WHEEL')) |
|
archive_paths.append((ap, p)) |
|
|
|
|
|
|
|
|
|
|
|
def sorter(t): |
|
ap = t[0] |
|
n = ap.count('/') |
|
if '.dist-info' in ap: |
|
n += 10000 |
|
return (n, ap) |
|
archive_paths = sorted(archive_paths, key=sorter) |
|
|
|
|
|
|
|
self.write_records((distinfo, info_dir), libdir, archive_paths) |
|
|
|
pathname = os.path.join(self.dirname, self.filename) |
|
self.build_zip(pathname, archive_paths) |
|
return pathname |
|
|
|
def skip_entry(self, arcname): |
|
""" |
|
Determine whether an archive entry should be skipped when verifying |
|
or installing. |
|
""" |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
return arcname.endswith(('/', '/RECORD.jws')) |
|
|
|
def install(self, paths, maker, **kwargs): |
|
""" |
|
Install a wheel to the specified paths. If kwarg ``warner`` is |
|
specified, it should be a callable, which will be called with two |
|
tuples indicating the wheel version of this software and the wheel |
|
version in the file, if there is a discrepancy in the versions. |
|
This can be used to issue any warnings to raise any exceptions. |
|
If kwarg ``lib_only`` is True, only the purelib/platlib files are |
|
installed, and the headers, scripts, data and dist-info metadata are |
|
not written. If kwarg ``bytecode_hashed_invalidation`` is True, written |
|
bytecode will try to use file-hash based invalidation (PEP-552) on |
|
supported interpreter versions (CPython 2.7+). |
|
|
|
The return value is a :class:`InstalledDistribution` instance unless |
|
``options.lib_only`` is True, in which case the return value is ``None``. |
|
""" |
|
|
|
dry_run = maker.dry_run |
|
warner = kwargs.get('warner') |
|
lib_only = kwargs.get('lib_only', False) |
|
bc_hashed_invalidation = kwargs.get('bytecode_hashed_invalidation', False) |
|
|
|
pathname = os.path.join(self.dirname, self.filename) |
|
name_ver = '%s-%s' % (self.name, self.version) |
|
data_dir = '%s.data' % name_ver |
|
info_dir = '%s.dist-info' % name_ver |
|
|
|
metadata_name = posixpath.join(info_dir, LEGACY_METADATA_FILENAME) |
|
wheel_metadata_name = posixpath.join(info_dir, 'WHEEL') |
|
record_name = posixpath.join(info_dir, 'RECORD') |
|
|
|
wrapper = codecs.getreader('utf-8') |
|
|
|
with ZipFile(pathname, 'r') as zf: |
|
with zf.open(wheel_metadata_name) as bwf: |
|
wf = wrapper(bwf) |
|
message = message_from_file(wf) |
|
wv = message['Wheel-Version'].split('.', 1) |
|
file_version = tuple([int(i) for i in wv]) |
|
if (file_version != self.wheel_version) and warner: |
|
warner(self.wheel_version, file_version) |
|
|
|
if message['Root-Is-Purelib'] == 'true': |
|
libdir = paths['purelib'] |
|
else: |
|
libdir = paths['platlib'] |
|
|
|
records = {} |
|
with zf.open(record_name) as bf: |
|
with CSVReader(stream=bf) as reader: |
|
for row in reader: |
|
p = row[0] |
|
records[p] = row |
|
|
|
data_pfx = posixpath.join(data_dir, '') |
|
info_pfx = posixpath.join(info_dir, '') |
|
script_pfx = posixpath.join(data_dir, 'scripts', '') |
|
|
|
|
|
|
|
fileop = FileOperator(dry_run=dry_run) |
|
fileop.record = True |
|
|
|
bc = not sys.dont_write_bytecode |
|
|
|
outfiles = [] |
|
|
|
|
|
workdir = tempfile.mkdtemp() |
|
|
|
|
|
|
|
maker.source_dir = workdir |
|
maker.target_dir = None |
|
try: |
|
for zinfo in zf.infolist(): |
|
arcname = zinfo.filename |
|
if isinstance(arcname, text_type): |
|
u_arcname = arcname |
|
else: |
|
u_arcname = arcname.decode('utf-8') |
|
if self.skip_entry(u_arcname): |
|
continue |
|
row = records[u_arcname] |
|
if row[2] and str(zinfo.file_size) != row[2]: |
|
raise DistlibException('size mismatch for ' |
|
'%s' % u_arcname) |
|
if row[1]: |
|
kind, value = row[1].split('=', 1) |
|
with zf.open(arcname) as bf: |
|
data = bf.read() |
|
_, digest = self.get_hash(data, kind) |
|
if digest != value: |
|
raise DistlibException('digest mismatch for ' |
|
'%s' % arcname) |
|
|
|
if lib_only and u_arcname.startswith((info_pfx, data_pfx)): |
|
logger.debug('lib_only: skipping %s', u_arcname) |
|
continue |
|
is_script = (u_arcname.startswith(script_pfx) |
|
and not u_arcname.endswith('.exe')) |
|
|
|
if u_arcname.startswith(data_pfx): |
|
_, where, rp = u_arcname.split('/', 2) |
|
outfile = os.path.join(paths[where], convert_path(rp)) |
|
else: |
|
|
|
if u_arcname in (wheel_metadata_name, record_name): |
|
continue |
|
outfile = os.path.join(libdir, convert_path(u_arcname)) |
|
if not is_script: |
|
with zf.open(arcname) as bf: |
|
fileop.copy_stream(bf, outfile) |
|
|
|
|
|
|
|
|
|
if os.name == 'posix': |
|
|
|
os.chmod(outfile, (zinfo.external_attr >> 16) & 0x1FF) |
|
outfiles.append(outfile) |
|
|
|
if not dry_run and row[1]: |
|
with open(outfile, 'rb') as bf: |
|
data = bf.read() |
|
_, newdigest = self.get_hash(data, kind) |
|
if newdigest != digest: |
|
raise DistlibException('digest mismatch ' |
|
'on write for ' |
|
'%s' % outfile) |
|
if bc and outfile.endswith('.py'): |
|
try: |
|
pyc = fileop.byte_compile(outfile, |
|
hashed_invalidation=bc_hashed_invalidation) |
|
outfiles.append(pyc) |
|
except Exception: |
|
|
|
|
|
logger.warning('Byte-compilation failed', |
|
exc_info=True) |
|
else: |
|
fn = os.path.basename(convert_path(arcname)) |
|
workname = os.path.join(workdir, fn) |
|
with zf.open(arcname) as bf: |
|
fileop.copy_stream(bf, workname) |
|
|
|
dn, fn = os.path.split(outfile) |
|
maker.target_dir = dn |
|
filenames = maker.make(fn) |
|
fileop.set_executable_mode(filenames) |
|
outfiles.extend(filenames) |
|
|
|
if lib_only: |
|
logger.debug('lib_only: returning None') |
|
dist = None |
|
else: |
|
|
|
|
|
|
|
|
|
|
|
commands = None |
|
file_version = self.info['Wheel-Version'] |
|
if file_version == '1.0': |
|
|
|
ep = posixpath.join(info_dir, 'entry_points.txt') |
|
try: |
|
with zf.open(ep) as bwf: |
|
epdata = read_exports(bwf) |
|
commands = {} |
|
for key in ('console', 'gui'): |
|
k = '%s_scripts' % key |
|
if k in epdata: |
|
commands['wrap_%s' % key] = d = {} |
|
for v in epdata[k].values(): |
|
s = '%s:%s' % (v.prefix, v.suffix) |
|
if v.flags: |
|
s += ' [%s]' % ','.join(v.flags) |
|
d[v.name] = s |
|
except Exception: |
|
logger.warning('Unable to read legacy script ' |
|
'metadata, so cannot generate ' |
|
'scripts') |
|
else: |
|
try: |
|
with zf.open(metadata_name) as bwf: |
|
wf = wrapper(bwf) |
|
commands = json.load(wf).get('extensions') |
|
if commands: |
|
commands = commands.get('python.commands') |
|
except Exception: |
|
logger.warning('Unable to read JSON metadata, so ' |
|
'cannot generate scripts') |
|
if commands: |
|
console_scripts = commands.get('wrap_console', {}) |
|
gui_scripts = commands.get('wrap_gui', {}) |
|
if console_scripts or gui_scripts: |
|
script_dir = paths.get('scripts', '') |
|
if not os.path.isdir(script_dir): |
|
raise ValueError('Valid script path not ' |
|
'specified') |
|
maker.target_dir = script_dir |
|
for k, v in console_scripts.items(): |
|
script = '%s = %s' % (k, v) |
|
filenames = maker.make(script) |
|
fileop.set_executable_mode(filenames) |
|
|
|
if gui_scripts: |
|
options = {'gui': True } |
|
for k, v in gui_scripts.items(): |
|
script = '%s = %s' % (k, v) |
|
filenames = maker.make(script, options) |
|
fileop.set_executable_mode(filenames) |
|
|
|
p = os.path.join(libdir, info_dir) |
|
dist = InstalledDistribution(p) |
|
|
|
|
|
paths = dict(paths) |
|
del paths['purelib'] |
|
del paths['platlib'] |
|
paths['lib'] = libdir |
|
p = dist.write_shared_locations(paths, dry_run) |
|
if p: |
|
outfiles.append(p) |
|
|
|
|
|
dist.write_installed_files(outfiles, paths['prefix'], |
|
dry_run) |
|
return dist |
|
except Exception: |
|
logger.exception('installation failed.') |
|
fileop.rollback() |
|
raise |
|
finally: |
|
shutil.rmtree(workdir) |
|
|
|
def _get_dylib_cache(self): |
|
global cache |
|
if cache is None: |
|
|
|
base = os.path.join(get_cache_base(), str('dylib-cache'), |
|
'%s.%s' % sys.version_info[:2]) |
|
cache = Cache(base) |
|
return cache |
|
|
|
def _get_extensions(self): |
|
pathname = os.path.join(self.dirname, self.filename) |
|
name_ver = '%s-%s' % (self.name, self.version) |
|
info_dir = '%s.dist-info' % name_ver |
|
arcname = posixpath.join(info_dir, 'EXTENSIONS') |
|
wrapper = codecs.getreader('utf-8') |
|
result = [] |
|
with ZipFile(pathname, 'r') as zf: |
|
try: |
|
with zf.open(arcname) as bf: |
|
wf = wrapper(bf) |
|
extensions = json.load(wf) |
|
cache = self._get_dylib_cache() |
|
prefix = cache.prefix_to_dir(pathname) |
|
cache_base = os.path.join(cache.base, prefix) |
|
if not os.path.isdir(cache_base): |
|
os.makedirs(cache_base) |
|
for name, relpath in extensions.items(): |
|
dest = os.path.join(cache_base, convert_path(relpath)) |
|
if not os.path.exists(dest): |
|
extract = True |
|
else: |
|
file_time = os.stat(dest).st_mtime |
|
file_time = datetime.datetime.fromtimestamp(file_time) |
|
info = zf.getinfo(relpath) |
|
wheel_time = datetime.datetime(*info.date_time) |
|
extract = wheel_time > file_time |
|
if extract: |
|
zf.extract(relpath, cache_base) |
|
result.append((name, dest)) |
|
except KeyError: |
|
pass |
|
return result |
|
|
|
def is_compatible(self): |
|
""" |
|
Determine if a wheel is compatible with the running system. |
|
""" |
|
return is_compatible(self) |
|
|
|
def is_mountable(self): |
|
""" |
|
Determine if a wheel is asserted as mountable by its metadata. |
|
""" |
|
return True |
|
|
|
def mount(self, append=False): |
|
pathname = os.path.abspath(os.path.join(self.dirname, self.filename)) |
|
if not self.is_compatible(): |
|
msg = 'Wheel %s not compatible with this Python.' % pathname |
|
raise DistlibException(msg) |
|
if not self.is_mountable(): |
|
msg = 'Wheel %s is marked as not mountable.' % pathname |
|
raise DistlibException(msg) |
|
if pathname in sys.path: |
|
logger.debug('%s already in path', pathname) |
|
else: |
|
if append: |
|
sys.path.append(pathname) |
|
else: |
|
sys.path.insert(0, pathname) |
|
extensions = self._get_extensions() |
|
if extensions: |
|
if _hook not in sys.meta_path: |
|
sys.meta_path.append(_hook) |
|
_hook.add(pathname, extensions) |
|
|
|
def unmount(self): |
|
pathname = os.path.abspath(os.path.join(self.dirname, self.filename)) |
|
if pathname not in sys.path: |
|
logger.debug('%s not in path', pathname) |
|
else: |
|
sys.path.remove(pathname) |
|
if pathname in _hook.impure_wheels: |
|
_hook.remove(pathname) |
|
if not _hook.impure_wheels: |
|
if _hook in sys.meta_path: |
|
sys.meta_path.remove(_hook) |
|
|
|
def verify(self): |
|
pathname = os.path.join(self.dirname, self.filename) |
|
name_ver = '%s-%s' % (self.name, self.version) |
|
data_dir = '%s.data' % name_ver |
|
info_dir = '%s.dist-info' % name_ver |
|
|
|
metadata_name = posixpath.join(info_dir, LEGACY_METADATA_FILENAME) |
|
wheel_metadata_name = posixpath.join(info_dir, 'WHEEL') |
|
record_name = posixpath.join(info_dir, 'RECORD') |
|
|
|
wrapper = codecs.getreader('utf-8') |
|
|
|
with ZipFile(pathname, 'r') as zf: |
|
with zf.open(wheel_metadata_name) as bwf: |
|
wf = wrapper(bwf) |
|
message = message_from_file(wf) |
|
wv = message['Wheel-Version'].split('.', 1) |
|
file_version = tuple([int(i) for i in wv]) |
|
|
|
|
|
records = {} |
|
with zf.open(record_name) as bf: |
|
with CSVReader(stream=bf) as reader: |
|
for row in reader: |
|
p = row[0] |
|
records[p] = row |
|
|
|
for zinfo in zf.infolist(): |
|
arcname = zinfo.filename |
|
if isinstance(arcname, text_type): |
|
u_arcname = arcname |
|
else: |
|
u_arcname = arcname.decode('utf-8') |
|
|
|
|
|
|
|
p = u_arcname.split('/') |
|
if '..' in p: |
|
raise DistlibException('invalid entry in ' |
|
'wheel: %r' % u_arcname) |
|
|
|
if self.skip_entry(u_arcname): |
|
continue |
|
row = records[u_arcname] |
|
if row[2] and str(zinfo.file_size) != row[2]: |
|
raise DistlibException('size mismatch for ' |
|
'%s' % u_arcname) |
|
if row[1]: |
|
kind, value = row[1].split('=', 1) |
|
with zf.open(arcname) as bf: |
|
data = bf.read() |
|
_, digest = self.get_hash(data, kind) |
|
if digest != value: |
|
raise DistlibException('digest mismatch for ' |
|
'%s' % arcname) |
|
|
|
def update(self, modifier, dest_dir=None, **kwargs): |
|
""" |
|
Update the contents of a wheel in a generic way. The modifier should |
|
be a callable which expects a dictionary argument: its keys are |
|
archive-entry paths, and its values are absolute filesystem paths |
|
where the contents the corresponding archive entries can be found. The |
|
modifier is free to change the contents of the files pointed to, add |
|
new entries and remove entries, before returning. This method will |
|
extract the entire contents of the wheel to a temporary location, call |
|
the modifier, and then use the passed (and possibly updated) |
|
dictionary to write a new wheel. If ``dest_dir`` is specified, the new |
|
wheel is written there -- otherwise, the original wheel is overwritten. |
|
|
|
The modifier should return True if it updated the wheel, else False. |
|
This method returns the same value the modifier returns. |
|
""" |
|
|
|
def get_version(path_map, info_dir): |
|
version = path = None |
|
key = '%s/%s' % (info_dir, LEGACY_METADATA_FILENAME) |
|
if key not in path_map: |
|
key = '%s/PKG-INFO' % info_dir |
|
if key in path_map: |
|
path = path_map[key] |
|
version = Metadata(path=path).version |
|
return version, path |
|
|
|
def update_version(version, path): |
|
updated = None |
|
try: |
|
v = NormalizedVersion(version) |
|
i = version.find('-') |
|
if i < 0: |
|
updated = '%s+1' % version |
|
else: |
|
parts = [int(s) for s in version[i + 1:].split('.')] |
|
parts[-1] += 1 |
|
updated = '%s+%s' % (version[:i], |
|
'.'.join(str(i) for i in parts)) |
|
except UnsupportedVersionError: |
|
logger.debug('Cannot update non-compliant (PEP-440) ' |
|
'version %r', version) |
|
if updated: |
|
md = Metadata(path=path) |
|
md.version = updated |
|
legacy = path.endswith(LEGACY_METADATA_FILENAME) |
|
md.write(path=path, legacy=legacy) |
|
logger.debug('Version updated from %r to %r', version, |
|
updated) |
|
|
|
pathname = os.path.join(self.dirname, self.filename) |
|
name_ver = '%s-%s' % (self.name, self.version) |
|
info_dir = '%s.dist-info' % name_ver |
|
record_name = posixpath.join(info_dir, 'RECORD') |
|
with tempdir() as workdir: |
|
with ZipFile(pathname, 'r') as zf: |
|
path_map = {} |
|
for zinfo in zf.infolist(): |
|
arcname = zinfo.filename |
|
if isinstance(arcname, text_type): |
|
u_arcname = arcname |
|
else: |
|
u_arcname = arcname.decode('utf-8') |
|
if u_arcname == record_name: |
|
continue |
|
if '..' in u_arcname: |
|
raise DistlibException('invalid entry in ' |
|
'wheel: %r' % u_arcname) |
|
zf.extract(zinfo, workdir) |
|
path = os.path.join(workdir, convert_path(u_arcname)) |
|
path_map[u_arcname] = path |
|
|
|
|
|
original_version, _ = get_version(path_map, info_dir) |
|
|
|
modified = modifier(path_map, **kwargs) |
|
if modified: |
|
|
|
current_version, path = get_version(path_map, info_dir) |
|
if current_version and (current_version == original_version): |
|
|
|
update_version(current_version, path) |
|
|
|
if dest_dir is None: |
|
fd, newpath = tempfile.mkstemp(suffix='.whl', |
|
prefix='wheel-update-', |
|
dir=workdir) |
|
os.close(fd) |
|
else: |
|
if not os.path.isdir(dest_dir): |
|
raise DistlibException('Not a directory: %r' % dest_dir) |
|
newpath = os.path.join(dest_dir, self.filename) |
|
archive_paths = list(path_map.items()) |
|
distinfo = os.path.join(workdir, info_dir) |
|
info = distinfo, info_dir |
|
self.write_records(info, workdir, archive_paths) |
|
self.build_zip(newpath, archive_paths) |
|
if dest_dir is None: |
|
shutil.copyfile(newpath, pathname) |
|
return modified |
|
|
|
def _get_glibc_version(): |
|
import platform |
|
ver = platform.libc_ver() |
|
result = [] |
|
if ver[0] == 'glibc': |
|
for s in ver[1].split('.'): |
|
result.append(int(s) if s.isdigit() else 0) |
|
result = tuple(result) |
|
return result |
|
|
|
def compatible_tags(): |
|
""" |
|
Return (pyver, abi, arch) tuples compatible with this Python. |
|
""" |
|
versions = [VER_SUFFIX] |
|
major = VER_SUFFIX[0] |
|
for minor in range(sys.version_info[1] - 1, - 1, -1): |
|
versions.append(''.join([major, str(minor)])) |
|
|
|
abis = [] |
|
for suffix in _get_suffixes(): |
|
if suffix.startswith('.abi'): |
|
abis.append(suffix.split('.', 2)[1]) |
|
abis.sort() |
|
if ABI != 'none': |
|
abis.insert(0, ABI) |
|
abis.append('none') |
|
result = [] |
|
|
|
arches = [ARCH] |
|
if sys.platform == 'darwin': |
|
m = re.match(r'(\w+)_(\d+)_(\d+)_(\w+)$', ARCH) |
|
if m: |
|
name, major, minor, arch = m.groups() |
|
minor = int(minor) |
|
matches = [arch] |
|
if arch in ('i386', 'ppc'): |
|
matches.append('fat') |
|
if arch in ('i386', 'ppc', 'x86_64'): |
|
matches.append('fat3') |
|
if arch in ('ppc64', 'x86_64'): |
|
matches.append('fat64') |
|
if arch in ('i386', 'x86_64'): |
|
matches.append('intel') |
|
if arch in ('i386', 'x86_64', 'intel', 'ppc', 'ppc64'): |
|
matches.append('universal') |
|
while minor >= 0: |
|
for match in matches: |
|
s = '%s_%s_%s_%s' % (name, major, minor, match) |
|
if s != ARCH: |
|
arches.append(s) |
|
minor -= 1 |
|
|
|
|
|
for abi in abis: |
|
for arch in arches: |
|
result.append((''.join((IMP_PREFIX, versions[0])), abi, arch)) |
|
|
|
if abi != 'none' and sys.platform.startswith('linux'): |
|
arch = arch.replace('linux_', '') |
|
parts = _get_glibc_version() |
|
if len(parts) == 2: |
|
if parts >= (2, 5): |
|
result.append((''.join((IMP_PREFIX, versions[0])), abi, |
|
'manylinux1_%s' % arch)) |
|
if parts >= (2, 12): |
|
result.append((''.join((IMP_PREFIX, versions[0])), abi, |
|
'manylinux2010_%s' % arch)) |
|
if parts >= (2, 17): |
|
result.append((''.join((IMP_PREFIX, versions[0])), abi, |
|
'manylinux2014_%s' % arch)) |
|
result.append((''.join((IMP_PREFIX, versions[0])), abi, |
|
'manylinux_%s_%s_%s' % (parts[0], parts[1], |
|
arch))) |
|
|
|
|
|
for i, version in enumerate(versions): |
|
result.append((''.join((IMP_PREFIX, version)), 'none', 'any')) |
|
if i == 0: |
|
result.append((''.join((IMP_PREFIX, version[0])), 'none', 'any')) |
|
|
|
|
|
for i, version in enumerate(versions): |
|
result.append((''.join(('py', version)), 'none', 'any')) |
|
if i == 0: |
|
result.append((''.join(('py', version[0])), 'none', 'any')) |
|
|
|
return set(result) |
|
|
|
|
|
COMPATIBLE_TAGS = compatible_tags() |
|
|
|
del compatible_tags |
|
|
|
|
|
def is_compatible(wheel, tags=None): |
|
if not isinstance(wheel, Wheel): |
|
wheel = Wheel(wheel) |
|
result = False |
|
if tags is None: |
|
tags = COMPATIBLE_TAGS |
|
for ver, abi, arch in tags: |
|
if ver in wheel.pyver and abi in wheel.abi and arch in wheel.arch: |
|
result = True |
|
break |
|
return result |
|
|