Repository: pantsbuild/pants

Issue:
Pants run `pex_binary` script broken
**Describe the bug**

I get: 
```
$ PANTS_SHA=961a52c9cdba841752527e7064d2ff09c421ae12 ./pants run :test -- moo
[...]
11:26:44.87 [ERROR] 1 Exception encountered:

  ProcessExecutionFailure: Process 'Building test.pex' failed with exit code 1.
stdout:

stderr:
Traceback (most recent call last):
[...]
  File "/Users/x/.cache/pants/named_caches/pex_root/installed_wheels/dae57b1c3877fc0567acc0103c6437da021bcfdf/pex-2.1.56-py2.py3-none-any.whl/pex/pex_builder.py", line 386, in set_script
    script, ", ".join(str(d) for d in distributions)
pex.pex_builder.InvalidExecutableSpecification: Could not find script 'cowsay' in any distribution  within PEX!

```

**Pants version**
Bleeding edge.

**OS**
MacOS.

**Additional info**

This works on `2.9.0.dev2`.
Also, running `./pants package :test` produces a working pex, so it is only `run` that is affected.


Reproduction:
```toml
[GLOBAL]
pants_version = "2.9.0.dev2"

backend_packages = [
  "pants.backend.python",
]
```

```python
python_requirement(
    name="cowsay", requirements=["cowsay==4.0"]
)

pex_binary(
    name="test",
    script="cowsay",
    dependencies=[":cowsay"]
)
```



Discussion:
This was broken by #13693 
OK, I've narrowed it down being due to that the requirements pex are split from the entry point pex, so when `main` is a script that comes from a requirement, we can't build the entry point pex:

https://github.com/pantsbuild/pants/blob/961a52c9cdba841752527e7064d2ff09c421ae12/src/python/pants/backend/python/goals/run_pex_binary.py#L60



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
@@ -12,11 +12,11 @@
 )
 from pants.backend.python.util_rules.interpreter_constraints import InterpreterConstraints
 from pants.backend.python.util_rules.local_dists import LocalDistsPex, LocalDistsPexRequest
-from pants.backend.python.util_rules.pex import Pex, PexRequest
+from pants.backend.python.util_rules.pex import Pex
 from pants.backend.python.util_rules.pex_environment import PexEnvironment
 from pants.backend.python.util_rules.pex_from_targets import (
     InterpreterConstraintsRequest,
-    RequirementsPexRequest,
+    PexFromTargetsRequest,
 )
 from pants.backend.python.util_rules.python_sources import (
     PythonSourceFiles,
@@ -50,15 +50,17 @@ async def create_pex_binary_run_request(
         InterpreterConstraints, InterpreterConstraintsRequest(addresses)
     )
 
-    entry_point_pex_get = Get(
+    pex_get = Get(
         Pex,
-        PexRequest(
+        PexFromTargetsRequest(
+            [field_set.address],
             output_filename=f"{field_set.address.target_name}.pex",
             internal_only=True,
+            include_source_files=False,
             # Note that the file for first-party entry points is not in the PEX itself. In that
             # case, it's loaded by setting `PEX_EXTRA_SYS_PATH`.
             main=entry_point.val or field_set.script.value,
-            interpreter_constraints=interpreter_constraints,
+            resolve_and_lockfile=field_set.resolve.resolve_and_lockfile(python_setup),
             additional_args=(
                 *field_set.generate_additional_args(pex_binary_defaults),
                 # N.B.: Since we cobble together the runtime environment via PEX_EXTRA_SYS_PATH
@@ -68,20 +70,10 @@ async def create_pex_binary_run_request(
             ),
         ),
     )
-    requirements_pex_get = Get(
-        Pex,
-        RequirementsPexRequest(
-            addresses=[field_set.address],
-            internal_only=True,
-            resolve_and_lockfile=field_set.resolve.resolve_and_lockfile(python_setup),
-        ),
-    )
     sources_get = Get(
         PythonSourceFiles, PythonSourceFilesRequest(transitive_targets.closure, include_files=True)
     )
-    entry_point_pex, requirements_pex, sources = await MultiGet(
-        entry_point_pex_get, requirements_pex_get, sources_get
-    )
+    pex, sources = await MultiGet(pex_get, sources_get)
 
     local_dists = await Get(
         LocalDistsPex,
@@ -97,10 +89,9 @@ async def create_pex_binary_run_request(
         Digest,
         MergeDigests(
             [
-                entry_point_pex.digest,
+                pex.digest,
                 local_dists.pex.digest,
                 local_dists.remaining_sources.source_files.snapshot.digest,
-                requirements_pex.digest,
             ]
         ),
     )
@@ -109,14 +100,12 @@ def in_chroot(relpath: str) -> str:
         return os.path.join("{chroot}", relpath)
 
     complete_pex_env = pex_env.in_workspace()
-    args = complete_pex_env.create_argv(
-        in_chroot(entry_point_pex.name), python=entry_point_pex.python
-    )
+    args = complete_pex_env.create_argv(in_chroot(pex.name), python=pex.python)
 
     chrooted_source_roots = [in_chroot(sr) for sr in sources.source_roots]
     extra_env = {
-        **complete_pex_env.environment_dict(python_configured=entry_point_pex.python is not None),
-        "PEX_PATH": ":".join([in_chroot(requirements_pex.name), in_chroot(local_dists.pex.name)]),
+        **complete_pex_env.environment_dict(python_configured=pex.python is not None),
+        "PEX_PATH": in_chroot(local_dists.pex.name),
         "PEX_EXTRA_SYS_PATH": os.pathsep.join(chrooted_source_roots),
     }
 


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
@@ -205,3 +205,27 @@ def test_local_dist() -> None:
         ]
         result = run_pants(args)
         assert result.stdout == "LOCAL DIST\n"
+
+
+def test_run_script_from_3rdparty_dist_issue_13747() -> None:
+    sources = {
+        "src/BUILD": dedent(
+            """\
+            python_requirement(name="cowsay", requirements=["cowsay==4.0"])
+            pex_binary(name="test", script="cowsay", dependencies=[":cowsay"])
+            """
+        ),
+    }
+    with setup_tmpdir(sources) as tmpdir:
+        SAY = "moooo"
+        args = [
+            "--backend-packages=pants.backend.python",
+            f"--source-root-patterns=['/{tmpdir}/src']",
+            "run",
+            f"{tmpdir}/src:test",
+            "--",
+            SAY,
+        ]
+        result = run_pants(args)
+        result.assert_success()
+        assert SAY in result.stdout.strip()