Repository: pex-tool/pex

Issue:
Pex resolver API is not completely hermetic to PYTHONPATH.
The `pex.resolver` module delegates work to pip by running a pip PEX in a subprocess. If the interpreter being resolved for is not the current interpreter and the current interpreter has portions of its stdlib exported on the `PYTHONPATH` and those portions of the stdlib are incompatible with the interpreter being resolved for, the bootstrap of the pip PEX will fail before it can scrub away the `PYTHONPATH`.

A minimal repro:
```
$ python3.8 <<EOF
> import os
> import sys
> 
> 
> # This puts python3.8 stdlib on PYTHONPATH.
> os.environ['PYTHONPATH'] = os.pathsep.join(sys.path)
> 
> 
> from pex import resolver
> from pex.interpreter import PythonInterpreter
> 
> 
> def find_python36():
>   for interp in PythonInterpreter.iter():
>     if interp.version[:2] == (3, 6):
>       return interp
>   raise RuntimeError('This test requires python3.6 be on the PATH.')
> 
> 
> result = resolver.resolve(requirements=['packaging==19.2'], interpreter=find_python36())
> print('Resolved: {}'.format(result))
> EOF
Fatal Python error: Py_Initialize: can't initialize sys standard streams
Traceback (most recent call last):
  File "/usr/lib/python3.8/abc.py", line 64, in <module>
ModuleNotFoundError: No module named '_abc'

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "/usr/lib/python3.8/io.py", line 52, in <module>
  File "/usr/lib/python3.8/abc.py", line 68, in <module>
  File "/usr/lib/python3.8/_py_abc.py", line 35
    def __new__(mcls, name, bases, namespace, /, **kwargs):
                                              ^
SyntaxError: invalid syntax
Traceback (most recent call last):
  File "<stdin>", line 20, in <module>
  File "/home/jsirois/dev/pantsbuild/pex/pex/resolver.py", line 701, in resolve
    return list(resolve_request.resolve_distributions(ignore_errors=ignore_errors))
  File "/home/jsirois/dev/pantsbuild/pex/pex/resolver.py", line 498, in resolve_distributions
    for resolve_result in self._run_parallel(inputs=to_resolve,
  File "/home/jsirois/dev/pantsbuild/pex/pex/resolver.py", line 366, in _run_parallel
    for result in execute_parallel(
  File "/home/jsirois/dev/pantsbuild/pex/pex/jobs.py", line 219, in execute_parallel
    raise error
pex.resolver.Unsatisfiable: pid: 13111 -> /usr/bin/python3.6 /tmp/tmp6ghsjpkj --disable-pip-version-check --isolated --exists-action i -q --no-cache-dir download --dest /tmp/tmpk4po5m_6/resolved_dists/cp36-cp36m packaging==19.2 raised Executing /usr/bin/python3.6 /tmp/tmp6ghsjpkj --disable-pip-version-check --isolated --exists-action i -q --no-cache-dir download --dest /tmp/tmpk4po5m_6/resolved_dists/cp36-cp36m packaging==19.2 failed with -6
```


Discussion:

Resolving patch:
diff --git a/pex/pip.py b/pex/pip.py
--- a/pex/pip.py
+++ b/pex/pip.py
@@ -12,6 +12,7 @@
 from pex.compatibility import urlparse
 from pex.distribution_target import DistributionTarget
 from pex.jobs import Job
+from pex.tracer import TRACER
 from pex.variables import ENV
 
 
@@ -70,6 +71,12 @@ def _spawn_pip_isolated(self, args, cache=None, interpreter=None):
 
     command = pip_args + args
     with ENV.strip().patch(PEX_ROOT=ENV.PEX_ROOT, PEX_VERBOSE=str(pex_verbosity)) as env:
+      # Guard against API calls from environment with ambient PYTHONPATH preventing pip PEX
+      # bootstrapping. See: https://github.com/pantsbuild/pex/issues/892
+      pythonpath = env.pop('PYTHONPATH', None)
+      if pythonpath:
+        TRACER.log('Scrubbed PYTHONPATH={} from the pip PEX environment.'.format(pythonpath), V=3)
+
       from pex.pex import PEX
       pip = PEX(pex=self._pip_pex_path, interpreter=interpreter)
       return Job(


Test patch:
diff --git a/tests/test_resolver.py b/tests/test_resolver.py
--- a/tests/test_resolver.py
+++ b/tests/test_resolver.py
@@ -3,6 +3,8 @@
 
 import functools
 import os
+import subprocess
+from textwrap import dedent
 
 import pytest
 
@@ -308,3 +310,42 @@ def resolve_pytest(python_version, pytest_version):
   assert 'configparser' in resolved_project_to_version
   assert 'pathlib2' in resolved_project_to_version
   assert 'contextlib2' in resolved_project_to_version
+
+
+def test_issues_892():
+  python27 = ensure_python_interpreter(PY27)
+  program = dedent("""\
+    from __future__ import print_function
+
+    import os
+    import sys
+
+
+    # This puts python3.6 stdlib on PYTHONPATH.
+    os.environ['PYTHONPATH'] = os.pathsep.join(sys.path)
+
+
+    from pex import resolver
+    from pex.interpreter import PythonInterpreter
+
+
+    python27 = PythonInterpreter.from_binary({python27!r})
+    result = resolver.resolve(requirements=['packaging==19.2'], interpreter=python27)
+    print('Resolved: {{}}'.format(result))
+  """.format(python27=python27))
+
+  python36 = ensure_python_interpreter(PY36)
+  cmd, process = PythonInterpreter.from_binary(python36).open_process(
+    args=['-c', program],
+    stderr=subprocess.PIPE
+  )
+  _, stderr = process.communicate()
+  assert process.returncode == 0, dedent(
+    """
+    Command {cmd} failed with {returncode}.
+
+    STDERR
+    ======
+    {stderr}
+    """.format(cmd=cmd, returncode=process.returncode, stderr=stderr.decode('utf8'))
+  )