Repository: pex-tool/pex

Issue:
venv_dir calculation doesn't correctly handle PEX_PYTHON_PATH with symlinks.
I noticed a bug in the variables.venv_dir hash computation. If PEX_PYTHON_PATH has the path of a symlinked interpreter that interpreter value isn't added to the hash of the venv_dir. This can cause collisions in which interpreter is used to resolve dependencies via resolver.resolve. This happens because the canonical path to the interpreter is computed but the PEX_PYTHON_PATH is assumed to have canonical paths. [line in  question](https://github.com/pantsbuild/pex/blob/8c14e563493b8f4a85b4a3c735596dd55586a52b/pex/variables.py#L748)

I think it would be reasonable to map the PEX_PYTHON_PATH entries to their realpath's before comparing with the requested venv interpreter.


Discussion:
@hrfuller is this another issue you plan on tackling like #1881? I do see you've assigned yourself. If so I'll leave it alone and be happy to review when you get time to fix.

FWIW I like to use self-assignment, and then later the in-progress label, as light tracking of what is planned / in progress.
Yeah that was the idea @jsirois. I have figured out a workaround, but would like to upstream a more robust fix to pex. I'll add the in progress label when I start work on it.

Resolving patch:
diff --git a/pex/variables.py b/pex/variables.py
--- a/pex/variables.py
+++ b/pex/variables.py
@@ -765,7 +765,9 @@ def add_pex_path_items(pexes):
         if (
             not ENV.PEX_PYTHON_PATH
             or interpreter_binary.startswith(ENV.PEX_PYTHON_PATH)
-            or os.path.realpath(interpreter_binary).startswith(ENV.PEX_PYTHON_PATH)
+            or os.path.realpath(interpreter_binary).startswith(
+                tuple(os.path.realpath(p) for p in ENV.PEX_PYTHON_PATH)
+            )
         ):
             interpreter_path = interpreter_binary
     if interpreter_path:


Test patch:
diff --git a/tests/test_pip.py b/tests/test_pip.py
--- a/tests/test_pip.py
+++ b/tests/test_pip.py
@@ -4,6 +4,8 @@
 from __future__ import absolute_import
 
 import glob
+import hashlib
+import json
 import os
 import warnings
 
@@ -13,7 +15,7 @@
 from pex.common import safe_rmtree
 from pex.interpreter import PythonInterpreter
 from pex.jobs import Job
-from pex.pip.installation import get_pip
+from pex.pip.installation import _PIP, PipInstallation, get_pip
 from pex.pip.tool import PackageIndexConfiguration, Pip
 from pex.pip.version import PipVersion, PipVersionValue
 from pex.platforms import Platform
@@ -294,3 +296,41 @@ def test_create_confounding_env_vars_issue_1668(
         requirements=["ansicolors==1.1.8"], download_dir=download_dir
     ).wait()
     assert ["ansicolors-1.1.8-py2.py3-none-any.whl"] == os.listdir(download_dir)
+
+
+def test_pip_pex_interpreter_venv_hash_issue_1885(
+    create_pip,  # type: CreatePip
+    current_interpreter,  # type: PythonInterpreter
+    tmpdir,  # type: Any
+):
+    # type: (...) -> None
+    """This test is under the test_pip module because the resolver
+    doesn't allow the user to control the pip.pex interpreter constraints, so those intperpreter
+    constraints can't feed into the venv_dir hash. Therefore if
+    PEX_PYTHON_PATH is present and contains symlinks that aren't resolved the wrong pip pex venv
+    may end up being invoked by resolver.resolve because the pip.pex venv dir collisions.
+
+    This tests that that doesn't happen.
+    """
+    # Remove any existing pip.pex which may exist as a result of other test suites.
+    installation = PipInstallation(
+        interpreter=current_interpreter,
+        version=PipVersion.VENDORED,
+    )
+    del _PIP[installation]
+    binary = current_interpreter.binary
+    binary_link = os.path.join(str(tmpdir), "python")
+    os.symlink(binary, binary_link)
+    pip_w_linked_ppp = create_pip(current_interpreter, PEX_PYTHON_PATH=binary_link)
+    print("binary link real path resolves to: {}".format(os.path.realpath(binary_link)))
+    venv_contents_hash = hashlib.sha1(
+        json.dumps(
+            {
+                "pex_path": {},
+                "PEX_PYTHON_PATH": (binary_link,),
+                "interpreter": os.path.realpath(binary_link),
+            },
+            sort_keys=True,
+        ).encode("utf-8")
+    ).hexdigest()
+    assert venv_contents_hash in pip_w_linked_ppp._pip_pex.venv_dir