diff --git a/news/4799.trivial b/news/4799.trivial new file mode 100644 index 00000000000..e69de29bb2d diff --git a/src/pip/_internal/build_env.py b/src/pip/_internal/build_env.py index 52a53392c5e..1989c8aad05 100644 --- a/src/pip/_internal/build_env.py +++ b/src/pip/_internal/build_env.py @@ -8,7 +8,7 @@ class BuildEnvironment(object): - """Manages a temporary environment to install build deps + """Creates and manages an isolated environment to install build deps """ def __init__(self, no_clean): @@ -62,3 +62,23 @@ def __exit__(self, exc_type, exc_val, exc_tb): os.environ.pop('PYTHONPATH', None) else: os.environ['PYTHONPATH'] = self.save_pythonpath + + def cleanup(self): + self._temp_dir.cleanup() + + +class NoOpBuildEnvironment(BuildEnvironment): + """A no-op drop-in replacement for BuildEnvironment + """ + + def __init__(self, no_clean): + pass + + def __enter__(self): + pass + + def __exit__(self, exc_type, exc_val, exc_tb): + pass + + def cleanup(self): + pass diff --git a/src/pip/_internal/operations/prepare.py b/src/pip/_internal/operations/prepare.py index e1d8a2baa81..86e2e7b9954 100644 --- a/src/pip/_internal/operations/prepare.py +++ b/src/pip/_internal/operations/prepare.py @@ -1,11 +1,15 @@ """Prepares a distribution for installation """ +import itertools import logging import os +import sys +from copy import copy from pip._vendor import pkg_resources, requests +from pip._internal.build_env import NoOpBuildEnvironment from pip._internal.compat import expanduser from pip._internal.download import ( is_dir_url, is_file_url, is_vcs_url, unpack_url, url_to_path, @@ -14,9 +18,14 @@ DirectoryUrlHashUnsupported, HashUnpinned, InstallationError, PreviousBuildDirError, VcsHashUnsupported, ) +from pip._internal.index import FormatControl +from pip._internal.req.req_install import InstallRequirement from pip._internal.utils.hashes import MissingHashes from pip._internal.utils.logging import indent_log -from pip._internal.utils.misc import display_path, normalize_path +from pip._internal.utils.misc import ( + call_subprocess, display_path, normalize_path, +) +from pip._internal.utils.ui import open_spinner from pip._internal.vcs import vcs logger = logging.getLogger(__name__) @@ -38,6 +47,26 @@ def make_abstract_dist(req): return IsSDist(req) +def _install_build_reqs(finder, prefix, build_requirements): + # NOTE: What follows is not a very good thing. + # Eventually, this should move into the BuildEnvironment class and + # that should handle all the isolation and sub-process invocation. + finder = copy(finder) + finder.format_control = FormatControl(set(), set([":all:"])) + urls = [ + finder.find_requirement( + InstallRequirement.from_line(r), upgrade=False).url + for r in build_requirements + ] + args = [ + sys.executable, '-m', 'pip', 'install', '--ignore-installed', + '--prefix', prefix, + ] + list(urls) + + with open_spinner("Installing build dependencies") as spinner: + call_subprocess(args, show_stdout=False, spinner=spinner) + + class DistAbstraction(object): """Abstracts out the wheel vs non-wheel Resolver.resolve() logic. @@ -64,7 +93,7 @@ def dist(self, finder): """Return a setuptools Dist object.""" raise NotImplementedError(self.dist) - def prep_for_dist(self): + def prep_for_dist(self, finder): """Ensure that we can get a Dist for this requirement.""" raise NotImplementedError(self.dist) @@ -75,7 +104,7 @@ def dist(self, finder): return list(pkg_resources.find_distributions( self.req.source_dir))[0] - def prep_for_dist(self): + def prep_for_dist(self, finder): # FIXME:https://github.com/pypa/pip/issues/1112 pass @@ -91,9 +120,27 @@ def dist(self, finder): ) return dist - def prep_for_dist(self): - self.req.run_egg_info() - self.req.assert_source_matches_version() + def prep_for_dist(self, finder): + # Before calling "setup.py egg_info", we need to set-up the build + # environment. + + build_requirements, isolate = self.req.get_pep_518_info() + + if 'setuptools' not in build_requirements: + logger.warning( + "This version of pip does not implement PEP 516, so " + "it cannot build a wheel without setuptools. You may need to " + "upgrade to a newer version of pip.") + + if not isolate: + self.req.build_env = NoOpBuildEnvironment(no_clean=False) + + with self.req.build_env as prefix: + if isolate: + _install_build_reqs(finder, prefix, build_requirements) + + self.req.run_egg_info() + self.req.assert_source_matches_version() class Installed(DistAbstraction): @@ -101,7 +148,7 @@ class Installed(DistAbstraction): def dist(self, finder): return self.req.satisfied_by - def prep_for_dist(self): + def prep_for_dist(self, finder): pass @@ -259,14 +306,14 @@ def prepare_linked_requirement(self, req, session, finder, (req, exc, req.link) ) abstract_dist = make_abstract_dist(req) - abstract_dist.prep_for_dist() + abstract_dist.prep_for_dist(finder) if self._download_should_save: # Make a .zip of the source_dir we already created. if req.link.scheme in vcs.all_schemes: req.archive(self.download_dir) return abstract_dist - def prepare_editable_requirement(self, req, require_hashes): + def prepare_editable_requirement(self, req, require_hashes, finder): """Prepare an editable requirement """ assert req.editable, "cannot prepare a non-editable req as editable" @@ -284,7 +331,7 @@ def prepare_editable_requirement(self, req, require_hashes): req.update_editable(not self._download_should_save) abstract_dist = make_abstract_dist(req) - abstract_dist.prep_for_dist() + abstract_dist.prep_for_dist(finder) if self._download_should_save: req.archive(self.download_dir) diff --git a/src/pip/_internal/req/req_install.py b/src/pip/_internal/req/req_install.py index d25f17c958b..c81a6ff3818 100644 --- a/src/pip/_internal/req/req_install.py +++ b/src/pip/_internal/req/req_install.py @@ -22,6 +22,7 @@ from pip._vendor.pkg_resources import RequirementParseError, parse_requirements from pip._internal import wheel +from pip._internal.build_env import BuildEnvironment from pip._internal.compat import native_str from pip._internal.download import ( is_archive_file, is_url, path_to_url, url_to_path, @@ -128,6 +129,7 @@ def __init__(self, req, comes_from, source_dir=None, editable=False, self.prepared = False self.isolated = isolated + self.build_env = BuildEnvironment(no_clean=True) @classmethod def from_editable(cls, editable_req, comes_from=None, isolated=False, @@ -880,6 +882,7 @@ def remove_temporary_source(self): rmtree(self.source_dir) self.source_dir = None self._temp_build_dir.cleanup() + self.build_env.cleanup() def install_editable(self, install_options, global_options=(), prefix=None): diff --git a/src/pip/_internal/resolve.py b/src/pip/_internal/resolve.py index 247c561c549..d7b5ab0eea3 100644 --- a/src/pip/_internal/resolve.py +++ b/src/pip/_internal/resolve.py @@ -187,7 +187,9 @@ def _get_abstract_dist_for(self, req): if req.editable: return self.preparer.prepare_editable_requirement( - req, self.require_hashes + req, + self.require_hashes, + self.finder, ) # satisfied_by is only evaluated by calling _check_skip_installed, @@ -246,11 +248,12 @@ def _resolve_one(self, requirement_set, req_to_install): return [] req_to_install.prepared = True - abstract_dist = self._get_abstract_dist_for(req_to_install) # register tmp src for cleanup in case something goes wrong requirement_set.reqs_to_cleanup.append(req_to_install) + abstract_dist = self._get_abstract_dist_for(req_to_install) + # Parse and return dependencies dist = abstract_dist.dist(self.finder) try: diff --git a/src/pip/_internal/wheel.py b/src/pip/_internal/wheel.py index 03c1b22b99f..17d52911698 100644 --- a/src/pip/_internal/wheel.py +++ b/src/pip/_internal/wheel.py @@ -617,37 +617,13 @@ def __init__(self, finder, preparer, wheel_cache, self.global_options = global_options or [] self.no_clean = no_clean - def _install_build_reqs(self, reqs, prefix): - # Local import to avoid circular import (wheel <-> req_install) - from pip._internal.req.req_install import InstallRequirement - from pip._internal.index import FormatControl - # Ignore the --no-binary option when installing the build system, so - # we don't recurse trying to build a self-hosting build system. - finder = copy.copy(self.finder) - finder.format_control = FormatControl(set(), set([":all:"])) - urls = [finder.find_requirement(InstallRequirement.from_line(r), - upgrade=False).url - for r in reqs] - - args = [sys.executable, '-m', 'pip', 'install', '--ignore-installed', - '--prefix', prefix] + list(urls) - with open_spinner("Installing build dependencies") as spinner: - call_subprocess(args, show_stdout=False, spinner=spinner) - def _build_one(self, req, output_dir, python_tag=None): """Build one wheel. :return: The filename of the built wheel, or None if the build failed. """ - build_reqs, isolate = req.get_pep_518_info() - if 'setuptools' not in build_reqs: - logger.warning( - "This version of pip does not implement PEP 516, so " - "it cannot build a wheel without setuptools. You may need to " - "upgrade to a newer version of pip.") # Install build deps into temporary directory (PEP 518) - with BuildEnvironment(self.no_clean) as prefix: - self._install_build_reqs(build_reqs, prefix) + with req.build_env: return self._build_one_inside_env(req, output_dir, python_tag=python_tag, isolate=True) diff --git a/tests/data/packages/pep518-3.0.tar.gz b/tests/data/packages/pep518-3.0.tar.gz index cfd03320375..f912e6d85a2 100644 Binary files a/tests/data/packages/pep518-3.0.tar.gz and b/tests/data/packages/pep518-3.0.tar.gz differ diff --git a/tests/data/src/pep518-3.0/pyproject.toml b/tests/data/src/pep518-3.0/pyproject.toml index 5e3bb233223..b51a1cec7c7 100644 --- a/tests/data/src/pep518-3.0/pyproject.toml +++ b/tests/data/src/pep518-3.0/pyproject.toml @@ -1,2 +1,2 @@ [build-system] -requires=["simple==3.0", "setuptools", "wheel"] +requires=["simplewheel==2.0", "setuptools", "wheel"] diff --git a/tests/data/src/pep518-3.0/setup.py b/tests/data/src/pep518-3.0/setup.py index f9d4c48913d..84e7feb702d 100644 --- a/tests/data/src/pep518-3.0/setup.py +++ b/tests/data/src/pep518-3.0/setup.py @@ -1,6 +1,8 @@ #!/usr/bin/env python from setuptools import find_packages, setup +import simple # ensure dependency is installed + setup(name='pep518', version='3.0', packages=find_packages()