Repository: pantsbuild/pants

Issue:
`packed` layout PEXes fail to `run` with `use_deprecated_pex_binary_run_semantics=false`
**Describe the bug**
With `use_deprecated_pex_binary_run_semantics=false`, a `layout="packed"` PEX will fail to run with:
```
Exception: Error executing interactive process: Permission denied (os error 13)
```

**Pants version**
`2.13.x` and `main`



Discussion:

Resolving patch:
diff --git a/src/python/pants/backend/python/goals/run_pex_binary.py b/src/python/pants/backend/python/goals/run_pex_binary.py
--- a/src/python/pants/backend/python/goals/run_pex_binary.py
+++ b/src/python/pants/backend/python/goals/run_pex_binary.py
@@ -9,7 +9,7 @@
     _create_python_source_run_request,
 )
 from pants.backend.python.subsystems.debugpy import DebugPy
-from pants.backend.python.target_types import PexBinaryDefaults
+from pants.backend.python.target_types import PexBinaryDefaults, PexLayout
 from pants.backend.python.util_rules.pex_environment import PexEnvironment
 from pants.core.goals.package import BuiltPackage
 from pants.core.goals.run import RunDebugAdapterRequest, RunFieldSet, RunRequest
@@ -31,6 +31,9 @@ async def create_pex_binary_run_request(
         built_pex = await Get(BuiltPackage, PexBinaryFieldSet, field_set)
         relpath = built_pex.artifacts[0].relpath
         assert relpath is not None
+        if field_set.layout.value != PexLayout.ZIPAPP.value:
+            relpath = os.path.join(relpath, "__main__.py")
+
         return RunRequest(
             digest=built_pex.digest,
             args=[os.path.join("{chroot}", relpath)],


Test patch:
diff --git a/src/python/pants/backend/python/goals/run_pex_binary_integration_test.py b/src/python/pants/backend/python/goals/run_pex_binary_integration_test.py
--- a/src/python/pants/backend/python/goals/run_pex_binary_integration_test.py
+++ b/src/python/pants/backend/python/goals/run_pex_binary_integration_test.py
@@ -4,11 +4,11 @@
 
 import json
 from textwrap import dedent
-from typing import Optional
+from typing import Callable, Optional
 
 import pytest
 
-from pants.backend.python.target_types import PexExecutionMode
+from pants.backend.python.target_types import PexExecutionMode, PexLayout
 from pants.testutil.pants_integration_test import PantsResult, run_pants, setup_tmpdir
 
 use_deprecated_semantics_args = pytest.mark.parametrize(
@@ -20,30 +20,14 @@
 )
 
 
-@pytest.mark.parametrize(
-    ("entry_point", "execution_mode", "include_tools"),
-    [
-        ("app.py", None, True),
-        ("app.py", None, True),
-        ("app.py", PexExecutionMode.VENV, False),
-        ("app.py:main", PexExecutionMode.ZIPAPP, True),
-        ("app.py:main", None, False),
-    ],
-)
-@use_deprecated_semantics_args
-def test_run_sample_script(
-    entry_point: str,
-    execution_mode: Optional[PexExecutionMode],
-    include_tools: bool,
+def run_generic_test(
+    *,
     use_deprecated_semantics_args: tuple[str, ...],
-) -> None:
-    """Test that we properly run a `pex_binary` target.
-
-    This checks a few things:
-    - We can handle source roots.
-    - We properly load third party requirements.
-    - We propagate the error code.
-    """
+    entry_point: str = "app.py",
+    execution_mode: Optional[PexExecutionMode] = None,
+    include_tools: bool = False,
+    layout: Optional[PexLayout] = None,
+) -> Callable[..., PantsResult]:
     sources = {
         "src_root1/project/app.py": dedent(
             """\
@@ -68,6 +52,7 @@ def main():
               entry_point={entry_point!r},
               execution_mode={execution_mode.value if execution_mode is not None else None!r},
               include_tools={include_tools!r},
+              layout={layout.value if layout is not None else None!r},
             )
             """
         ),
@@ -103,19 +88,53 @@ def run(*extra_args: str, **extra_env: str) -> PantsResult:
             return run_pants(args, extra_env=extra_env)
 
     result = run()
+
     assert "Hola, mundo.\n" in result.stderr
     file = result.stdout.strip()
     if use_deprecated_semantics_args:
         assert file.endswith("src_root2/utils/strutil.py")
         assert "pants-sandbox-" in file
     else:
+        assert "src_root2" not in file
         assert file.endswith("utils/strutil.py")
-        assert "pants-sandbox-" not in file
+        if layout == PexLayout.LOOSE:
+            # Loose PEXs execute their own code directly
+            assert "pants-sandbox-" in file
+        else:
+            assert "pants-sandbox-" not in file
     assert result.exit_code == 23
 
+    return run
+
+
+@pytest.mark.parametrize("entry_point", ["app.py", "app.py:main"])
+@use_deprecated_semantics_args
+def test_entry_point(
+    entry_point: str,
+    use_deprecated_semantics_args: tuple[str, ...],
+):
+    run_generic_test(
+        use_deprecated_semantics_args=use_deprecated_semantics_args, entry_point=entry_point
+    )
+
+
+@pytest.mark.parametrize("execution_mode", [None, PexExecutionMode.VENV])
+@pytest.mark.parametrize("include_tools", [True, False])
+@use_deprecated_semantics_args
+def test_execution_mode_and_include_tools(
+    execution_mode: Optional[PexExecutionMode],
+    include_tools: bool,
+    use_deprecated_semantics_args: tuple[str, ...],
+):
+    run = run_generic_test(
+        use_deprecated_semantics_args=use_deprecated_semantics_args,
+        execution_mode=execution_mode,
+        include_tools=include_tools,
+    )
+
     if include_tools:
         result = run("--", "info", PEX_TOOLS="1")
-        assert result.exit_code == 0
+        assert result.exit_code == 0, result.stderr
         pex_info = json.loads(result.stdout)
         assert (execution_mode is PexExecutionMode.VENV) == pex_info["venv"]
         assert ("prepend" if execution_mode is PexExecutionMode.VENV else "false") == pex_info[
@@ -127,6 +146,15 @@ def run(*extra_args: str, **extra_env: str) -> PantsResult:
             assert pex_info["strip_pex_env"]
 
 
+@pytest.mark.parametrize("layout", PexLayout)
+@use_deprecated_semantics_args
+def test_layout(
+    layout: Optional[PexLayout],
+    use_deprecated_semantics_args: tuple[str, ...],
+):
+    run_generic_test(use_deprecated_semantics_args=use_deprecated_semantics_args, layout=layout)
+
+
 @use_deprecated_semantics_args
 def test_no_strip_pex_env_issues_12057(use_deprecated_semantics_args: tuple[str, ...]) -> None:
     sources = {