Repository: pex-tool/pex

Issue:
Release 2.1.118
On the docket:
+ [x] Overwriting sys.argv[0] leaves no remaining methods to identify the actual sys.argv[0] #2006
+ [x] Support for generating non-hermetic console scripts in venv #2004
Overwriting sys.argv[0] leaves no remaining methods to identify the actual sys.argv[0]
As of the 2.1.91 release, sys.argv[0] is no longer a reliable option for identifying the executed script as called. 

For example, creating a symlink to a pex, then calling that symlink yields a sys.argv[0] that does not point to the symlink. The PEX environment variable also does not point to the symlink.

Are there any known remaining ways to obtain argv[0] as provided by the calling binary?


Discussion:
Moved from "Release 2.1.117 #1996" to make way for #1998.
There is probably not. Let me dig up why that changed. My policy is to not break folks until the 3.x transition so this is a bug unless the change was intentional and fixed something else and that something else trumps this after debate.

You'll make my day if this broke your [conscript BusyBox](https://pypi.org/project/conscript/).
Yeah. so #1785 was 1st released in 2.1.91; so that's likely the root cause of the regression. I'll see what I can do today and get out a ... pretty luke warm hotfix at this point if possible.
Well, that was too easy. Removing the `os.realpath` added to the generated `__main__.py` by pex_builder left all unit tests and ITs passing. I think that means my old enemy macOS is involved, we'll see in CI. Its probably the @#$! weird stuff they do with the user tmp dir here in which case I'll need to fix the test instead of the production code - which was hacky if that's what I did.

Resolving patch:
diff --git a/pex/pex_builder.py b/pex/pex_builder.py
--- a/pex/pex_builder.py
+++ b/pex/pex_builder.py
@@ -127,7 +127,7 @@ def __entry_point_from_filename__(filename):
   sys.exit(2)
 
 __installed_from__ = os.environ.pop(__INSTALLED_FROM__, None)
-sys.argv[0] = os.path.realpath(__installed_from__ or sys.argv[0])
+sys.argv[0] = __installed_from__ or sys.argv[0]
 
 sys.path[0] = os.path.abspath(sys.path[0])
 sys.path.insert(0, os.path.abspath(os.path.join(__entry_point__, {bootstrap_dir!r})))
diff --git a/pex/version.py b/pex/version.py
--- a/pex/version.py
+++ b/pex/version.py
@@ -1,4 +1,4 @@
 # Copyright 2015 Pants project contributors (see CONTRIBUTORS.md).
 # Licensed under the Apache License, Version 2.0 (see LICENSE).
 
-__version__ = "2.1.117"
+__version__ = "2.1.118"


Test patch:
diff --git a/tests/integration/test_issue_1730.py b/tests/integration/test_issue_1730.py
--- a/tests/integration/test_issue_1730.py
+++ b/tests/integration/test_issue_1730.py
@@ -1,7 +1,7 @@
 # Copyright 2022 Pants project contributors (see CONTRIBUTORS.md).
 # Licensed under the Apache License, Version 2.0 (see LICENSE).
 
-from __future__ import absolute_import
+from __future__ import absolute_import, print_function
 
 import os
 
@@ -24,11 +24,17 @@ def test_check_install_issue_1730(
     # type: (...) -> None
 
     pex_root = os.path.join(str(tmpdir), "pex_root")
+    constraints = os.path.join(str(tmpdir), "constraints.txt")
+    with open(constraints, "w") as fp:
+        print("packaging==21.3", file=fp)
+
     pex_args = [
         "--pex-root",
         pex_root,
         "--runtime-pex-root",
         pex_root,
+        "--constraints",
+        constraints,
         "pantsbuild.pants.testutil==2.12.0.dev3",
         "--",
         "-c",
diff --git a/tests/integration/test_issue_2006.py b/tests/integration/test_issue_2006.py
new file mode 100644
--- /dev/null
+++ b/tests/integration/test_issue_2006.py
@@ -0,0 +1,70 @@
+# Copyright 2022 Pants project contributors (see CONTRIBUTORS.md).
+# Licensed under the Apache License, Version 2.0 (see LICENSE).
+
+import os.path
+import subprocess
+import sys
+
+import pytest
+
+from pex.compatibility import PY3
+from pex.testing import PY38, ensure_python_interpreter, run_pex_command
+from pex.typing import TYPE_CHECKING
+
+if TYPE_CHECKING:
+    from typing import Any, List
+
+
+@pytest.mark.parametrize(
+    ["boot_args"],
+    [
+        pytest.param([], id="__main__.py boot"),
+        pytest.param(["--sh-boot"], id="--sh-boot"),
+    ],
+)
+def test_symlink_preserved_in_argv0(
+    tmpdir,  # type: Any
+    boot_args,  # type: List[str]
+):
+    # type: (...) -> None
+
+    # N.B.: We use conscript (https://pypi.org/project/conscript/) here to test a very common real
+    # use case for knowing the original path used to launch an executable. In general this allows
+    # the executable to react differently based on its name. In the conscript case, it uses that
+    # name to select a matching console script in the PEX if one exists and run that. If no such
+    # match exists and there are no args, it launches a REPL session over the PEX contents.
+
+    pex = os.path.join(str(tmpdir), "speak.pex")
+    run_pex_command(
+        args=["conscript==0.1.5", "cowsay==5.0", "fortune==1.1.0", "-c", "conscript", "-o", pex]
+        + boot_args
+    ).assert_success()
+
+    assert (
+        "5.0" == subprocess.check_output(args=[pex, "cowsay", "--version"]).decode("utf-8").strip()
+    )
+
+    cowsay = os.path.join(str(tmpdir), "cowsay")
+    os.symlink(pex, cowsay)
+    assert "5.0" == subprocess.check_output(args=[cowsay, "--version"]).decode("utf-8").strip(), (
+        "Expected the symlink used to launch this PEX to be preserved in sys.argv[0] such that "
+        "conscript could observe it and select the cowsay console script inside the PEX for"
+        "execution. Without symlink preservation, the real PEX name of speak.pex will match no "
+        "internal console scripts and the conscript entry point will drop into a REPL session over "
+        "the PEX causing this test to hang waiting for a REPL session exit signal / command."
+    )
+
+    fortune_file = os.path.join(str(tmpdir), "fortunes.txt")
+    with open(fortune_file, "w") as fp:
+        fp.write("Just the one")
+    fortune = os.path.join(str(tmpdir), "fortune")
+    os.symlink(pex, fortune)
+
+    # N.B.: This fortune implementation uses print(..., file=...) without
+    # `from __future__ import print_function`; so fails under Python 2.7 despite the fact its
+    # released as a py2.py3 wheel.
+    python = sys.executable if PY3 else ensure_python_interpreter(PY38)
+    assert (
+        "Just the one"
+        == subprocess.check_output(args=[python, fortune, fortune_file]).decode("utf-8").strip()
+    )