diff --git a/src/python/pants/backend/python/goals/package_pex_binary.py b/src/python/pants/backend/python/goals/package_pex_binary.py index d8944bef8c0..cb3ef45519b 100644 --- a/src/python/pants/backend/python/goals/package_pex_binary.py +++ b/src/python/pants/backend/python/goals/package_pex_binary.py @@ -24,6 +24,7 @@ PexPlatformsField, PexResolveLocalPlatformsField, PexScriptField, + PexShBootField, PexShebangField, PexStripEnvField, PexVenvHermeticScripts, @@ -64,6 +65,7 @@ class PexBinaryFieldSet(PackageFieldSet, RunFieldSet): emit_warnings: PexEmitWarningsField ignore_errors: PexIgnoreErrorsField inherit_path: PexInheritPathField + sh_boot: PexShBootField shebang: PexShebangField strip_env: PexStripEnvField platforms: PexPlatformsField @@ -92,6 +94,8 @@ def generate_additional_args(self, pex_binary_defaults: PexBinaryDefaults) -> Tu args.append("--ignore-errors") if self.inherit_path.value is not None: args.append(f"--inherit-path={self.inherit_path.value}") + if self.sh_boot.value is True: + args.append("--sh-boot") if self.shebang.value is not None: args.append(f"--python-shebang={self.shebang.value}") if self.strip_env.value is False: diff --git a/src/python/pants/backend/python/goals/package_pex_binary_integration_test.py b/src/python/pants/backend/python/goals/package_pex_binary_integration_test.py index 3fb92910036..1b61ada9376 100644 --- a/src/python/pants/backend/python/goals/package_pex_binary_integration_test.py +++ b/src/python/pants/backend/python/goals/package_pex_binary_integration_test.py @@ -418,3 +418,37 @@ def execute_pex(address: Address, **extra_env) -> Results: ) assert "bob" == non_hermetic_results.pythonpath assert bob_sys_path_entry in non_hermetic_results.sys_path + + +def test_sh_boot_plumb(rule_runner: PythonRuleRunner) -> None: + rule_runner.write_files( + { + "src/py/project/app.py": dedent( + """\ + print("hello") + """ + ), + "src/py/project/BUILD": dedent( + """\ + python_sources(name="lib") + pex_binary( + entry_point="app.py", + sh_boot=True + ) + """ + ), + } + ) + tgt = rule_runner.get_target(Address("src/py/project")) + field_set = PexBinaryFieldSet.create(tgt) + result = rule_runner.request(BuiltPackage, [field_set]) + assert len(result.artifacts) == 1 + expected_pex_relpath = "src.py.project/project.pex" + assert expected_pex_relpath == result.artifacts[0].relpath + + rule_runner.write_digest(result.digest) + + executable = os.path.join(rule_runner.build_root, expected_pex_relpath) + with open(executable, "rb") as f: + shebang = f.readline().decode() + assert "#!/bin/sh" in shebang diff --git a/src/python/pants/backend/python/target_types.py b/src/python/pants/backend/python/target_types.py index 0664c197d8c..d6c6ae36512 100644 --- a/src/python/pants/backend/python/target_types.py +++ b/src/python/pants/backend/python/target_types.py @@ -496,6 +496,29 @@ class PexIgnoreErrorsField(BoolField): help = "Should PEX ignore errors when it cannot resolve dependencies?" +class PexShBootField(BoolField): + alias = "sh_boot" + default = False + help = help_text( + """ + Should PEX create a modified ZIPAPP that uses `/bin/sh` to boot? + + If you know the machines that the PEX will be distributed to have + POSIX compliant `/bin/sh` (almost all do, see: + https://pubs.opengroup.org/onlinepubs/9699919799/utilities/sh.html); + then this is probably the way you want your PEX to boot. Instead of + launching via a Python shebang, the PEX will launch via a `#!/bin/sh` + shebang that executes a small script embedded in the head of the PEX + ZIPAPP that performs initial interpreter selection and re-execution of + the underlying PEX in a way that is often more robust than a Python + shebang and always faster on 2nd and subsequent runs since the sh + script has a constant overhead of O(1ms) whereas the Python overhead + to perform the same interpreter selection and re-execution is + O(100ms). + """ + ) + + class PexShebangField(StringField): alias = "shebang" help = help_text( @@ -680,6 +703,7 @@ class PexVenvHermeticScripts(BoolField): PexInheritPathField, PexStripEnvField, PexIgnoreErrorsField, + PexShBootField, PexShebangField, PexEmitWarningsField, PexLayoutField,