Repository: pantsbuild/pants

Issue:
The `pex_binary` target is missing options to `--include-tools` and `--unzip`.
The `--include-tools` option came up over in https://github.com/pantsbuild/pex/issues/1181 where a Pants user wanted to run a PEX of their application via Nginx Unit's Python ASGI support.


Discussion:

Resolving patch:
diff --git a/src/python/pants/backend/python/goals/package_pex_binary.py b/src/python/pants/backend/python/goals/package_pex_binary.py
--- a/src/python/pants/backend/python/goals/package_pex_binary.py
+++ b/src/python/pants/backend/python/goals/package_pex_binary.py
@@ -10,11 +10,13 @@
     PexEmitWarningsField,
     PexEntryPointField,
     PexIgnoreErrorsField,
+    PexIncludeToolsField,
     PexInheritPathField,
 )
 from pants.backend.python.target_types import PexPlatformsField as PythonPlatformsField
 from pants.backend.python.target_types import (
     PexShebangField,
+    PexUnzipField,
     PexZipSafeField,
     ResolvedPexEntryPoint,
     ResolvePexEntryPointRequest,
@@ -50,6 +52,8 @@ class PexBinaryFieldSet(PackageFieldSet, RunFieldSet):
     shebang: PexShebangField
     zip_safe: PexZipSafeField
     platforms: PythonPlatformsField
+    unzip: PexUnzipField
+    include_tools: PexIncludeToolsField
 
     def generate_additional_args(self, pex_binary_defaults: PexBinaryDefaults) -> Tuple[str, ...]:
         args = []
@@ -65,6 +69,10 @@ def generate_additional_args(self, pex_binary_defaults: PexBinaryDefaults) -> Tu
             args.append(f"--python-shebang={self.shebang.value}")
         if self.zip_safe.value is False:
             args.append("--not-zip-safe")
+        if self.unzip.value is True:
+            args.append("--unzip")
+        if self.include_tools.value is True:
+            args.append("--include-tools")
         return tuple(args)
 
 
diff --git a/src/python/pants/backend/python/target_types.py b/src/python/pants/backend/python/target_types.py
--- a/src/python/pants/backend/python/target_types.py
+++ b/src/python/pants/backend/python/target_types.py
@@ -254,6 +254,27 @@ def value_or_global_default(self, pex_binary_defaults: PexBinaryDefaults) -> boo
         return self.value
 
 
+class PexUnzipField(BoolField):
+    alias = "unzip"
+    default = False
+    value: bool
+    help = (
+        "Whether to have the PEX unzip itself into the PEX_ROOT before running.\n\nEnabling unzip "
+        "mode can provide lower startup latencies for most PEX files; even on first run."
+    )
+
+
+class PexIncludeToolsField(BoolField):
+    alias = "include_tools"
+    default = False
+    value: bool
+    help = (
+        "Whether to include Pex tools in the PEX bootstrap code.\n\nWith tools included, the "
+        "generated PEX file can be executed with `PEX_TOOLS=1 <pex file> --help` to gain access "
+        "to all the available tools."
+    )
+
+
 class PexBinary(Target):
     alias = "pex_binary"
     core_fields = (
@@ -268,6 +289,8 @@ class PexBinary(Target):
         PexIgnoreErrorsField,
         PexShebangField,
         PexEmitWarningsField,
+        PexUnzipField,
+        PexIncludeToolsField,
     )
     help = (
         "A Python target that can be converted into an executable PEX file.\n\nPEX files are "


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
@@ -1,21 +1,21 @@
 # Copyright 2020 Pants project contributors (see CONTRIBUTORS.md).
 # Licensed under the Apache License, Version 2.0 (see LICENSE).
-
+import json
 from textwrap import dedent
 
 import pytest
 
-from pants.testutil.pants_integration_test import run_pants, setup_tmpdir
+from pants.testutil.pants_integration_test import PantsResult, run_pants, setup_tmpdir
 
 
 @pytest.mark.parametrize(
-    "tgt_content",
+    ("entry_point", "unzip", "include_tools"),
     [
-        "python_library(name='lib')\npex_binary(entry_point='app.py')",
-        "python_library(name='lib')\npex_binary(entry_point='app.py:main')",
+        ("app.py", True, True),
+        ("app.py:main", False, False),
     ],
 )
-def test_run_sample_script(tgt_content: str) -> None:
+def test_run_sample_script(entry_point: str, unzip: bool, include_tools: bool) -> None:
     """Test that we properly run a `pex_binary` target.
 
     This checks a few things:
@@ -39,7 +39,16 @@ def main():
               main()
             """
         ),
-        "src_root1/project/BUILD": tgt_content,
+        "src_root1/project/BUILD": dedent(
+            f"""\
+            python_library(name='lib')
+            pex_binary(
+              entry_point={entry_point!r},
+              unzip={unzip!r},
+              include_tools={include_tools!r},
+            )
+            """
+        ),
         "src_root2/utils/strutil.py": dedent(
             """\
             def upper_case(s):
@@ -48,18 +57,27 @@ def upper_case(s):
         ),
         "src_root2/utils/BUILD": "python_library()",
     }
-    with setup_tmpdir(sources) as tmpdir:
-        result = run_pants(
-            [
+
+    def run(*extra_args: str, **extra_env: str) -> PantsResult:
+        with setup_tmpdir(sources) as tmpdir:
+            args = [
                 "--backend-packages=pants.backend.python",
                 f"--source-root-patterns=['/{tmpdir}/src_root1', '/{tmpdir}/src_root2']",
                 "--pants-ignore=__pycache__",
                 "--pants-ignore=/src/python",
                 "run",
                 f"{tmpdir}/src_root1/project/app.py",
+                *extra_args,
             ]
-        )
+            return run_pants(args, extra_env=extra_env)
 
+    result = run()
     assert "Hola, mundo.\n" in result.stderr
     assert result.stdout == "HELLO WORLD.\n"
     assert result.exit_code == 23
+
+    if include_tools:
+        result = run("--", "info", PEX_TOOLS="1")
+        assert result.exit_code == 0
+        pex_info = json.loads(result.stdout)
+        assert unzip == pex_info["unzip"]