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

Issue #2140: Build wheels during pip install #2618

Merged
merged 3 commits into from
Apr 13, 2015
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGES.txt
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,9 @@
* Support ``--install-option`` and ``--global-option`` per requirement in
requirement files (:pull:`2537`)

* Build Wheels prior to installing from sdist, caching them in the pip cache
directory to speed up subsequent installs. (:pull:`2618`)

**6.1.1 (2015-04-07)**

* No longer ignore dependencies which have been added to the standard library,
Expand Down
33 changes: 33 additions & 0 deletions docs/reference/pip_install.rst
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,15 @@ Description
.. pip-command-description:: install


Overview
++++++++

Pip install has several stages:

1. Resolve dependencies. What will be installed is determined here.
2. Build wheels. All the dependencies that can be are built into wheels.
3. Install the packages (and uninstall anything being upgraded/replaced).

Installation Order
++++++++++++++++++

Expand Down Expand Up @@ -388,6 +397,26 @@ Windows
:file:`<CSIDL_LOCAL_APPDATA>\\pip\\Cache`


Wheel cache
***********

Pip will read from the subdirectory ``wheels`` within the pip cache dir and use
any packages found there. This is disabled via the same ``no-cache-dir`` option
that disables the HTTP cache. The internal structure of that cache is not part
of the Pip API. As of 7.0 pip uses a subdirectory per sdist that wheels were
built from, and wheels within that subdirectory.

Pip attempts to choose the best wheels from those built in preference to
building a new wheel. Note that this means when a package has both optional
C extensions and builds `py` tagged wheels when the C extension can't be built
that pip will not attempt to build a better wheel for Python's that would have
supported it, once any generic wheel is built. To correct this, make sure that
the wheel's are built with Python specific tags - e.g. pp on Pypy.

When no wheels are found for an sdist, pip will attempt to build a wheel
automatically and insert it into the wheel cache.


Hash Verification
+++++++++++++++++

Expand Down Expand Up @@ -482,6 +511,10 @@ implement the following command::
This should implement the complete process of installing the package in
"editable" mode.

All packages will be attempted to built into wheels::

setup.py bdist_wheel -d XXX

One further ``setup.py`` command is invoked by ``pip install``::

setup.py clean
Expand Down
2 changes: 2 additions & 0 deletions pip/basecommand.py
Original file line number Diff line number Diff line change
Expand Up @@ -289,6 +289,7 @@ def populate_requirement_set(requirement_set, args, options, finder,
requirement_set.add_requirement(
InstallRequirement.from_line(
name, None, isolated=options.isolated_mode,
cache_root=options.cache_dir
)
)

Expand All @@ -298,6 +299,7 @@ def populate_requirement_set(requirement_set, args, options, finder,
name,
default_vcs=options.default_vcs,
isolated=options.isolated_mode,
cache_root=options.cache_dir
)
)

Expand Down
3 changes: 2 additions & 1 deletion pip/commands/freeze.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,8 @@ def run(self, options, args):
local_only=options.local,
user_only=options.user,
skip_regex=options.skip_requirements_regex,
isolated=options.isolated_mode)
isolated=options.isolated_mode,
cache_root=options.cache_dir)

for line in freeze(**freeze_kwargs):
sys.stdout.write(line + '\n')
26 changes: 23 additions & 3 deletions pip/commands/install.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,14 @@
import tempfile
import shutil
import warnings
try:
import wheel
except ImportError:
wheel = None

from pip.req import RequirementSet
from pip.locations import virtualenv_no_global, distutils_scheme
from pip.basecommand import RequirementCommand
from pip.locations import virtualenv_no_global, distutils_scheme
from pip.index import PackageFinder
from pip.exceptions import (
InstallationError, CommandError, PreviousBuildDirError,
Expand All @@ -18,6 +22,7 @@
from pip.utils import ensure_dir
from pip.utils.build import BuildDirectory
from pip.utils.deprecation import RemovedInPip8Warning
from pip.wheel import WheelBuilder


logger = logging.getLogger(__name__)
Expand Down Expand Up @@ -233,12 +238,12 @@ def run(self, options, args):
with self._build_session(options) as session:

finder = self._build_package_finder(options, index_urls, session)

build_delete = (not (options.no_clean or options.build_dir))
with BuildDirectory(options.build_dir,
delete=build_delete) as build_dir:
requirement_set = RequirementSet(
build_dir=build_dir,
cache_root=options.cache_dir,
src_dir=options.src_dir,
download_dir=options.download_dir,
upgrade=options.upgrade,
Expand All @@ -261,7 +266,22 @@ def run(self, options, args):
return

try:
requirement_set.prepare_files(finder)
if options.download_dir or not wheel:
# on -d don't do complex things like building
# wheels, and don't try to build wheels when wheel is
# not installed.
requirement_set.prepare_files(finder)
else:
# build wheels before install.
wb = WheelBuilder(
requirement_set,
finder,
build_options=[],
global_options=[],
Copy link
Member

Choose a reason for hiding this comment

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

Wondering if this is not an issue (for build_options and global_options)... maybe the use of these would disable the wheel caching as I dont think we want to build wheels for all possible options ?

)
# Ignore the result: a failed wheel will be
# installed from the sdist/vcs whatever.
wb.build(autobuilding=True)

if not options.download_dir:
requirement_set.install(
Expand Down
1 change: 1 addition & 0 deletions pip/commands/list.py
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,7 @@ def find_packages_latest_versions(self, options):
for dist in installed_packages:
req = InstallRequirement.from_line(
dist.key, None, isolated=options.isolated_mode,
cache_root=options.cache_dir,
)
typ = 'unknown'
try:
Expand Down
5 changes: 4 additions & 1 deletion pip/commands/uninstall.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ def run(self, options, args):

requirement_set = RequirementSet(
build_dir=None,
cache_root=options.cache_dir,
src_dir=None,
download_dir=None,
isolated=options.isolated_mode,
Expand All @@ -54,13 +55,15 @@ def run(self, options, args):
requirement_set.add_requirement(
InstallRequirement.from_line(
name, isolated=options.isolated_mode,
cache_root=options.cache_dir,
)
)
for filename in options.requirements:
for req in parse_requirements(
filename,
options=options,
session=session):
session=session,
cache_root=options.cache_dir):
requirement_set.add_requirement(req)
if not requirement_set.has_requirements:
raise InstallationError(
Expand Down
1 change: 1 addition & 0 deletions pip/commands/wheel.py
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,7 @@ def run(self, options, args):
delete=build_delete) as build_dir:
requirement_set = RequirementSet(
build_dir=build_dir,
cache_root=options.cache_dir,
src_dir=options.src_dir,
download_dir=None,
ignore_dependencies=options.ignore_dependencies,
Expand Down
47 changes: 30 additions & 17 deletions pip/index.py
Original file line number Diff line number Diff line change
Expand Up @@ -648,7 +648,6 @@ def _get_pages(self, locations, project_name):

all_locations.append(link)

_egg_info_re = re.compile(r'([a-z0-9_.]+)-([a-z0-9_.!+-]+)', re.I)
_py_version_re = re.compile(r'-py([123]\.?[0-9]?)$')

def _sort_links(self, links):
Expand Down Expand Up @@ -742,7 +741,7 @@ def _link_package_versions(self, link, search_name):
version = wheel.version

if not version:
version = self._egg_info_matches(egg_info, search_name, link)
version = egg_info_matches(egg_info, search_name, link)
if version is None:
self._log_skipped_link(
link, 'wrong project name (not %s)' % search_name)
Expand Down Expand Up @@ -783,25 +782,39 @@ def _link_package_versions(self, link, search_name):

return InstallationCandidate(search_name, version, link)

def _egg_info_matches(self, egg_info, search_name, link):
match = self._egg_info_re.search(egg_info)
if not match:
logger.debug('Could not parse version from link: %s', link)
return None
name = match.group(0).lower()
# To match the "safe" name that pkg_resources creates:
name = name.replace('_', '-')
# project name and version must be separated by a dash
look_for = search_name.lower() + "-"
if name.startswith(look_for):
return match.group(0)[len(look_for):]
else:
return None

def _get_page(self, link):
return HTMLPage.get_page(link, session=self.session)


def egg_info_matches(
egg_info, search_name, link,
_egg_info_re=re.compile(r'([a-z0-9_.]+)-([a-z0-9_.!+-]+)', re.I)):
"""Pull the version part out of a string.

:param egg_info: The string to parse. E.g. foo-2.1
:param search_name: The name of the package this belongs to. None to
infer the name. Note that this cannot unambiguously parse strings
like foo-2-2 which might be foo, 2-2 or foo-2, 2.
:param link: The link the string came from, for logging on failure.
"""
match = _egg_info_re.search(egg_info)
if not match:
logger.debug('Could not parse version from link: %s', link)
return None
if search_name is None:
full_match = match.group(0)
return full_match[full_match.index('-'):]
name = match.group(0).lower()
# To match the "safe" name that pkg_resources creates:
name = name.replace('_', '-')
# project name and version must be separated by a dash
look_for = search_name.lower() + "-"
if name.startswith(look_for):
return match.group(0)[len(look_for):]
else:
return None


class HTMLPage(object):
"""Represents one page, along with its URL"""

Expand Down
5 changes: 4 additions & 1 deletion pip/operations/freeze.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,8 @@ def freeze(
find_links=None, local_only=None, user_only=None, skip_regex=None,
find_tags=False,
default_vcs=None,
isolated=False):
isolated=False,
cache_root=None):
find_links = find_links or []
skip_match = None

Expand Down Expand Up @@ -75,11 +76,13 @@ def freeze(
line,
default_vcs=default_vcs,
isolated=isolated,
cache_root=cache_root,
)
else:
line_req = InstallRequirement.from_line(
line,
isolated=isolated,
cache_root=cache_root,
)

if not line_req.name:
Expand Down
19 changes: 9 additions & 10 deletions pip/req/req_file.py
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ def parser_exit(self, msg):


def parse_requirements(filename, finder=None, comes_from=None, options=None,
session=None):
session=None, cache_root=None):
"""
Parse a requirements file and yield InstallRequirement instances.

Expand All @@ -87,7 +87,6 @@ def parse_requirements(filename, finder=None, comes_from=None, options=None,
:param options: Global options.
:param session: Instance of pip.download.PipSession.
"""

if session is None:
raise TypeError(
"parse_requirements() missing 1 required keyword argument: "
Expand All @@ -99,15 +98,15 @@ def parse_requirements(filename, finder=None, comes_from=None, options=None,
)

parser = parse_content(
filename, content, finder, comes_from, options, session
filename, content, finder, comes_from, options, session, cache_root
)

for item in parser:
yield item


def parse_content(filename, content, finder=None, comes_from=None,
options=None, session=None):
options=None, session=None, cache_root=None):

# Split, sanitize and join lines with continuations.
content = content.splitlines()
Expand All @@ -129,8 +128,8 @@ def parse_content(filename, content, finder=None, comes_from=None,
comes_from = '-r %s (line %s)' % (filename, line_number)
isolated = options.isolated_mode if options else False
yield InstallRequirement.from_line(
req, comes_from, isolated=isolated, options=opts
)
req, comes_from, isolated=isolated, options=opts,
cache_root=cache_root)

# ---------------------------------------------------------------------
elif linetype == REQUIREMENT_EDITABLE:
Expand All @@ -139,8 +138,8 @@ def parse_content(filename, content, finder=None, comes_from=None,
default_vcs = options.default_vcs if options else None
yield InstallRequirement.from_editable(
value, comes_from=comes_from,
default_vcs=default_vcs, isolated=isolated
)
default_vcs=default_vcs, isolated=isolated,
cache_root=cache_root)

# ---------------------------------------------------------------------
elif linetype == REQUIREMENT_FILE:
Expand All @@ -152,8 +151,8 @@ def parse_content(filename, content, finder=None, comes_from=None,
req_url = os.path.join(os.path.dirname(filename), value)
# TODO: Why not use `comes_from='-r {} (line {})'` here as well?
parser = parse_requirements(
req_url, finder, comes_from, options, session
)
req_url, finder, comes_from, options, session,
cache_root=cache_root)
for req in parser:
yield req

Expand Down
Loading