Skip to content

Commit

Permalink
Fix pex resolution to respect --ignore-errors. (#828)
Browse files Browse the repository at this point in the history
Previously, pip might warn in red that versions were mismatched but
we would get no failure until pex runtime resolution. Improve pex
runtime resolution error messages and implement pex buildtime
resolution sanity checks when --no-ignore-errors.
  • Loading branch information
jsirois authored Dec 14, 2019
1 parent 1b7689e commit 31af708
Show file tree
Hide file tree
Showing 11 changed files with 338 additions and 201 deletions.
7 changes: 4 additions & 3 deletions pex/bin/pex.py
Original file line number Diff line number Diff line change
Expand Up @@ -234,8 +234,8 @@ def configure_clp_pex_options(parser):
dest='ignore_errors',
default=False,
action='store_true',
help='Ignore run-time requirement resolution errors when invoking the pex. '
'[Default: %default]')
help='Ignore requirement resolution solver errors when building pexes and later invoking '
'them. [Default: %default]')

group.add_option(
'--inherit-path',
Expand Down Expand Up @@ -565,7 +565,8 @@ def walk_and_do(fn, src_dir):
build=options.build,
use_wheel=options.use_wheel,
compile=options.compile,
max_parallel_jobs=options.max_parallel_jobs)
max_parallel_jobs=options.max_parallel_jobs,
ignore_errors=options.ignore_errors)

for resolved_dist in resolveds:
log(' %s -> %s' % (resolved_dist.requirement, resolved_dist.distribution),
Expand Down
59 changes: 44 additions & 15 deletions pex/environment.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,17 +9,18 @@
import site
import sys
import zipfile
from collections import OrderedDict
from collections import OrderedDict, defaultdict

from pex import pex_builder, pex_warnings
from pex.bootstrap import Bootstrap
from pex.common import atomic_directory, die, open_zip
from pex.interpreter import PythonInterpreter
from pex.orderedset import OrderedSet
from pex.package import distribution_compatible
from pex.platforms import Platform
from pex.third_party.pkg_resources import DistributionNotFound, Environment, Requirement, WorkingSet
from pex.tracer import TRACER
from pex.util import CacheHelper
from pex.util import CacheHelper, DistributionHelper


def _import_pkg_resources():
Expand Down Expand Up @@ -226,16 +227,16 @@ def activate(self):
return self._working_set

def _resolve(self, working_set, reqs):
reqs = reqs[:]
unresolved_reqs = set()
resolveds = set()
reqs_by_key = OrderedDict((req.key, req) for req in reqs)
unresolved_reqs = OrderedDict()
resolveds = OrderedSet()

environment = self._target_interpreter_env.copy()
environment['extra'] = list(set(itertools.chain(*(req.extras for req in reqs))))

# Resolve them one at a time so that we can figure out which ones we need to elide should
# there be an interpreter incompatibility.
for req in reqs:
for req in reqs_by_key.values():
if req.marker and not req.marker.evaluate(environment=environment):
TRACER.log('Skipping activation of `%s` due to environment marker de-selection' % req)
continue
Expand All @@ -244,27 +245,55 @@ def _resolve(self, working_set, reqs):
resolveds.update(working_set.resolve([req], env=self))
except DistributionNotFound as e:
TRACER.log('Failed to resolve a requirement: %s' % e)
unresolved_reqs.add(e.req.project_name)
requirers = unresolved_reqs.setdefault(e.req, OrderedSet())
if e.requirers:
unresolved_reqs.update(e.requirers)

unresolved_reqs = set([req.lower() for req in unresolved_reqs])
requirers.update(reqs_by_key[requirer] for requirer in e.requirers)

if unresolved_reqs:
TRACER.log('Unresolved requirements:')
for req in unresolved_reqs:
TRACER.log(' - %s' % req)

TRACER.log('Distributions contained within this pex:')
distributions_by_key = defaultdict(list)
if not self._pex_info.distributions:
TRACER.log(' None')
else:
for dist in self._pex_info.distributions:
TRACER.log(' - %s' % dist)
for dist_name, dist_digest in self._pex_info.distributions.items():
TRACER.log(' - %s' % dist_name)
distribution = DistributionHelper.distribution_from_path(
path=os.path.join(self._pex_info.install_cache, dist_digest, dist_name)
)
distributions_by_key[distribution.as_requirement().key].append(distribution)

if not self._pex_info.ignore_errors:
items = []
for index, (requirement, requirers) in enumerate(unresolved_reqs.items()):
rendered_requirers = ''
if requirers:
rendered_requirers = (
'\n Required by:'
'\n {requirers}'
).format(requirers='\n '.join(map(str, requirers)))

items.append(
'{index: 2d}: {requirement}'
'{rendered_requirers}'
'\n But this pex only contains:'
'\n {distributions}'.format(
index=index + 1,
requirement=requirement,
rendered_requirers=rendered_requirers,
distributions='\n '.join(os.path.basename(d.location)
for d in distributions_by_key[requirement.key])
)
)

die(
'Failed to execute PEX file, missing %s compatible dependencies for:\n%s' % (
Platform.current(),
'\n'.join(str(r) for r in unresolved_reqs)
'Failed to execute PEX file. Needed {platform} compatible dependencies for:\n{items}'
.format(
platform=Platform.of_interpreter(self._interpreter),
items='\n\t'.join(items)
)
)

Expand Down
12 changes: 9 additions & 3 deletions pex/package.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,18 @@ def distribution_compatible(dist, supported_tags=None):
by the platform in question; defaults to the current interpreter's supported tags.
:returns: True if the distribution is compatible, False if it is unrecognized or incompatible.
"""
if supported_tags is None:
supported_tags = get_supported()

filename, ext = os.path.splitext(os.path.basename(dist.location))
if ext.lower() != '.whl':
return False
# This supports resolving pex's own vendored distributions which are vendored in directory
# directory with the project name (`pip/` for pip) and not the corresponding wheel name
# (`pip-19.3.1-py2.py3-none-any.whl/` for pip). Pex only vendors universal wheels for all
# platforms it supports at buildtime and runtime so this is always safe.
return True

if supported_tags is None:
supported_tags = get_supported()

try:
name_, raw_version_, py_tag, abi_tag, arch_tag = filename.rsplit('-', 4)
except ValueError:
Expand Down
6 changes: 3 additions & 3 deletions pex/pex_builder.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
from pex.finders import get_entry_point_from_console_script, get_script_from_distributions
from pex.interpreter import PythonInterpreter
from pex.pex_info import PexInfo
from pex.pip import spawn_install_wheel
from pex.pip import get_pip
from pex.third_party.pkg_resources import DefaultProvider, ZipProvider, get_provider
from pex.tracer import TRACER
from pex.util import CacheHelper, DistributionHelper
Expand Down Expand Up @@ -278,7 +278,7 @@ def _add_dist_dir(self, path, dist_name):

def _add_dist_wheel_file(self, path, dist_name):
with temporary_dir() as install_dir:
spawn_install_wheel(
get_pip().spawn_install_wheel(
wheel=path,
install_dir=install_dir,
target=DistributionTarget.for_interpreter(self.interpreter)
Expand Down Expand Up @@ -327,7 +327,7 @@ def add_dist_location(self, dist, name=None):
dist_path = dist
if os.path.isfile(dist_path) and dist_path.endswith('.whl'):
dist_path = os.path.join(safe_mkdtemp(), os.path.basename(dist))
spawn_install_wheel(
get_pip().spawn_install_wheel(
wheel=dist,
install_dir=dist_path,
target=DistributionTarget.for_interpreter(self.interpreter)
Expand Down
Loading

0 comments on commit 31af708

Please sign in to comment.