Skip to content

Commit 67d9d7d

Browse files
Implement run-time Python interpreter searching
There is currently an assumption that the Python interpreter used while building .pex files (i.e. the value of `DefaultInterpreter`) is also the Python interpreter that runs them. This can be overridden with the `shebang` parameter on individual `python_binary` and `python_test` targets, which sets the shebang in the generated .pex file to a specific value. The functionality this provides is limited: - If additional interpreter arguments are specified by the target, the generated .pex file will depend on a shell at `/bin/sh` in the run-time environment (which isn't guaranteed to exist, e.g. in distroless containers), as well as `/usr/bin/env` (also not guaranteed to exist). - It is possible to build the .pex file using a Python interpreter defined as a build target in the repo, but it isn't possible to run the .pex file using that interpreter because the .pex file's shebang is (by definition) static, but the interpreter's path relative to the .pex file's path varies depending on the context (e.g. whether the .pex file exists inside a build environment vs outside of one). This was discussed in please-build#247. Provide more flexibility in how .pex files are run by prepending a native-code preamble that implements dynamic searching of Python interpreters. The behaviour of this preamble is controlled at run time by a special zip member within the .pex file, `.bootstrap/PLZ_PREAMBLE_CONFIG`, which contains a list of candidate paths for Python interpreters under which the .pex file should be executed and a list of arguments to pass to the interpreter when attempting to execute it. The lists can be customised per `python_binary` or `python_test` target via the `runtime_interpreters` and `runtime_interpreter_args` parameters respectively; new plugin configuration options, `DefaultRuntimeInterpreters` and `DefaultRuntimeInterpreterArgs`, provide default values. If both are unspecified, the plugin's current behaviour is maintained (i.e. the same interpreter is used to both build and run the code). The flexibility offered by the new .pex preamble is sufficient to be able to remove `python_binary` and `python_test`'s `shebang` parameter (and the `DefaultShebang` plugin configuration option) altogether. The new .pex preamble is compact enough to prepend to all .pex files in a repo without collectively making them prohibitively large: ~98KB statically linked with musl on Linux, ~83KB dynamically linked on Darwin (which doesn't have a stable kernel ABI, and strongly discourages static linking). Among other possibilities, this allows for the creation of .pex files that are truly portable between run-time environments of the same operating system and architecture - e.g., the preamble can be configured to attempt to execute an interpreter that only exists within a production environment (such as a distroless container with its interpreter at `/python/bin/python3`), falling back on an interpreter defined as a build target in the repo if the .pex file is still located within Please's development/testing environment.
1 parent 3c3513a commit 67d9d7d

File tree

28 files changed

+1252
-111
lines changed

28 files changed

+1252
-111
lines changed

.github/workflows/plugin_test.yaml

Lines changed: 18 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,8 @@ jobs:
2828
runs-on: ${{ (inputs.platform == 'freebsd_amd64' || inputs.platform == 'linux_amd64') && 'ubuntu-24.04' || (inputs.platform == 'linux_arm64' && 'ubuntu-24.04-arm' || (inputs.platform == 'darwin_amd64' && 'macos-15-intel' || (inputs.platform == 'darwin_arm64' && 'macos-15' || 'unknown'))) }}
2929
steps:
3030
- name: Install Python in CI environment
31+
# actions/setup-python doesn't support FreeBSD - instead, interpreters for the supported Python versions are
32+
# pre-installed in the FreeBSD runner image.
3133
if: ${{ inputs.platform != 'freebsd_amd64' }}
3234
id: python
3335
uses: actions/setup-python@v6
@@ -36,14 +38,11 @@ jobs:
3638
update-environment: false
3739
- name: Check out code
3840
uses: actions/checkout@v5
39-
- name: Configure plugin's default Python interpreter
40-
run: |
41-
_python_path="${{ inputs.platform == 'freebsd_amd64' && format('python{0}', inputs.python) || steps.python.outputs.python-path }}"
42-
echo "PLZ_ARGS=-o plugin.python.defaultinterpreter:$_python_path" >> $GITHUB_ENV
4341
- name: Configure plugin to use please_pex tool built from source
4442
if: inputs.please_pex_from_repo
45-
run: |
46-
echo "PLZ_ARGS=${PLZ_ARGS:+$PLZ_ARGS }-o plugin.python.pextool://tools/please_pex" >> $GITHUB_ENV
43+
# PexTool can't be set to //tools/please_pex for e2e tests because it can't be built inside the e2e test
44+
# environment - skip the e2e tests if we're using //tools/please_pex as the pex tool.
45+
run: echo "PLZ_ARGS=${PLZ_ARGS:+$PLZ_ARGS }-o plugin.python.pextool://tools/please_pex -e plz_e2e_test" >> $GITHUB_ENV
4746
- name: Run tests (in nested runner)
4847
if: ${{ inputs.platform == 'freebsd_amd64' }}
4948
uses: cross-platform-actions/[email protected]
@@ -55,11 +54,22 @@ jobs:
5554
shell: bash
5655
environment_variables: PLZ_ARGS
5756
shutdown_vm: true
58-
run: ./pleasew test --keep_going --log_file plz-out/log/test.log ${{ inputs.test_targets }}
57+
run: |
58+
_external_interpreter="python${{ inputs.python }}"
59+
_in_repo_interpreter="//third_party/cc/cpython:cpython_${{ inputs.python }}|python"
60+
echo "*** Running tests using external Python interpreter: $_external_interpreter"
61+
./pleasew -o "plugin.python.defaultinterpreter:$_external_interpreter" test --rerun --keep_going --log_file plz-out/log/test-external_interpreter.log ${{ inputs.test_targets }}
62+
echo "*** Running tests using in-repo Python interpreter: $_in_repo_interpreter"
63+
./pleasew -o "plugin.python.defaultinterpreter:$_in_repo_interpreter" test --rerun --keep_going --log_file plz-out/log/test-in_repo_interpreter.log ${{ inputs.test_targets }}
5964
- name: Run tests (on host)
6065
if: ${{ inputs.platform != 'freebsd_amd64' }}
6166
run: |
62-
./pleasew test --keep_going --log_file plz-out/log/test.log ${{ inputs.test_targets }}
67+
_external_interpreter="${{ steps.python.outputs.python-path }}"
68+
_in_repo_interpreter="//third_party/cc/cpython:cpython_${{ inputs.python }}|python"
69+
echo "*** Running tests using external Python interpreter: $_external_interpreter"
70+
./pleasew -o "plugin.python.defaultinterpreter:$_external_interpreter" test --rerun --keep_going --log_file plz-out/log/test-external_interpreter.log ${{ inputs.test_targets }}
71+
echo "*** Running tests using in-repo Python interpreter: $_in_repo_interpreter"
72+
./pleasew -o "plugin.python.defaultinterpreter:$_in_repo_interpreter" test --rerun --keep_going --log_file plz-out/log/test-in_repo_interpreter.log ${{ inputs.test_targets }}
6373
- name: Archive logs
6474
uses: actions/upload-artifact@v4
6575
with:

.plzconfig

Lines changed: 46 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,31 @@
11
[Please]
2-
Version = >=17.21.0
2+
Version = >=17.25.1
33

44
[Build]
55
hashcheckers = sha256
66

77
[Plugin "cc"]
88
Target = //plugins:cc
9+
LdGarbageCollection = true
10+
11+
DefaultOptCFlags = -std=c99
12+
DefaultOptCFlags = -Os
13+
DefaultOptCFlags = -DNDEBUG
14+
DefaultOptCFlags = -Wall
15+
DefaultOptCFlags = -Wextra
16+
DefaultOptCFlags = -Werror
17+
DefaultOptCFlags = -flto
18+
DefaultOptCFlags = -D_LARGEFILE64_SOURCE
19+
DefaultOptCFlags = -D_FILE_OFFSET_BITS=64
20+
21+
DefaultDbgCFlags = -std=c99
22+
DefaultDbgCFlags = -g3
23+
DefaultDbgCFlags = -DDEBUG
24+
DefaultDbgCFlags = -Wall
25+
DefaultDbgCFlags = -Wextra
26+
DefaultDbgCFlags = -Werror
27+
DefaultDbgCFlags = -D_LARGEFILE64_SOURCE
28+
DefaultDbgCFlags = -D_FILE_OFFSET_BITS=64
929

1030
[Plugin "go"]
1131
Target = //plugins:go
@@ -21,6 +41,7 @@ Target = //plugins:shell
2141

2242
[Plugin "e2e"]
2343
Target = //plugins:e2e
44+
PleaseVersion = 17.25.1
2445
DefaultPlugin = //test:python-rules
2546

2647
[PluginDefinition]
@@ -30,25 +51,34 @@ name = python
3051
ConfigKey = DefaultInterpreter
3152
DefaultValue = python3
3253
Inherit = true
33-
Help = The Python interpreter to use for any case where one is not explicitly specified
54+
Help = The default Python interpreter to use when building .pex files.
3455

35-
[PluginConfig "default_shebang"]
56+
[PluginConfig "interpreter_options"]
57+
ConfigKey = InterpreterOptions
58+
Optional = true
59+
Repeatable = true
60+
Help = A list of additional arguments to pass to the Python interpreter when building .pex files.
61+
62+
[PluginConfig "default_runtime_interpreters"]
63+
ConfigKey = DefaultRuntimeInterpreters
64+
Optional = true
65+
Repeatable = true
3666
Inherit = true
67+
Help = A list of possible paths to Python interpreters with which to run .pex files.
68+
69+
[PluginConfig "default_runtime_interpreter_args"]
70+
ConfigKey = DefaultRuntimeInterpreterArgs
3771
Optional = true
38-
Help = The shebang to set on binaries / tests where one is not explicitly specified
72+
Repeatable = true
73+
Inherit = true
74+
Help = A list of additional arguments to pass to the Python interpreter when running .pex files.
3975

4076
[PluginConfig "pex_tool"]
4177
ConfigKey = PexTool
4278
DefaultValue = //tools:please_pex
4379
Inherit = true
4480
Help = A path or build label for the pex tool, which is used to create .pex files from python sources
4581

46-
[PluginConfig "interpreter_options"]
47-
ConfigKey = InterpreterOptions
48-
Optional = true
49-
Repeatable = true
50-
Help = Additional command line arguments to pass to the Python interpreter.
51-
5282
[PluginConfig "test_runner"]
5383
ConfigKey = TestRunner
5484
DefaultValue = unittest
@@ -146,6 +176,12 @@ DefaultValue = "info"
146176
Help = Passes verbosity level to wheel_tool, for example: wheel_tool --verbosity info
147177
Optional = true
148178

179+
[PluginConfig "default_preamble_verbosity"]
180+
ConfigKey = DefaultPreambleVerbosity
181+
DefaultValue = error
182+
Inherit = true
183+
Help = The default logging level for the .pex preamble. Must be one of trace, debug, info, warn, error or fatal.
184+
149185
[PluginConfig "feature_flags"]
150186
DefaultValue = ""
151187
Repeatable = true

0 commit comments

Comments
 (0)