Repository: pex-tool/pex

Issue:
Empty string PEX_PATH="" env var causes CWD (.) to be added bootstrapped pex_path
The CWD doesn't usually contain a pex so the the pex being executed fails to bootstrap with a pex_info load error looking for a file `./PEX_INFO`.

Ran into this issue when upgrading from 2.1.97 -> 2.1.105. Was able to work around it by unsetting PEX_PATH entirely, but if this is a regression I can look into the root cause. Is this an intentional change?




Discussion:
Sounds like a regression to me. It broke you!
I added this to the 2.1.109 release since it should be a very tiny, very easy fix. @hrfuller do you want to grab it or should I? I'm looking to release 2.1.109 today.

Resolving patch:
diff --git a/pex/variables.py b/pex/variables.py
--- a/pex/variables.py
+++ b/pex/variables.py
@@ -301,11 +301,17 @@ def _get_int(self, variable):
                 % (variable, self._environ[variable])
             )
 
-    def _maybe_get_path_tuple(self, variable):
-        # type: (str) -> Optional[Tuple[str, ...]]
+    def _maybe_get_path_tuple(
+        self,
+        variable,  # type: str
+        empty_string_is_cwd=True,  # type: bool
+    ):
+        # type: (...) -> Optional[Tuple[str, ...]]
         value = self._maybe_get_string(variable)
         if value is None:
             return None
+        if not value and not empty_string_is_cwd:
+            return None
         return tuple(
             OrderedSet(os.path.normpath(os.path.expanduser(p)) for p in value.split(os.pathsep))
         )
@@ -600,7 +606,7 @@ def PEX_PATH(self):
 
         See also PEX_EXTRA_SYS_PATH for how to add arbitrary entries to the sys.path.
         """
-        return self._maybe_get_path_tuple("PEX_PATH") or ()
+        return self._maybe_get_path_tuple("PEX_PATH", empty_string_is_cwd=False) or ()
 
     @property
     def PEX_SCRIPT(self):


Test patch:
diff --git a/tests/integration/test_issue_1936.py b/tests/integration/test_issue_1936.py
new file mode 100644
--- /dev/null
+++ b/tests/integration/test_issue_1936.py
@@ -0,0 +1,38 @@
+# Copyright 2022 Pants project contributors (see CONTRIBUTORS.md).
+# Licensed under the Apache License, Version 2.0 (see LICENSE).
+
+import os.path
+import subprocess
+
+from pex.testing import make_env, run_pex_command
+from pex.typing import TYPE_CHECKING
+
+if TYPE_CHECKING:
+    from typing import Any
+
+
+def test_empty_pex_path(tmpdir):
+    # type: (Any) -> None
+
+    empty_pex = os.path.join(str(tmpdir), "empty.pex")
+    run_pex_command(args=["-o", empty_pex]).assert_success()
+
+    # The previously failing case.
+    subprocess.check_call(args=[empty_pex, "-c", ""], env=make_env(PEX_PATH=""))
+
+    colors_pex = os.path.join(str(tmpdir), "colors.pex")
+    run_pex_command(
+        args=["ansicolors==1.1.8", "-o", colors_pex, "--layout", "packed"]
+    ).assert_success()
+
+    assert 0 != subprocess.call(
+        args=[empty_pex, "-c", "import colors"]
+    ), "Expected a PEX_PATH including colors.pex to be needed in order to import colors."
+    subprocess.check_call(
+        args=[empty_pex, "-c", "import colors"], env=make_env(PEX_PATH=colors_pex)
+    )
+    # Demonstrate that . can be used instead of the empty string to trigger a CWD PEX_PATH if that
+    # is what is intended.
+    subprocess.check_call(
+        args=[empty_pex, "-c", "import colors"], env=make_env(PEX_PATH="."), cwd=colors_pex
+    )
diff --git a/tests/test_variables.py b/tests/test_variables.py
--- a/tests/test_variables.py
+++ b/tests/test_variables.py
@@ -258,3 +258,11 @@ def test_warnings():
             for env_var in ("PEX_ALWAYS_CACHE", "PEX_FORCE_LOCAL", "PEX_UNZIP")
         )
     ) == tuple(sorted(warning_by_message_first_sentence))
+
+
+def test_empty_pex_path_issue_1936():
+    # type: () -> None
+
+    assert () == Variables(environ={}).PEX_PATH
+    assert () == Variables(environ={"PEX_PATH": ""}).PEX_PATH
+    assert (".",) == Variables(environ={"PEX_PATH": "."}).PEX_PATH