Repository: pex-tool/pex

Issue:
Auto-created --venv venvs in the PEX_ROOT have bad paths.
For example, in the activate script:
```
$ grep VIRTUAL_ENV= /home/jsirois/.pex/venvs/58fcefe170c11af69bfde52b33ba6424ccb3260f/800ae8ef047bd783543e8282c22bfdbee7b7fca8/bin/activate 
VIRTUAL_ENV="/home/jsirois/.pex/venvs/58fcefe170c11af69bfde52b33ba6424ccb3260f/800ae8ef047bd783543e8282c22bfdbee7b7fca8.9c28554a8e4c4931a664c3d329088975"
```
The `AtomicDirectory` `work_dir` ends up embedded instead of the final resting `target_dir`:
https://github.com/pantsbuild/pex/blob/4619b107b1e6c37cfd98a1b0f43bb4f7a33d3945/pex/common.py#L309-L313


Discussion:

Resolving patch:
diff --git a/pex/pex_bootstrapper.py b/pex/pex_bootstrapper.py
--- a/pex/pex_bootstrapper.py
+++ b/pex/pex_bootstrapper.py
@@ -374,8 +374,8 @@ def ensure_venv(pex):
             from .tools.commands.venv import populate_venv_with_pex
             from .tools.commands.virtualenv import Virtualenv
 
-            virtualenv = Virtualenv.create(
-                venv_dir=venv.work_dir,
+            virtualenv = Virtualenv.create_atomic(
+                venv_dir=venv,
                 interpreter=pex.interpreter,
                 copies=pex_info.venv_copies,
             )
diff --git a/pex/tools/commands/virtualenv.py b/pex/tools/commands/virtualenv.py
--- a/pex/tools/commands/virtualenv.py
+++ b/pex/tools/commands/virtualenv.py
@@ -9,7 +9,7 @@
 import sys
 from contextlib import closing
 
-from pex.common import is_exe, safe_mkdir
+from pex.common import AtomicDirectory, is_exe, safe_mkdir
 from pex.interpreter import PythonInterpreter
 from pex.third_party.pkg_resources import resource_string
 from pex.tracer import TRACER
@@ -26,12 +26,10 @@ class PipUnavailableError(Exception):
     """Indicates no local copy of Pip could be found for install."""
 
 
-def _iter_executables(directory):
+def _iter_files(directory):
     # type: (str) -> Iterator[str]
     for entry in os.listdir(directory):
-        path = os.path.join(directory, entry)
-        if is_exe(path):
-            yield path
+        yield os.path.join(directory, entry)
 
 
 def _is_python_script(executable):
@@ -52,8 +50,6 @@ def _is_python_script(executable):
                 (
                       python
                     | pypy
-                    | jython
-                    | ipy
                 )
                 # Optional Python version
                 (\d+(\.\d+)*)?
@@ -106,6 +102,22 @@ def create(
                 interpreter.execute(args=args)
         return cls(venv_dir)
 
+    @classmethod
+    def create_atomic(
+        cls,
+        venv_dir,  # type: AtomicDirectory
+        interpreter=None,  # type: Optional[PythonInterpreter]
+        force=False,  # type: bool
+        copies=False,  # type: bool
+    ):
+        # type: (...) -> Virtualenv
+        virtualenv = cls.create(
+            venv_dir=venv_dir.work_dir, interpreter=interpreter, force=force, copies=copies
+        )
+        for script in virtualenv._rewrite_base_scripts(real_venv_dir=venv_dir.target_dir):
+            TRACER.log("Re-writing {}".format(script))
+        return virtualenv
+
     def __init__(
         self,
         venv_dir,  # type: str
@@ -129,7 +141,7 @@ def __init__(
                 "site-packages",
             )
         )
-        self._base_executables = frozenset(_iter_executables(self._bin_dir))
+        self._base_bin = frozenset(_iter_files(self._bin_dir))
 
     @property
     def venv_dir(self):
@@ -161,7 +173,28 @@ def interpreter(self):
 
     def iter_executables(self):
         # type: () -> Iterator[str]
-        return _iter_executables(self._bin_dir)
+        for path in _iter_files(self._bin_dir):
+            if is_exe(path):
+                yield path
+
+    def _rewrite_base_scripts(self, real_venv_dir):
+        # type: (str) -> Iterator[str]
+        scripts = [
+            path
+            for path in self._base_bin
+            if _is_python_script(path) or re.search(r"^[Aa]ctivate", os.path.basename(path))
+        ]
+        if scripts:
+            rewritten_files = set()
+            with closing(fileinput.input(files=sorted(scripts), inplace=True)) as fi:
+                for line in fi:
+                    rewritten_line = line.replace(self._venv_dir, real_venv_dir)
+                    if rewritten_line != line:
+                        filename = fi.filename()
+                        if filename not in rewritten_files:
+                            rewritten_files.add(filename)
+                            yield filename
+                    sys.stdout.write(rewritten_line)
 
     def rewrite_scripts(
         self,
@@ -171,13 +204,13 @@ def rewrite_scripts(
         # type: (...) -> Iterator[str]
         python_scripts = []
         for executable in self.iter_executables():
-            if executable in self._base_executables:
+            if executable in self._base_bin:
                 continue
             if not _is_python_script(executable):
                 continue
             python_scripts.append(executable)
         if python_scripts:
-            with closing(fileinput.input(files=python_scripts, inplace=True)) as fi:
+            with closing(fileinput.input(files=sorted(python_scripts), inplace=True)) as fi:
                 # N.B.: `fileinput` is strange, but useful: the context manager above monkey-patches
                 # sys.stdout to print to the corresponding original input file, which is has moved
                 # aside.


Test patch:
diff --git a/tests/test_pex_bootstrapper.py b/tests/test_pex_bootstrapper.py
--- a/tests/test_pex_bootstrapper.py
+++ b/tests/test_pex_bootstrapper.py
@@ -3,6 +3,7 @@
 
 import os
 import shutil
+import subprocess
 import sys
 from textwrap import dedent
 
@@ -11,12 +12,14 @@
 from pex.common import temporary_dir
 from pex.interpreter import PythonInterpreter
 from pex.interpreter_constraints import UnsatisfiableInterpreterConstraintsError
-from pex.pex_bootstrapper import iter_compatible_interpreters
+from pex.pex import PEX
+from pex.pex_bootstrapper import ensure_venv, iter_compatible_interpreters
+from pex.pex_builder import PEXBuilder
 from pex.testing import PY27, PY35, PY36, ensure_python_interpreter
 from pex.typing import TYPE_CHECKING
 
 if TYPE_CHECKING:
-    from typing import AnyStr, Iterable, List, Optional
+    from typing import Any, Iterable, List, Optional
 
 
 def basenames(*paths):
@@ -169,6 +172,7 @@ def test_find_compatible_interpreters_bias_current():
 
 
 def test_find_compatible_interpreters_siblings_of_current_issues_1109():
+    # type: () -> None
     py27 = ensure_python_interpreter(PY27)
     py36 = ensure_python_interpreter(PY36)
 
@@ -182,3 +186,51 @@ def test_find_compatible_interpreters_siblings_of_current_issues_1109():
         assert [os.path.realpath(p) for p in (python36, python27)] == find_interpreters(
             path=[path_entry], preferred_interpreter=PythonInterpreter.from_binary(python36)
         )
+
+
+def test_ensure_venv_activate_issues_1276(tmpdir):
+    # type: (Any) -> None
+    pex_root = os.path.join(str(tmpdir), "pex_root")
+    pb = PEXBuilder()
+    pb.info.pex_root = pex_root
+    pb.info.venv = True
+    pb.freeze()
+
+    venv_pex = ensure_venv(PEX(pb.path()))
+
+    expected_python_bin_dir = (
+        subprocess.check_output(
+            args=[
+                venv_pex,
+                "-c",
+                "import os, sys; print(os.path.realpath(os.path.dirname(sys.executable)))",
+            ],
+        )
+        .decode("utf-8")
+        .strip()
+    )
+    bash_activate = os.path.join(os.path.dirname(venv_pex), "bin", "activate")
+
+    subprocess.check_call(
+        args=[
+            "/usr/bin/env",
+            "bash",
+            "-c",
+            dedent(
+                """\
+                source "{activate_script}"
+
+                actual_python_bin_dir="$(dirname "$(command -v python)")"
+                expected_python_bin_dir="{expected_python_bin_dir}"
+
+                if [ "${{actual_python_bin_dir}}" != "${{expected_python_bin_dir}}" ]; then
+                    echo >&2 "Actual Python Bin Dir: ${{actual_python_bin_dir}}"
+                    echo >&2 "Expected Python Bin Dir: ${{expected_python_bin_dir}}"
+                  exit 42
+                fi
+                """.format(
+                    activate_script=bash_activate, expected_python_bin_dir=expected_python_bin_dir
+                )
+            ),
+        ]
+    )