Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Install build requirements before calling setup.py #4799

Merged
merged 18 commits into from Mar 1, 2018
Merged
Empty file added news/4799.trivial
Empty file.
17 changes: 17 additions & 0 deletions src/pip/_internal/build_env.py
Original file line number Diff line number Diff line change
Expand Up @@ -62,3 +62,20 @@ 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):
def __init__(self, no_clean):
pass

def __enter__(self):
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note: I should have overridden the initialization method here as well.

pass

def __exit__(self, exc_type, exc_val, exc_tb):
pass

def cleanup(self):
pass
64 changes: 54 additions & 10 deletions src/pip/_internal/operations/prepare.py
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -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__)
Expand All @@ -38,6 +47,23 @@ def make_abstract_dist(req):
return IsSDist(req)


def _install_build_reqs(finder, prefix, build_requirements):
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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: trailing comma

] + 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.

Expand All @@ -64,7 +90,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)

Expand All @@ -75,7 +101,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

Expand All @@ -91,17 +117,35 @@ def dist(self, finder):
)
return dist

def prep_for_dist(self):
self.req.run_egg_info()
self.req.assert_source_matches_version()
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Specifically here.

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()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A comment here saying that this is for installing stuff into a PEP 518 environment would be nice. :)


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):

def dist(self, finder):
return self.req.satisfied_by

def prep_for_dist(self):
def prep_for_dist(self, finder):
pass


Expand Down Expand Up @@ -259,14 +303,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"
Expand All @@ -284,7 +328,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)
Expand Down
3 changes: 3 additions & 0 deletions src/pip/_internal/req/req_install.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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):
Expand Down
7 changes: 5 additions & 2 deletions src/pip/_internal/resolve.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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:
Expand Down
26 changes: 1 addition & 25 deletions src/pip/_internal/wheel.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Binary file modified tests/data/packages/pep518-3.0.tar.gz
Binary file not shown.
2 changes: 1 addition & 1 deletion tests/data/src/pep518-3.0/pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
[build-system]
requires=["simple==3.0", "setuptools", "wheel"]
requires=["simplewheel==2.0", "setuptools", "wheel"]
2 changes: 2 additions & 0 deletions tests/data/src/pep518-3.0/setup.py
Original file line number Diff line number Diff line change
@@ -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()
Expand Down