2019-10-20 15:14:50 +02:00
|
|
|
"""Support for installing and building the "wheel" binary package format.
|
2012-10-02 07:50:24 +02:00
|
|
|
"""
|
2019-07-20 08:36:59 +02:00
|
|
|
|
|
|
|
# The following comment should be removed at some point in the future.
|
|
|
|
# mypy: strict-optional=False
|
|
|
|
|
2014-08-31 01:52:28 +02:00
|
|
|
from __future__ import absolute_import
|
2012-10-02 07:50:24 +02:00
|
|
|
|
2017-10-02 18:54:37 +02:00
|
|
|
import collections
|
2013-11-22 03:59:31 +01:00
|
|
|
import compileall
|
2012-10-02 07:50:24 +02:00
|
|
|
import csv
|
2014-08-31 01:52:28 +02:00
|
|
|
import logging
|
2015-06-01 23:24:11 +02:00
|
|
|
import os.path
|
2013-04-02 07:44:46 +02:00
|
|
|
import re
|
2013-04-05 23:21:11 +02:00
|
|
|
import shutil
|
2014-12-22 19:47:37 +01:00
|
|
|
import stat
|
2013-04-05 23:21:11 +02:00
|
|
|
import sys
|
2015-02-28 11:10:42 +01:00
|
|
|
import warnings
|
2013-04-05 23:21:11 +02:00
|
|
|
from base64 import urlsafe_b64encode
|
2014-02-17 00:01:32 +01:00
|
|
|
from email.parser import Parser
|
2017-06-13 14:17:00 +02:00
|
|
|
|
2018-01-23 17:02:00 +01:00
|
|
|
from pip._vendor import pkg_resources
|
2017-06-13 14:17:00 +02:00
|
|
|
from pip._vendor.distlib.scripts import ScriptMaker
|
2019-07-22 04:49:51 +02:00
|
|
|
from pip._vendor.distlib.util import get_export_entry
|
2019-12-21 17:14:50 +01:00
|
|
|
from pip._vendor.packaging.utils import canonicalize_name
|
2019-12-31 19:00:57 +01:00
|
|
|
from pip._vendor.six import StringIO, ensure_str
|
2013-04-05 23:21:11 +02:00
|
|
|
|
2019-12-01 22:55:47 +01:00
|
|
|
from pip._internal.exceptions import InstallationError, UnsupportedWheel
|
2019-10-12 03:49:39 +02:00
|
|
|
from pip._internal.locations import get_major_minor_version
|
2019-11-03 14:24:11 +01:00
|
|
|
from pip._internal.utils.misc import captured_stdout, ensure_dir, hash_file
|
2019-12-14 17:10:36 +01:00
|
|
|
from pip._internal.utils.temp_dir import TempDirectory
|
2017-10-02 18:54:37 +02:00
|
|
|
from pip._internal.utils.typing import MYPY_CHECK_RUNNING
|
2019-12-14 17:10:36 +01:00
|
|
|
from pip._internal.utils.unpacking import unpack_file
|
2016-11-29 19:33:30 +01:00
|
|
|
|
2017-10-02 18:54:37 +02:00
|
|
|
if MYPY_CHECK_RUNNING:
|
2019-12-31 18:28:26 +01:00
|
|
|
from email.message import Message
|
2019-02-22 12:17:07 +01:00
|
|
|
from typing import (
|
2019-11-07 03:10:11 +01:00
|
|
|
Dict, List, Optional, Sequence, Tuple, IO, Text, Any,
|
2019-11-03 14:07:30 +01:00
|
|
|
Iterable, Callable, Set,
|
2018-12-16 10:16:39 +01:00
|
|
|
)
|
2019-11-07 03:10:11 +01:00
|
|
|
|
|
|
|
from pip._internal.models.scheme import Scheme
|
2018-12-16 10:16:39 +01:00
|
|
|
|
2019-01-24 03:44:54 +01:00
|
|
|
InstalledCSVRow = Tuple[str, ...]
|
2018-12-16 10:16:39 +01:00
|
|
|
|
2016-11-29 19:33:30 +01:00
|
|
|
|
2014-02-15 07:21:22 +01:00
|
|
|
VERSION_COMPATIBLE = (1, 0)
|
|
|
|
|
2013-04-02 07:44:46 +02:00
|
|
|
|
2014-08-31 01:52:28 +02:00
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
|
|
|
2019-01-24 03:44:54 +01:00
|
|
|
def normpath(src, p):
|
2019-09-28 17:17:53 +02:00
|
|
|
# type: (str, str) -> str
|
2019-01-24 03:44:54 +01:00
|
|
|
return os.path.relpath(src, p).replace(os.path.sep, '/')
|
|
|
|
|
|
|
|
|
2019-06-26 11:44:43 +02:00
|
|
|
def rehash(path, blocksize=1 << 20):
|
|
|
|
# type: (str, int) -> Tuple[str, str]
|
|
|
|
"""Return (encoded_digest, length) for path using hashlib.sha256()"""
|
|
|
|
h, length = hash_file(path, blocksize)
|
2014-01-27 15:07:10 +01:00
|
|
|
digest = 'sha256=' + urlsafe_b64encode(
|
|
|
|
h.digest()
|
|
|
|
).decode('latin1').rstrip('=')
|
2018-12-16 10:16:39 +01:00
|
|
|
# unicode/str python2 issues
|
|
|
|
return (digest, str(length)) # type: ignore
|
2012-10-02 07:50:24 +02:00
|
|
|
|
2014-01-27 15:07:10 +01:00
|
|
|
|
2012-10-02 07:50:24 +02:00
|
|
|
def open_for_csv(name, mode):
|
2019-11-12 19:08:48 +01:00
|
|
|
# type: (str, Text) -> IO[Any]
|
2012-10-02 07:50:24 +02:00
|
|
|
if sys.version_info[0] < 3:
|
2018-12-16 10:16:39 +01:00
|
|
|
nl = {} # type: Dict[str, Any]
|
2012-10-02 07:50:24 +02:00
|
|
|
bin = 'b'
|
|
|
|
else:
|
2018-12-16 10:16:39 +01:00
|
|
|
nl = {'newline': ''} # type: Dict[str, Any]
|
2012-10-02 07:50:24 +02:00
|
|
|
bin = ''
|
|
|
|
return open(name, mode + bin, **nl)
|
|
|
|
|
2014-01-27 15:07:10 +01:00
|
|
|
|
2012-10-02 07:50:24 +02:00
|
|
|
def fix_script(path):
|
2018-12-16 10:16:39 +01:00
|
|
|
# type: (str) -> Optional[bool]
|
2012-10-02 07:50:24 +02:00
|
|
|
"""Replace #!python with #!/path/to/python
|
2019-10-20 15:14:50 +02:00
|
|
|
Return True if file was changed.
|
|
|
|
"""
|
2012-10-02 07:50:24 +02:00
|
|
|
# XXX RECORD hashes will need to be updated
|
|
|
|
if os.path.isfile(path):
|
2014-10-02 23:45:37 +02:00
|
|
|
with open(path, 'rb') as script:
|
2012-10-02 07:50:24 +02:00
|
|
|
firstline = script.readline()
|
2014-06-27 04:44:42 +02:00
|
|
|
if not firstline.startswith(b'#!python'):
|
2012-10-02 07:50:24 +02:00
|
|
|
return False
|
|
|
|
exename = sys.executable.encode(sys.getfilesystemencoding())
|
2014-06-27 04:44:42 +02:00
|
|
|
firstline = b'#!' + exename + os.linesep.encode("ascii")
|
2012-10-02 07:50:24 +02:00
|
|
|
rest = script.read()
|
2014-10-02 23:45:37 +02:00
|
|
|
with open(path, 'wb') as script:
|
2012-10-02 07:50:24 +02:00
|
|
|
script.write(firstline)
|
|
|
|
script.write(rest)
|
|
|
|
return True
|
2018-12-16 10:16:39 +01:00
|
|
|
return None
|
2013-07-06 07:20:09 +02:00
|
|
|
|
2017-05-19 12:10:57 +02:00
|
|
|
|
2019-12-31 18:45:13 +01:00
|
|
|
def wheel_root_is_purelib(metadata):
|
|
|
|
# type: (Message) -> bool
|
|
|
|
return metadata.get("Root-Is-Purelib", "").lower() == "true"
|
|
|
|
|
|
|
|
|
2013-10-23 16:57:12 +02:00
|
|
|
def get_entrypoints(filename):
|
2018-12-16 10:16:39 +01:00
|
|
|
# type: (str) -> Tuple[Dict[str, str], Dict[str, str]]
|
2013-10-23 16:57:12 +02:00
|
|
|
if not os.path.exists(filename):
|
|
|
|
return {}, {}
|
2013-11-08 12:32:35 +01:00
|
|
|
|
2013-11-19 14:23:15 +01:00
|
|
|
# This is done because you can pass a string to entry_points wrappers which
|
|
|
|
# means that they may or may not be valid INI files. The attempt here is to
|
|
|
|
# strip leading and trailing whitespace in order to make them valid INI
|
|
|
|
# files.
|
2013-11-08 12:32:35 +01:00
|
|
|
with open(filename) as fp:
|
|
|
|
data = StringIO()
|
|
|
|
for line in fp:
|
|
|
|
data.write(line.strip())
|
|
|
|
data.write("\n")
|
|
|
|
data.seek(0)
|
|
|
|
|
2016-11-19 00:27:16 +01:00
|
|
|
# get the entry points and then the script names
|
|
|
|
entry_points = pkg_resources.EntryPoint.parse_map(data)
|
|
|
|
console = entry_points.get('console_scripts', {})
|
|
|
|
gui = entry_points.get('gui_scripts', {})
|
2013-11-08 12:32:35 +01:00
|
|
|
|
2016-11-19 00:27:16 +01:00
|
|
|
def _split_ep(s):
|
2019-09-28 17:17:53 +02:00
|
|
|
# type: (pkg_resources.EntryPoint) -> Tuple[str, str]
|
2019-10-20 15:14:50 +02:00
|
|
|
"""get the string representation of EntryPoint,
|
|
|
|
remove space and split on '='
|
|
|
|
"""
|
2019-09-28 17:17:53 +02:00
|
|
|
split_parts = str(s).replace(" ", "").split("=")
|
|
|
|
return split_parts[0], split_parts[1]
|
2016-11-19 00:27:16 +01:00
|
|
|
|
|
|
|
# convert the EntryPoint objects into strings with module:function
|
|
|
|
console = dict(_split_ep(v) for v in console.values())
|
|
|
|
gui = dict(_split_ep(v) for v in gui.values())
|
2013-10-23 16:57:12 +02:00
|
|
|
return console, gui
|
|
|
|
|
2013-11-08 12:32:35 +01:00
|
|
|
|
2017-10-02 18:54:37 +02:00
|
|
|
def message_about_scripts_not_on_PATH(scripts):
|
2018-12-16 10:16:39 +01:00
|
|
|
# type: (Sequence[str]) -> Optional[str]
|
2017-10-02 18:54:37 +02:00
|
|
|
"""Determine if any scripts are not on PATH and format a warning.
|
|
|
|
Returns a warning message if one or more scripts are not on PATH,
|
|
|
|
otherwise None.
|
|
|
|
"""
|
|
|
|
if not scripts:
|
|
|
|
return None
|
|
|
|
|
|
|
|
# Group scripts by the path they were installed in
|
2019-09-28 16:42:27 +02:00
|
|
|
grouped_by_dir = collections.defaultdict(set) # type: Dict[str, Set[str]]
|
2017-10-02 18:54:37 +02:00
|
|
|
for destfile in scripts:
|
|
|
|
parent_dir = os.path.dirname(destfile)
|
|
|
|
script_name = os.path.basename(destfile)
|
|
|
|
grouped_by_dir[parent_dir].add(script_name)
|
|
|
|
|
2018-04-04 09:51:57 +02:00
|
|
|
# We don't want to warn for directories that are on PATH.
|
2018-04-07 13:55:01 +02:00
|
|
|
not_warn_dirs = [
|
2018-04-19 03:10:45 +02:00
|
|
|
os.path.normcase(i).rstrip(os.sep) for i in
|
2018-07-02 14:14:28 +02:00
|
|
|
os.environ.get("PATH", "").split(os.pathsep)
|
2018-04-07 13:55:01 +02:00
|
|
|
]
|
2018-04-04 09:51:57 +02:00
|
|
|
# If an executable sits with sys.executable, we don't warn for it.
|
|
|
|
# This covers the case of venv invocations without activating the venv.
|
2018-04-06 10:54:16 +02:00
|
|
|
not_warn_dirs.append(os.path.normcase(os.path.dirname(sys.executable)))
|
2017-10-02 18:54:37 +02:00
|
|
|
warn_for = {
|
|
|
|
parent_dir: scripts for parent_dir, scripts in grouped_by_dir.items()
|
2018-04-06 10:54:16 +02:00
|
|
|
if os.path.normcase(parent_dir) not in not_warn_dirs
|
2019-09-28 16:42:27 +02:00
|
|
|
} # type: Dict[str, Set[str]]
|
2017-10-02 18:54:37 +02:00
|
|
|
if not warn_for:
|
|
|
|
return None
|
|
|
|
|
|
|
|
# Format a message
|
|
|
|
msg_lines = []
|
2019-09-28 16:42:27 +02:00
|
|
|
for parent_dir, dir_scripts in warn_for.items():
|
|
|
|
sorted_scripts = sorted(dir_scripts) # type: List[str]
|
2019-02-21 22:40:29 +01:00
|
|
|
if len(sorted_scripts) == 1:
|
|
|
|
start_text = "script {} is".format(sorted_scripts[0])
|
2017-10-02 18:54:37 +02:00
|
|
|
else:
|
|
|
|
start_text = "scripts {} are".format(
|
2019-02-21 22:40:29 +01:00
|
|
|
", ".join(sorted_scripts[:-1]) + " and " + sorted_scripts[-1]
|
2017-10-02 18:54:37 +02:00
|
|
|
)
|
|
|
|
|
|
|
|
msg_lines.append(
|
|
|
|
"The {} installed in '{}' which is not on PATH."
|
|
|
|
.format(start_text, parent_dir)
|
|
|
|
)
|
|
|
|
|
|
|
|
last_line_fmt = (
|
|
|
|
"Consider adding {} to PATH or, if you prefer "
|
|
|
|
"to suppress this warning, use --no-warn-script-location."
|
|
|
|
)
|
|
|
|
if len(msg_lines) == 1:
|
|
|
|
msg_lines.append(last_line_fmt.format("this directory"))
|
|
|
|
else:
|
|
|
|
msg_lines.append(last_line_fmt.format("these directories"))
|
|
|
|
|
2019-11-16 00:16:41 +01:00
|
|
|
# Add a note if any directory starts with ~
|
|
|
|
warn_for_tilde = any(
|
|
|
|
i[0] == "~" for i in os.environ.get("PATH", "").split(os.pathsep) if i
|
|
|
|
)
|
|
|
|
if warn_for_tilde:
|
|
|
|
tilde_warning_msg = (
|
|
|
|
"NOTE: The current PATH contains path(s) starting with `~`, "
|
|
|
|
"which may not be expanded by all applications."
|
|
|
|
)
|
|
|
|
msg_lines.append(tilde_warning_msg)
|
|
|
|
|
2017-10-02 18:54:37 +02:00
|
|
|
# Returns the formatted multiline message
|
|
|
|
return "\n".join(msg_lines)
|
|
|
|
|
|
|
|
|
2018-10-24 18:19:58 +02:00
|
|
|
def sorted_outrows(outrows):
|
2018-12-16 10:16:39 +01:00
|
|
|
# type: (Iterable[InstalledCSVRow]) -> List[InstalledCSVRow]
|
2019-10-20 15:14:50 +02:00
|
|
|
"""Return the given rows of a RECORD file in sorted order.
|
2018-10-24 18:19:58 +02:00
|
|
|
|
|
|
|
Each row is a 3-tuple (path, hash, size) and corresponds to a record of
|
|
|
|
a RECORD file (see PEP 376 and PEP 427 for details). For the rows
|
|
|
|
passed to this function, the size can be an integer as an int or string,
|
|
|
|
or the empty string.
|
|
|
|
"""
|
|
|
|
# Normally, there should only be one row per path, in which case the
|
|
|
|
# second and third elements don't come into play when sorting.
|
|
|
|
# However, in cases in the wild where a path might happen to occur twice,
|
|
|
|
# we don't want the sort operation to trigger an error (but still want
|
|
|
|
# determinism). Since the third element can be an int or string, we
|
|
|
|
# coerce each element to a string to avoid a TypeError in this case.
|
|
|
|
# For additional background, see--
|
|
|
|
# https://github.com/pypa/pip/issues/5868
|
|
|
|
return sorted(outrows, key=lambda row: tuple(str(x) for x in row))
|
|
|
|
|
|
|
|
|
2019-01-24 03:44:54 +01:00
|
|
|
def get_csv_rows_for_installed(
|
|
|
|
old_csv_rows, # type: Iterable[List[str]]
|
|
|
|
installed, # type: Dict[str, str]
|
2019-11-12 19:08:48 +01:00
|
|
|
changed, # type: Set[str]
|
2019-01-24 03:44:54 +01:00
|
|
|
generated, # type: List[str]
|
2019-01-24 04:16:10 +01:00
|
|
|
lib_dir, # type: str
|
2019-01-24 03:44:54 +01:00
|
|
|
):
|
|
|
|
# type: (...) -> List[InstalledCSVRow]
|
2019-02-15 11:26:18 +01:00
|
|
|
"""
|
|
|
|
:param installed: A map from archive RECORD path to installation RECORD
|
|
|
|
path.
|
|
|
|
"""
|
2019-01-24 03:44:54 +01:00
|
|
|
installed_rows = [] # type: List[InstalledCSVRow]
|
2019-01-24 04:16:10 +01:00
|
|
|
for row in old_csv_rows:
|
|
|
|
if len(row) > 3:
|
|
|
|
logger.warning(
|
|
|
|
'RECORD line has more than three elements: {}'.format(row)
|
|
|
|
)
|
2019-02-15 11:26:18 +01:00
|
|
|
# Make a copy because we are mutating the row.
|
|
|
|
row = list(row)
|
|
|
|
old_path = row[0]
|
|
|
|
new_path = installed.pop(old_path, old_path)
|
|
|
|
row[0] = new_path
|
|
|
|
if new_path in changed:
|
|
|
|
digest, length = rehash(new_path)
|
2019-01-24 04:16:10 +01:00
|
|
|
row[1] = digest
|
|
|
|
row[2] = length
|
|
|
|
installed_rows.append(tuple(row))
|
2019-01-24 03:44:54 +01:00
|
|
|
for f in generated:
|
|
|
|
digest, length = rehash(f)
|
|
|
|
installed_rows.append((normpath(f, lib_dir), digest, str(length)))
|
|
|
|
for f in installed:
|
|
|
|
installed_rows.append((installed[f], '', ''))
|
|
|
|
return installed_rows
|
|
|
|
|
|
|
|
|
2019-07-22 04:49:51 +02:00
|
|
|
class MissingCallableSuffix(Exception):
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
2019-09-07 15:59:41 +02:00
|
|
|
def _raise_for_invalid_entrypoint(specification):
|
2019-09-28 17:17:53 +02:00
|
|
|
# type: (str) -> None
|
2019-07-22 04:49:51 +02:00
|
|
|
entry = get_export_entry(specification)
|
|
|
|
if entry is not None and entry.suffix is None:
|
|
|
|
raise MissingCallableSuffix(str(entry))
|
|
|
|
|
|
|
|
|
|
|
|
class PipScriptMaker(ScriptMaker):
|
|
|
|
def make(self, specification, options=None):
|
2019-09-28 17:17:53 +02:00
|
|
|
# type: (str, Dict[str, Any]) -> List[str]
|
2019-09-07 15:59:41 +02:00
|
|
|
_raise_for_invalid_entrypoint(specification)
|
2019-07-22 04:49:51 +02:00
|
|
|
return super(PipScriptMaker, self).make(specification, options)
|
|
|
|
|
|
|
|
|
2019-10-12 03:31:35 +02:00
|
|
|
def install_unpacked_wheel(
|
2018-12-16 10:16:39 +01:00
|
|
|
name, # type: str
|
|
|
|
wheeldir, # type: str
|
2019-11-07 03:10:11 +01:00
|
|
|
scheme, # type: Scheme
|
2019-10-12 04:24:50 +02:00
|
|
|
req_description, # type: str
|
2018-12-16 10:16:39 +01:00
|
|
|
pycompile=True, # type: bool
|
|
|
|
warn_script_location=True # type: bool
|
|
|
|
):
|
|
|
|
# type: (...) -> None
|
2019-10-12 04:24:50 +02:00
|
|
|
"""Install a wheel.
|
|
|
|
|
|
|
|
:param name: Name of the project to install
|
|
|
|
:param wheeldir: Base directory of the unpacked wheel
|
|
|
|
:param scheme: Distutils scheme dictating the install directories
|
|
|
|
:param req_description: String used in place of the requirement, for
|
|
|
|
logging
|
|
|
|
:param pycompile: Whether to byte-compile installed Python files
|
|
|
|
:param warn_script_location: Whether to check that scripts are installed
|
|
|
|
into a directory on PATH
|
2019-12-21 17:14:50 +01:00
|
|
|
:raises UnsupportedWheel:
|
|
|
|
* when the directory holds an unpacked wheel with incompatible
|
|
|
|
Wheel-Version
|
|
|
|
* when the .dist-info dir does not match the wheel
|
2019-10-12 04:24:50 +02:00
|
|
|
"""
|
2018-09-19 13:00:33 +02:00
|
|
|
# TODO: Investigate and break this up.
|
|
|
|
# TODO: Look into moving this into a dedicated class for representing an
|
|
|
|
# installation.
|
|
|
|
|
2019-12-31 18:12:17 +01:00
|
|
|
source = wheeldir.rstrip(os.path.sep) + os.path.sep
|
|
|
|
|
2019-12-31 16:17:41 +01:00
|
|
|
try:
|
2019-12-31 19:44:20 +01:00
|
|
|
info_dir = wheel_dist_info_dir(source, name)
|
2019-12-31 20:00:16 +01:00
|
|
|
metadata = wheel_metadata(source, info_dir)
|
2019-12-31 18:28:26 +01:00
|
|
|
version = wheel_version(metadata)
|
2019-12-31 16:17:41 +01:00
|
|
|
except UnsupportedWheel as e:
|
|
|
|
raise UnsupportedWheel(
|
|
|
|
"{} has an invalid wheel, {}".format(name, str(e))
|
|
|
|
)
|
|
|
|
|
2019-12-01 23:20:34 +01:00
|
|
|
check_compatibility(version, name)
|
|
|
|
|
2019-12-31 18:45:13 +01:00
|
|
|
if wheel_root_is_purelib(metadata):
|
2019-11-07 03:10:11 +01:00
|
|
|
lib_dir = scheme.purelib
|
2013-06-30 19:58:54 +02:00
|
|
|
else:
|
2019-11-07 03:10:11 +01:00
|
|
|
lib_dir = scheme.platlib
|
2012-10-02 07:50:24 +02:00
|
|
|
|
2019-12-31 19:23:48 +01:00
|
|
|
subdirs = os.listdir(source)
|
2019-12-30 16:59:23 +01:00
|
|
|
data_dirs = [s for s in subdirs if s.endswith('.data')]
|
2013-11-01 18:28:35 +01:00
|
|
|
|
|
|
|
# Record details of the files moved
|
|
|
|
# installed = files copied from the wheel to the destination
|
|
|
|
# changed = files changed while installing (scripts #! line typically)
|
|
|
|
# generated = files newly generated during the install (script wrappers)
|
2018-12-16 10:16:39 +01:00
|
|
|
installed = {} # type: Dict[str, str]
|
2012-10-02 07:50:24 +02:00
|
|
|
changed = set()
|
2018-12-16 10:16:39 +01:00
|
|
|
generated = [] # type: List[str]
|
2012-10-02 07:50:24 +02:00
|
|
|
|
2013-11-22 03:59:31 +01:00
|
|
|
# Compile all of the pyc files that we're going to be installing
|
|
|
|
if pycompile:
|
2014-08-21 15:40:02 +02:00
|
|
|
with captured_stdout() as stdout:
|
2015-02-28 11:10:42 +01:00
|
|
|
with warnings.catch_warnings():
|
|
|
|
warnings.filterwarnings('ignore')
|
|
|
|
compileall.compile_dir(source, force=True, quiet=True)
|
2015-02-28 00:17:21 +01:00
|
|
|
logger.debug(stdout.getvalue())
|
2013-11-22 03:59:31 +01:00
|
|
|
|
2012-10-02 07:50:24 +02:00
|
|
|
def record_installed(srcfile, destfile, modified=False):
|
2019-09-28 17:17:53 +02:00
|
|
|
# type: (str, str, bool) -> None
|
2012-10-02 07:50:24 +02:00
|
|
|
"""Map archive RECORD paths to installation RECORD paths."""
|
|
|
|
oldpath = normpath(srcfile, wheeldir)
|
2013-06-30 19:58:54 +02:00
|
|
|
newpath = normpath(destfile, lib_dir)
|
2012-10-02 07:50:24 +02:00
|
|
|
installed[oldpath] = newpath
|
|
|
|
if modified:
|
|
|
|
changed.add(destfile)
|
|
|
|
|
2019-09-28 17:17:53 +02:00
|
|
|
def clobber(
|
|
|
|
source, # type: str
|
|
|
|
dest, # type: str
|
|
|
|
is_base, # type: bool
|
|
|
|
fixer=None, # type: Optional[Callable[[str], Any]]
|
|
|
|
filter=None # type: Optional[Callable[[str], bool]]
|
|
|
|
):
|
|
|
|
# type: (...) -> None
|
2015-03-31 03:40:30 +02:00
|
|
|
ensure_dir(dest) # common for the 'include' path
|
2013-05-24 04:42:03 +02:00
|
|
|
|
2012-10-02 07:50:24 +02:00
|
|
|
for dir, subdirs, files in os.walk(source):
|
|
|
|
basedir = dir[len(source):].lstrip(os.path.sep)
|
2014-04-22 08:07:25 +02:00
|
|
|
destdir = os.path.join(dest, basedir)
|
2019-12-30 16:59:23 +01:00
|
|
|
if is_base and basedir == '':
|
|
|
|
subdirs[:] = [s for s in subdirs if not s.endswith('.data')]
|
2012-10-02 07:50:24 +02:00
|
|
|
for f in files:
|
2013-10-23 16:57:12 +02:00
|
|
|
# Skip unwanted files
|
|
|
|
if filter and filter(f):
|
|
|
|
continue
|
2012-10-02 07:50:24 +02:00
|
|
|
srcfile = os.path.join(dir, f)
|
|
|
|
destfile = os.path.join(dest, basedir, f)
|
2014-04-23 01:09:04 +02:00
|
|
|
# directory creation is lazy and after the file filtering above
|
2014-04-23 22:08:07 +02:00
|
|
|
# to ensure we don't install empty dirs; empty dirs can't be
|
|
|
|
# uninstalled.
|
2015-03-31 03:40:30 +02:00
|
|
|
ensure_dir(destdir)
|
2014-11-20 17:17:42 +01:00
|
|
|
|
2018-05-14 19:13:35 +02:00
|
|
|
# copyfile (called below) truncates the destination if it
|
|
|
|
# exists and then writes the new contents. This is fine in most
|
|
|
|
# cases, but can cause a segfault if pip has loaded a shared
|
|
|
|
# object (e.g. from pyopenssl through its vendored urllib3)
|
|
|
|
# Since the shared object is mmap'd an attempt to call a
|
|
|
|
# symbol in it will then cause a segfault. Unlinking the file
|
|
|
|
# allows writing of new contents while allowing the process to
|
|
|
|
# continue to use the old copy.
|
|
|
|
if os.path.exists(destfile):
|
|
|
|
os.unlink(destfile)
|
|
|
|
|
2014-11-20 17:17:42 +01:00
|
|
|
# We use copyfile (not move, copy, or copy2) to be extra sure
|
|
|
|
# that we are not moving directories over (copyfile fails for
|
|
|
|
# directories) as well as to ensure that we are not copying
|
|
|
|
# over any metadata because we want more control over what
|
|
|
|
# metadata we actually copy over.
|
|
|
|
shutil.copyfile(srcfile, destfile)
|
|
|
|
|
|
|
|
# Copy over the metadata for the file, currently this only
|
|
|
|
# includes the atime and mtime.
|
|
|
|
st = os.stat(srcfile)
|
|
|
|
if hasattr(os, "utime"):
|
|
|
|
os.utime(destfile, (st.st_atime, st.st_mtime))
|
|
|
|
|
2014-12-22 19:47:37 +01:00
|
|
|
# If our file is executable, then make our destination file
|
|
|
|
# executable.
|
|
|
|
if os.access(srcfile, os.X_OK):
|
|
|
|
st = os.stat(srcfile)
|
|
|
|
permissions = (
|
|
|
|
st.st_mode | stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH
|
|
|
|
)
|
|
|
|
os.chmod(destfile, permissions)
|
|
|
|
|
2012-10-02 07:50:24 +02:00
|
|
|
changed = False
|
|
|
|
if fixer:
|
|
|
|
changed = fixer(destfile)
|
|
|
|
record_installed(srcfile, destfile, changed)
|
|
|
|
|
2013-06-30 19:58:54 +02:00
|
|
|
clobber(source, lib_dir, True)
|
2012-10-02 07:50:24 +02:00
|
|
|
|
2019-12-30 16:59:23 +01:00
|
|
|
dest_info_dir = os.path.join(lib_dir, info_dir)
|
|
|
|
|
2013-10-23 16:57:12 +02:00
|
|
|
# Get the defined entry points
|
2019-12-30 16:59:23 +01:00
|
|
|
ep_file = os.path.join(dest_info_dir, 'entry_points.txt')
|
2013-10-23 16:57:12 +02:00
|
|
|
console, gui = get_entrypoints(ep_file)
|
|
|
|
|
|
|
|
def is_entrypoint_wrapper(name):
|
2019-09-28 17:17:53 +02:00
|
|
|
# type: (str) -> bool
|
2013-10-23 16:57:12 +02:00
|
|
|
# EP, EP.exe and EP-script.py are scripts generated for
|
|
|
|
# entry point EP by setuptools
|
|
|
|
if name.lower().endswith('.exe'):
|
|
|
|
matchname = name[:-4]
|
|
|
|
elif name.lower().endswith('-script.py'):
|
|
|
|
matchname = name[:-10]
|
2013-11-02 18:04:24 +01:00
|
|
|
elif name.lower().endswith(".pya"):
|
|
|
|
matchname = name[:-4]
|
2013-10-23 16:57:12 +02:00
|
|
|
else:
|
|
|
|
matchname = name
|
|
|
|
# Ignore setuptools-generated scripts
|
|
|
|
return (matchname in console or matchname in gui)
|
|
|
|
|
2012-10-02 07:50:24 +02:00
|
|
|
for datadir in data_dirs:
|
|
|
|
fixer = None
|
2013-10-23 16:57:12 +02:00
|
|
|
filter = None
|
2012-10-02 07:50:24 +02:00
|
|
|
for subdir in os.listdir(os.path.join(wheeldir, datadir)):
|
|
|
|
fixer = None
|
|
|
|
if subdir == 'scripts':
|
|
|
|
fixer = fix_script
|
2013-10-23 16:57:12 +02:00
|
|
|
filter = is_entrypoint_wrapper
|
2012-10-02 07:50:24 +02:00
|
|
|
source = os.path.join(wheeldir, datadir, subdir)
|
2019-11-07 03:10:11 +01:00
|
|
|
dest = getattr(scheme, subdir)
|
2013-10-23 16:57:12 +02:00
|
|
|
clobber(source, dest, False, fixer=fixer, filter=filter)
|
|
|
|
|
2019-11-07 03:10:11 +01:00
|
|
|
maker = PipScriptMaker(None, scheme.scripts)
|
2014-06-07 05:20:42 +02:00
|
|
|
|
2014-06-07 05:07:43 +02:00
|
|
|
# Ensure old scripts are overwritten.
|
|
|
|
# See https://github.com/pypa/pip/issues/1800
|
|
|
|
maker.clobber = True
|
2013-11-03 02:00:02 +01:00
|
|
|
|
|
|
|
# Ensure we don't generate any variants for scripts because this is almost
|
|
|
|
# never what somebody wants.
|
|
|
|
# See https://bitbucket.org/pypa/distlib/issue/35/
|
2017-12-15 06:58:30 +01:00
|
|
|
maker.variants = {''}
|
2013-10-23 16:57:12 +02:00
|
|
|
|
2013-11-02 17:03:29 +01:00
|
|
|
# This is required because otherwise distlib creates scripts that are not
|
|
|
|
# executable.
|
|
|
|
# See https://bitbucket.org/pypa/distlib/issue/32/
|
|
|
|
maker.set_mode = True
|
|
|
|
|
2019-07-22 04:49:51 +02:00
|
|
|
scripts_to_generate = []
|
2013-11-03 02:00:02 +01:00
|
|
|
|
2013-10-23 16:57:12 +02:00
|
|
|
# Special case pip and setuptools to generate versioned wrappers
|
2013-11-01 18:28:35 +01:00
|
|
|
#
|
|
|
|
# The issue is that some projects (specifically, pip and setuptools) use
|
|
|
|
# code in setup.py to create "versioned" entry points - pip2.7 on Python
|
|
|
|
# 2.7, pip3.3 on Python 3.3, etc. But these entry points are baked into
|
|
|
|
# the wheel metadata at build time, and so if the wheel is installed with
|
|
|
|
# a *different* version of Python the entry points will be wrong. The
|
|
|
|
# correct fix for this is to enhance the metadata to be able to describe
|
|
|
|
# such versioned entry points, but that won't happen till Metadata 2.0 is
|
|
|
|
# available.
|
|
|
|
# In the meantime, projects using versioned entry points will either have
|
|
|
|
# incorrect versioned entry points, or they will not be able to distribute
|
|
|
|
# "universal" wheels (i.e., they will need a wheel per Python version).
|
|
|
|
#
|
|
|
|
# Because setuptools and pip are bundled with _ensurepip and virtualenv,
|
|
|
|
# we need to use universal wheels. So, as a stopgap until Metadata 2.0, we
|
|
|
|
# override the versioned entry points in the wheel and generate the
|
2016-06-10 21:27:07 +02:00
|
|
|
# correct ones. This code is purely a short-term measure until Metadata 2.0
|
2013-11-01 18:28:35 +01:00
|
|
|
# is available.
|
2013-11-07 18:28:10 +01:00
|
|
|
#
|
|
|
|
# To add the level of hack in this section of code, in order to support
|
|
|
|
# ensurepip this code will look for an ``ENSUREPIP_OPTIONS`` environment
|
|
|
|
# variable which will control which version scripts get installed.
|
|
|
|
#
|
|
|
|
# ENSUREPIP_OPTIONS=altinstall
|
|
|
|
# - Only pipX.Y and easy_install-X.Y will be generated and installed
|
|
|
|
# ENSUREPIP_OPTIONS=install
|
|
|
|
# - pipX.Y, pipX, easy_install-X.Y will be generated and installed. Note
|
|
|
|
# that this option is technically if ENSUREPIP_OPTIONS is set and is
|
|
|
|
# not altinstall
|
|
|
|
# DEFAULT
|
|
|
|
# - The default behavior is to install pip, pipX, pipX.Y, easy_install
|
|
|
|
# and easy_install-X.Y.
|
2013-10-23 16:57:12 +02:00
|
|
|
pip_script = console.pop('pip', None)
|
|
|
|
if pip_script:
|
2013-11-05 01:12:59 +01:00
|
|
|
if "ENSUREPIP_OPTIONS" not in os.environ:
|
2019-07-22 04:49:51 +02:00
|
|
|
scripts_to_generate.append('pip = ' + pip_script)
|
2013-11-05 01:12:59 +01:00
|
|
|
|
|
|
|
if os.environ.get("ENSUREPIP_OPTIONS", "") != "altinstall":
|
2019-07-22 04:49:51 +02:00
|
|
|
scripts_to_generate.append(
|
|
|
|
'pip%s = %s' % (sys.version_info[0], pip_script)
|
|
|
|
)
|
2013-11-05 01:12:59 +01:00
|
|
|
|
2019-07-22 04:49:51 +02:00
|
|
|
scripts_to_generate.append(
|
|
|
|
'pip%s = %s' % (get_major_minor_version(), pip_script)
|
|
|
|
)
|
2013-10-23 16:57:12 +02:00
|
|
|
# Delete any other versioned pip entry points
|
|
|
|
pip_ep = [k for k in console if re.match(r'pip(\d(\.\d)?)?$', k)]
|
|
|
|
for k in pip_ep:
|
|
|
|
del console[k]
|
|
|
|
easy_install_script = console.pop('easy_install', None)
|
|
|
|
if easy_install_script:
|
2013-11-05 01:12:59 +01:00
|
|
|
if "ENSUREPIP_OPTIONS" not in os.environ:
|
2019-07-22 04:49:51 +02:00
|
|
|
scripts_to_generate.append(
|
|
|
|
'easy_install = ' + easy_install_script
|
|
|
|
)
|
2013-11-05 01:12:59 +01:00
|
|
|
|
2019-07-22 04:49:51 +02:00
|
|
|
scripts_to_generate.append(
|
|
|
|
'easy_install-%s = %s' % (
|
|
|
|
get_major_minor_version(), easy_install_script
|
|
|
|
)
|
2019-07-18 09:12:05 +02:00
|
|
|
)
|
2013-10-23 16:57:12 +02:00
|
|
|
# Delete any other versioned easy_install entry points
|
2014-01-27 15:07:10 +01:00
|
|
|
easy_install_ep = [
|
|
|
|
k for k in console if re.match(r'easy_install(-\d\.\d)?$', k)
|
|
|
|
]
|
2013-10-23 16:57:12 +02:00
|
|
|
for k in easy_install_ep:
|
|
|
|
del console[k]
|
|
|
|
|
2013-11-01 18:28:35 +01:00
|
|
|
# Generate the console and GUI entry points specified in the wheel
|
2019-07-22 04:49:51 +02:00
|
|
|
scripts_to_generate.extend(
|
|
|
|
'%s = %s' % kv for kv in console.items()
|
|
|
|
)
|
2017-10-02 18:54:37 +02:00
|
|
|
|
2019-07-22 04:49:51 +02:00
|
|
|
gui_scripts_to_generate = [
|
|
|
|
'%s = %s' % kv for kv in gui.items()
|
|
|
|
]
|
|
|
|
|
|
|
|
generated_console_scripts = [] # type: List[str]
|
|
|
|
|
|
|
|
try:
|
|
|
|
generated_console_scripts = maker.make_multiple(scripts_to_generate)
|
|
|
|
generated.extend(generated_console_scripts)
|
2017-10-02 18:54:37 +02:00
|
|
|
|
2014-01-27 15:07:10 +01:00
|
|
|
generated.extend(
|
2019-07-22 04:49:51 +02:00
|
|
|
maker.make_multiple(gui_scripts_to_generate, {'gui': True})
|
|
|
|
)
|
|
|
|
except MissingCallableSuffix as e:
|
|
|
|
entry = e.args[0]
|
|
|
|
raise InstallationError(
|
2019-09-07 15:56:36 +02:00
|
|
|
"Invalid script entry point: {} for req: {} - A callable "
|
2019-12-10 23:18:38 +01:00
|
|
|
"suffix is required. Cf https://packaging.python.org/"
|
|
|
|
"specifications/entry-points/#use-for-scripts for more "
|
2019-10-12 04:24:50 +02:00
|
|
|
"information.".format(entry, req_description)
|
2014-01-27 15:07:10 +01:00
|
|
|
)
|
2012-10-02 07:50:24 +02:00
|
|
|
|
2019-07-22 04:49:51 +02:00
|
|
|
if warn_script_location:
|
|
|
|
msg = message_about_scripts_not_on_PATH(generated_console_scripts)
|
|
|
|
if msg is not None:
|
|
|
|
logger.warning(msg)
|
|
|
|
|
2015-12-04 10:19:17 +01:00
|
|
|
# Record pip as the installer
|
2019-12-30 16:59:23 +01:00
|
|
|
installer = os.path.join(dest_info_dir, 'INSTALLER')
|
|
|
|
temp_installer = os.path.join(dest_info_dir, 'INSTALLER.pip')
|
2015-12-04 10:19:17 +01:00
|
|
|
with open(temp_installer, 'wb') as installer_file:
|
|
|
|
installer_file.write(b'pip\n')
|
|
|
|
shutil.move(temp_installer, installer)
|
|
|
|
generated.append(installer)
|
|
|
|
|
|
|
|
# Record details of all files installed
|
2019-12-30 16:59:23 +01:00
|
|
|
record = os.path.join(dest_info_dir, 'RECORD')
|
|
|
|
temp_record = os.path.join(dest_info_dir, 'RECORD.pip')
|
2012-10-02 07:50:24 +02:00
|
|
|
with open_for_csv(record, 'r') as record_in:
|
|
|
|
with open_for_csv(temp_record, 'w+') as record_out:
|
|
|
|
reader = csv.reader(record_in)
|
2019-01-24 03:44:54 +01:00
|
|
|
outrows = get_csv_rows_for_installed(
|
|
|
|
reader, installed=installed, changed=changed,
|
|
|
|
generated=generated, lib_dir=lib_dir,
|
|
|
|
)
|
2012-10-02 07:50:24 +02:00
|
|
|
writer = csv.writer(record_out)
|
2018-10-24 18:19:58 +02:00
|
|
|
# Sort to simplify testing.
|
|
|
|
for row in sorted_outrows(outrows):
|
2018-06-21 22:05:37 +02:00
|
|
|
writer.writerow(row)
|
2012-10-02 07:50:24 +02:00
|
|
|
shutil.move(temp_record, record)
|
|
|
|
|
2014-01-27 15:07:10 +01:00
|
|
|
|
2019-12-14 17:10:36 +01:00
|
|
|
def install_wheel(
|
|
|
|
name, # type: str
|
|
|
|
wheel_path, # type: str
|
|
|
|
scheme, # type: Scheme
|
|
|
|
req_description, # type: str
|
|
|
|
pycompile=True, # type: bool
|
|
|
|
warn_script_location=True, # type: bool
|
2019-12-14 17:21:00 +01:00
|
|
|
_temp_dir_for_testing=None, # type: Optional[str]
|
2019-12-14 17:10:36 +01:00
|
|
|
):
|
|
|
|
# type: (...) -> None
|
2019-12-14 17:21:00 +01:00
|
|
|
with TempDirectory(
|
|
|
|
path=_temp_dir_for_testing, kind="unpacked-wheel"
|
|
|
|
) as unpacked_dir:
|
|
|
|
unpack_file(wheel_path, unpacked_dir.path)
|
2019-12-14 17:10:36 +01:00
|
|
|
install_unpacked_wheel(
|
|
|
|
name=name,
|
2019-12-14 17:21:00 +01:00
|
|
|
wheeldir=unpacked_dir.path,
|
2019-12-14 17:10:36 +01:00
|
|
|
scheme=scheme,
|
|
|
|
req_description=req_description,
|
|
|
|
pycompile=pycompile,
|
|
|
|
warn_script_location=warn_script_location,
|
|
|
|
)
|
|
|
|
|
|
|
|
|
2019-12-31 19:44:20 +01:00
|
|
|
def wheel_dist_info_dir(source, name):
|
|
|
|
# type: (str, str) -> str
|
2019-12-31 19:23:48 +01:00
|
|
|
"""Returns the name of the contained .dist-info directory.
|
|
|
|
|
|
|
|
Raises AssertionError or UnsupportedWheel if not found, >1 found, or
|
|
|
|
it doesn't match the provided name.
|
|
|
|
"""
|
|
|
|
subdirs = os.listdir(source)
|
|
|
|
info_dirs = [s for s in subdirs if s.endswith('.dist-info')]
|
|
|
|
|
2019-12-31 19:43:19 +01:00
|
|
|
if not info_dirs:
|
|
|
|
raise UnsupportedWheel(".dist-info directory not found")
|
2019-12-31 19:23:48 +01:00
|
|
|
|
2019-12-31 19:43:19 +01:00
|
|
|
if len(info_dirs) > 1:
|
|
|
|
raise UnsupportedWheel(
|
|
|
|
"multiple .dist-info directories found: {}".format(
|
|
|
|
", ".join(info_dirs)
|
|
|
|
)
|
2019-12-31 19:23:48 +01:00
|
|
|
)
|
|
|
|
|
|
|
|
info_dir = info_dirs[0]
|
|
|
|
|
|
|
|
info_dir_name = canonicalize_name(info_dir)
|
|
|
|
canonical_name = canonicalize_name(name)
|
|
|
|
if not info_dir_name.startswith(canonical_name):
|
|
|
|
raise UnsupportedWheel(
|
2019-12-31 19:39:40 +01:00
|
|
|
".dist-info directory {!r} does not start with {!r}".format(
|
|
|
|
info_dir, canonical_name
|
2019-12-31 19:23:48 +01:00
|
|
|
)
|
|
|
|
)
|
|
|
|
|
|
|
|
return info_dir
|
|
|
|
|
|
|
|
|
2019-12-31 20:00:16 +01:00
|
|
|
def wheel_metadata(source, dist_info_dir):
|
|
|
|
# type: (str, str) -> Message
|
2019-12-31 18:28:26 +01:00
|
|
|
"""Return the WHEEL metadata of an extracted wheel, if possible.
|
|
|
|
Otherwise, raise UnsupportedWheel.
|
2014-02-15 07:21:22 +01:00
|
|
|
"""
|
|
|
|
try:
|
2019-12-31 20:00:16 +01:00
|
|
|
with open(os.path.join(source, dist_info_dir, "WHEEL"), "rb") as f:
|
2019-12-31 19:00:57 +01:00
|
|
|
wheel_text = ensure_str(f.read())
|
2019-12-31 16:17:41 +01:00
|
|
|
except (IOError, OSError) as e:
|
|
|
|
raise UnsupportedWheel("could not read WHEEL file: {!r}".format(e))
|
|
|
|
except UnicodeDecodeError as e:
|
|
|
|
raise UnsupportedWheel("error decoding WHEEL: {!r}".format(e))
|
|
|
|
|
|
|
|
# FeedParser (used by Parser) does not raise any exceptions. The returned
|
|
|
|
# message may have .defects populated, but for backwards-compatibility we
|
|
|
|
# currently ignore them.
|
2019-12-31 18:28:26 +01:00
|
|
|
return Parser().parsestr(wheel_text)
|
2019-12-31 16:17:41 +01:00
|
|
|
|
2019-12-31 18:28:26 +01:00
|
|
|
|
|
|
|
def wheel_version(wheel_data):
|
|
|
|
# type: (Message) -> Tuple[int, ...]
|
|
|
|
"""Given WHEEL metadata, return the parsed Wheel-Version.
|
|
|
|
Otherwise, raise UnsupportedWheel.
|
|
|
|
"""
|
2019-12-31 16:17:41 +01:00
|
|
|
version_text = wheel_data["Wheel-Version"]
|
|
|
|
if version_text is None:
|
|
|
|
raise UnsupportedWheel("WHEEL is missing Wheel-Version")
|
|
|
|
|
|
|
|
version = version_text.strip()
|
|
|
|
|
|
|
|
try:
|
|
|
|
return tuple(map(int, version.split('.')))
|
|
|
|
except ValueError:
|
|
|
|
raise UnsupportedWheel("invalid Wheel-Version: {!r}".format(version))
|
2014-02-15 07:21:22 +01:00
|
|
|
|
|
|
|
|
2014-02-18 05:16:54 +01:00
|
|
|
def check_compatibility(version, name):
|
2019-12-31 16:17:41 +01:00
|
|
|
# type: (Tuple[int, ...], str) -> None
|
2019-10-20 15:14:50 +02:00
|
|
|
"""Raises errors or warns if called with an incompatible Wheel-Version.
|
2014-02-18 05:16:54 +01:00
|
|
|
|
2014-02-19 08:44:44 +01:00
|
|
|
Pip should refuse to install a Wheel-Version that's a major series
|
|
|
|
ahead of what it's compatible with (e.g 2.0 > 1.1); and warn when
|
|
|
|
installing a version only minor version ahead (e.g 1.2 > 1.1).
|
|
|
|
|
|
|
|
version: a 2-tuple representing a Wheel-Version (Major, Minor)
|
2014-02-18 05:16:54 +01:00
|
|
|
name: name of wheel or package to raise exception about
|
2014-02-19 08:44:44 +01:00
|
|
|
|
|
|
|
:raises UnsupportedWheel: when an incompatible Wheel-Version is given
|
2014-02-18 05:16:54 +01:00
|
|
|
"""
|
|
|
|
if version[0] > VERSION_COMPATIBLE[0]:
|
|
|
|
raise UnsupportedWheel(
|
|
|
|
"%s's Wheel-Version (%s) is not compatible with this version "
|
|
|
|
"of pip" % (name, '.'.join(map(str, version)))
|
|
|
|
)
|
|
|
|
elif version > VERSION_COMPATIBLE:
|
2014-08-31 01:52:28 +02:00
|
|
|
logger.warning(
|
|
|
|
'Installing from a newer Wheel-Version (%s)',
|
|
|
|
'.'.join(map(str, version)),
|
|
|
|
)
|