Skip to content

Commit

Permalink
Only python_binary's constraint should be included in a built pex (pa…
Browse files Browse the repository at this point in the history
…ntsbuild#7776)

### Problem

See the problem described in pantsbuild#7775.

### Solution

Compute and validate the transitive constraints, but only include the `python_binary`'s constraint in the built pex.

### Result

Fixes pantsbuild#7775, but leaves a TODO about supporting building a binary for an interpreter for which we do not have a valid interpreter.
  • Loading branch information
stuhood authored and illicitonion committed May 21, 2019
1 parent c041871 commit ebf5716
Show file tree
Hide file tree
Showing 3 changed files with 56 additions and 9 deletions.
17 changes: 16 additions & 1 deletion src/python/pants/backend/python/tasks/python_binary_create.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
from pex.pex_builder import PEXBuilder
from pex.pex_info import PexInfo

from pants.backend.python.interpreter_cache import PythonInterpreterCache
from pants.backend.python.subsystems.pex_build_util import (PexBuilderWrapper,
has_python_requirements,
has_python_sources, has_resources,
Expand All @@ -35,6 +36,7 @@ def subsystem_dependencies(cls):
return super(PythonBinaryCreate, cls).subsystem_dependencies() + (
PexBuilderWrapper.Factory,
PythonNativeCode.scoped(cls),
PythonInterpreterCache,
)

@memoized_property
Expand Down Expand Up @@ -102,6 +104,18 @@ def execute(self):
atomic_copy(pex_path, pex_copy)
self.context.log.info('created pex {}'.format(os.path.relpath(pex_copy, get_buildroot())))

def _validate_interpreter_constraints(self, constraint_tgts):
"""Validate that the transitive constraints of the given PythonBinary target are compatible.
If no (local) interpreter can satisfy all of the given targets, raises
PythonInterpreterCache.UnsatisfiableInterpreterConstraintsError.
TODO: This currently does so by finding a concrete local interpreter that matches all of the
constraints, but it is possible to do this in memory instead.
see https://github.com/pantsbuild/pants/issues/7775
"""
PythonInterpreterCache.global_instance().select_interpreter_for_targets(constraint_tgts)

def _create_binary(self, binary_tgt, results_dir):
"""Create a .pex file for the specified binary target."""
# Note that we rebuild a chroot from scratch, instead of using the REQUIREMENTS_PEX
Expand Down Expand Up @@ -142,7 +156,8 @@ def _create_binary(self, binary_tgt, results_dir):

# Add interpreter compatibility constraints to pex info. This will first check the targets for any
# constraints, and if they do not have any will resort to the global constraints.
pex_builder.add_interpreter_constraints_from(constraint_tgts)
self._validate_interpreter_constraints(constraint_tgts)
pex_builder.add_interpreter_constraints_from([binary_tgt])

# Dump everything into the builder's chroot.
for tgt in source_tgts:
Expand Down
2 changes: 1 addition & 1 deletion testprojects/src/python/interpreter_selection/BUILD
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ python_binary(

# Note: Used by tests, but also useful for manual testing.
python_binary(
name = 'deliberately_conficting_compatibility',
name = 'deliberately_conflicting_compatibility',
dependencies = [
':echo_interpreter_version_lib',
],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
from pants.util.process_handler import subprocess
from pants_test.backend.python.interpreter_selection_utils import (PY_3, PY_27,
skip_unless_python3_present,
skip_unless_python27_and_python3_present,
skip_unless_python27_present)
from pants_test.pants_run_integration_test import PantsRunIntegrationTest

Expand Down Expand Up @@ -53,20 +54,25 @@ def _echo_version(self, version):

# Run the built pex.
exe = os.path.join(distdir, binary_name + '.pex')
proc = subprocess.Popen([exe], stdout=subprocess.PIPE)
(stdout_data, _) = proc.communicate()
return stdout_data.decode('utf-8')
return self._popen_stdout(exe)

def _popen_stdout(self, exe):
proc = subprocess.Popen([exe], stdout=subprocess.PIPE)
(stdout_data, _) = proc.communicate()
return stdout_data.decode('utf-8')

def _test_version(self, version):
echo = self._echo_version(version)
v = echo.split('.') # E.g., 2.7.13.
self._assert_version_matches(self._echo_version(version), version)

def _assert_version_matches(self, actual, expected):
v = actual.strip().split('.') # E.g., 2.7.13.
self.assertTrue(len(v) > 2, 'Not a valid version string: {}'.format(v))
expected_components = version.split('.')
expected_components = expected.split('.')
self.assertEqual(expected_components, v[:len(expected_components)])

def test_cli_option_wins_compatibility_conflict(self):
# Tests that targets with compatibility conflicts collide.
binary_target = '{}:deliberately_conficting_compatibility'.format(self.testproject)
binary_target = '{}:deliberately_conflicting_compatibility'.format(self.testproject)
pants_run = self._build_pex(binary_target)
self.assert_success(pants_run, 'Failed to build {binary}.'.format(binary=binary_target))

Expand Down Expand Up @@ -103,6 +109,32 @@ def test_conflict_via_config(self):
"Did not output interpreters discoved by Pants."
)

@skip_unless_python27_and_python3_present
def test_binary_uses_own_compatibility(self):
"""Tests that a binary target uses its own compatiblity, rather than including that of its
transitive dependencies.
"""
# This target depends on a 2.7 minimum library, but does not declare its own compatibility.
# By specifying a version on the CLI, we ensure that the binary target will use that, and then
# test that it ends up with the version we request (and not the lower version specified on its
# dependency).
with temporary_dir() as distdir:
config = {
'GLOBAL': {
'pants_distdir': distdir
}
}
args = [
'--python-setup-interpreter-constraints=["CPython>=3.6,<4"]',
]
binary_name = 'echo_interpreter_version'
binary_target = '{}:{}'.format(self.testproject, binary_name)
pants_run = self._build_pex(binary_target, config=config, args=args)
self.assert_success(pants_run, 'Failed to build {binary}.'.format(binary=binary_target))

actual = self._popen_stdout(os.path.join(distdir, binary_name + '.pex'))
self._assert_version_matches(actual, '3')

@skip_unless_python3_present
def test_select_3(self):
self._test_version(PY_3)
Expand Down

0 comments on commit ebf5716

Please sign in to comment.