Skip to content

Commit 7fac02f

Browse files
committed
fix(bootstrap): unify executable and zipapp startup
Describe executables and ZIPs with one private application specification, and share environment preparation, storage ownership and packaging across ordinary binaries, legacy executable ZIPs and py_zipapp entry points. Use native POSIX exec to preserve the application PID, signals and terminal behavior, with a detached watcher for temporary-file cleanup. On Windows, wait for the application child and its console cleanup before removal. Publish complete persistent images without replacing concurrent entries. Preserve public providers, custom templates and stage-two behavior. Keep historical raw-template adapters available and support Python 3.9 ZIP loaders. Document cleanup lifetime and legacy compatibility limitations. Validation: 727 root targets pass; the launcher matrix passes 313 cases with two skips. Native checks pass on macOS, Windows and Linux arm64/x64, including final loader/ZIP checks on all four pools. Changed-file hooks, documentation, distribution and independent implementation audit pass.
1 parent 6f65150 commit 7fac02f

40 files changed

Lines changed: 5045 additions & 160 deletions

‎.bazelrc.deleted_packages‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,8 +28,8 @@ common --deleted_packages=gazelle/manifest/hasher
2828
common --deleted_packages=gazelle/manifest/test
2929
common --deleted_packages=gazelle/modules_mapping
3030
common --deleted_packages=gazelle/python
31-
common --deleted_packages=gazelle/pythonconfig
3231
common --deleted_packages=gazelle/python/private
32+
common --deleted_packages=gazelle/pythonconfig
3333
common --deleted_packages=tests/integration/bzlmod_lockfile
3434
common --deleted_packages=tests/integration/compile_pip_requirements
3535
common --deleted_packages=tests/integration/compile_pip_requirements_test_from_external_repo

‎docs/environment-variables.md‎

Lines changed: 63 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
::::{envvar} RULES_PYTHON_ADDITIONAL_INTERPRETER_ARGS
44

55
This variable allows for additional arguments to be provided to the Python interpreter
6-
at bootstrap time when the `bash` bootstrap is used. If
6+
at bootstrap time. If
77
`RULES_PYTHON_ADDITIONAL_INTERPRETER_ARGS` were provided as `-Xaaa`, then the command
88
would be:
99

@@ -20,6 +20,12 @@ in the command executed being:
2020
python /path/to/debugger.py --port 12345 --file /path/to/file.py
2121
```
2222

23+
The Bash entry point parses the first line with `read -a` and places these
24+
arguments before the target's `interpreter_args`. Python entry points use
25+
`shlex.split` and place them after the target arguments. This preserves each
26+
entry point's existing precedence. The variable is removed before the
27+
application runs, so nested launchers do not apply it again.
28+
2329
:::{seealso}
2430
The {bzl:obj}`interpreter_args` attribute.
2531

@@ -31,13 +37,16 @@ The guide on {any}`How to integrate a debugger`
3137
:::{versionchanged} 1.7.0
3238
Support added for {bzl:flag}`--bootstrap_impl=system_python`.
3339
:::
40+
:::{versionchanged} VERSION_NEXT_PATCH
41+
Target and additional interpreter arguments also apply to `python app.zip`.
42+
:::
3443

3544
::::
3645

3746
:::{envvar} RULES_PYTHON_BOOTSTRAP_VERBOSE
3847

3948
When `1`, debug information about bootstrapping of a program is printed to
40-
stderr.
49+
stderr. Temporary runtime directories are retained to help diagnose failures.
4150
:::
4251

4352
:::{envvar} RULES_PYTHON_BZLMOD_DEBUG
@@ -57,23 +66,61 @@ be removed in a subsequent major `rules_python` version. Defaults to `0` if unse
5766
Directory to use as the root for creating files necessary for bootstrapping so
5867
that a binary can run.
5968

60-
Only applicable when {bzl:flag}`--venvs_use_declare_symlink=no` is used.
61-
62-
When set, a binary will attempt to find a unique, reusable, location within this
63-
directory for the files it needs to create to aid startup. The files may not be
64-
deleted upon program exit; it is the responsibility of the caller to ensure
65-
cleanup.
66-
67-
Manually specifying the directory is useful to lower the overhead of
68-
extracting/creating files on every program execution. By using a location
69-
outside /tmp, longer lived programs don't have to worry about files in /tmp
70-
being cleaned up by the OS.
71-
72-
If not set, then a temporary directory will be created and deleted upon program
73-
exit.
69+
Applies to runtime-created virtual environments and to `py_zipapp_binary` and
70+
`py_zipapp_test`. Legacy executable ZIPs always use temporary extraction; their
71+
virtual environments cannot persist because they refer to that extraction.
72+
73+
When set, a binary reuses files beneath this directory. The caller owns their
74+
lifetime and must arrange cleanup. ZIP applications prepare a unique staging
75+
directory and publish it only after setup succeeds. Concurrent launches reuse
76+
the completed result. Startup refuses an incomplete existing cache rather than
77+
removing files another process may be using. Use a fresh extract root if an
78+
existing entry is damaged.
79+
80+
Each new cache entry is a directory symlink to a completed image in a hidden
81+
backing directory beside it. Publishing the symlink cannot replace another
82+
entry, including one created concurrently. The backing belongs to the caller
83+
once published. Removing just the symlink does not reclaim its image; clean
84+
the extract root when its applications are no longer running. Older cache
85+
entries stored directly as directories remain readable.
86+
87+
ZIP cache identities include application files, permissions, bootstrap code,
88+
interpreter options and resolved external-runtime facts. Updating a binary can
89+
leave older cache entries behind. Shell and Python entry points share the same
90+
image identity. Published directories follow the caller's umask.
91+
92+
When unset, bootstraps create temporary runtime directories. Bash entry points
93+
use `TMPDIR` or `/tmp`; Python entry points follow `tempfile`'s directory
94+
selection. On POSIX, an independent process removes these directories
95+
asynchronously after the original interpreter PID exits, including across
96+
exec. The application keeps its native PID, signal delivery and terminal job.
97+
Windows waits for the application child before removing its runtime. Console
98+
Ctrl-C is delivered by Windows; the bootstrap waits for the application's own
99+
cleanup and exit status without forwarding another interrupt.
100+
101+
The temporary lifetime ends with the original interpreter, even if a forked
102+
child outlives it. Such applications need a persistent extract root. Linux
103+
namespace PID 1 and child subreapers can adopt the cleanup process; waiting for
104+
every child can then block until application exit. Persistent extraction avoids
105+
that process. Namespace or cgroup shutdown can kill it before removal finishes,
106+
and SIGKILL during setup before registration cannot guarantee cleanup. On
107+
systems without a native exit watch or suitable Linux procfs, PID reuse can
108+
delay removal.
109+
110+
The lifetime and publication behavior above applies to the default application
111+
launchers. Raw templates and older custom rules exposing only `PyExecutableInfo`
112+
retain their existing behavior. Their Windows ZIP adapter re-extracts a persistent
113+
cache on every launch; directory links can make a repeated launch fail. Use
114+
temporary extraction for that compatibility path.
74115

75116
:::{versionadded} 1.2.0
76117
:::
118+
119+
:::{versionchanged} VERSION_NEXT_PATCH
120+
Ordinary and ZIP entry points preserve native POSIX execution and share
121+
failure-safe temporary cleanup. ZIP caches are published after preparation and
122+
include bootstrap inputs in their identity.
123+
:::
77124
::::
78125

79126
:::{envvar} RULES_PYTHON_GAZELLE_VERBOSE

‎news/bootstrap-runtime.fixed.md‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
(bootstrap) Fixed cancellation and temporary-runtime cleanup across ordinary
2+
binaries, legacy ZIPs, and both entry points of `py_zipapp_binary` and
3+
`py_zipapp_test`. POSIX launchers preserve the application's PID, signal delivery,
4+
and terminal job; cleanup follows interpreter exit. Ordinary runtime-created
5+
virtual environments now prepare in Python. ZIP caches publish only completed
6+
trees, and Python ZIP entry points honor target and additional interpreter
7+
arguments. Paths containing spaces, partial extraction, concurrent startup, and
8+
custom startup templates retain their intended behavior.

‎python/private/BUILD.bazel‎

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,49 @@ filegroup(
105105
visibility = NOT_ACTUALLY_PUBLIC,
106106
)
107107

108+
filegroup(
109+
name = "bootstrap_cleanup",
110+
srcs = ["bootstrap_cleanup.py"],
111+
visibility = NOT_ACTUALLY_PUBLIC,
112+
)
113+
114+
filegroup(
115+
name = "application_sources",
116+
srcs = glob(["_rules_python_bootstrap/*.py"]),
117+
visibility = NOT_ACTUALLY_PUBLIC,
118+
)
119+
120+
py_library(
121+
name = "application_bootstrap",
122+
srcs = glob(
123+
["_rules_python_bootstrap/*.py"],
124+
exclude = ["_rules_python_bootstrap/driver.py"],
125+
),
126+
)
127+
128+
exports_files(
129+
[
130+
"_rules_python_bootstrap/driver.py",
131+
"application_python_template.txt",
132+
"application_zip_template.txt",
133+
"application_shell_template.sh",
134+
],
135+
visibility = NOT_ACTUALLY_PUBLIC,
136+
)
137+
138+
bzl_library(
139+
name = "application",
140+
srcs = ["application.bzl"],
141+
deps = [
142+
":builders",
143+
":common",
144+
":py_application_info",
145+
":py_internal",
146+
"@bazel_skylib//lib:paths",
147+
"@bazel_skylib//lib:shell",
148+
],
149+
)
150+
108151
filegroup(
109152
name = "stage2_bootstrap_template",
110153
srcs = ["stage2_bootstrap_template.py"],
@@ -542,6 +585,7 @@ bzl_library(
542585
name = "py_executable",
543586
srcs = ["py_executable.bzl"],
544587
deps = [
588+
":application",
545589
":attr_builders",
546590
":attributes",
547591
":builders",
@@ -562,6 +606,7 @@ bzl_library(
562606
":venv_runfiles",
563607
"@bazel_skylib//lib:dicts",
564608
"@bazel_skylib//lib:paths",
609+
"@bazel_skylib//lib:shell",
565610
"@bazel_skylib//lib:structs",
566611
"@bazel_skylib//rules:common_settings",
567612
"@rules_cc//cc/common",
@@ -969,6 +1014,11 @@ bzl_library(
9691014
srcs = ["platform_info.bzl"],
9701015
)
9711016

1017+
bzl_library(
1018+
name = "py_application_info",
1019+
srcs = ["py_application_info.bzl"],
1020+
)
1021+
9721022
bzl_library(
9731023
name = "py_cc_toolchain_info",
9741024
srcs = ["py_cc_toolchain_info.bzl"],
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
"""Private application preparation; imported only from a declared image path."""
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
"""Opt-in preparation diagnostics without application arguments or environment."""
2+
3+
import os
4+
import sys
5+
6+
7+
def verbose(event, *paths):
8+
if os.environ.get("RULES_PYTHON_BOOTSTRAP_VERBOSE"):
9+
print("rules_python bootstrap:", event, *paths, file=sys.stderr, flush=True)
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
"""Load the declared private package without using application import paths."""
2+
3+
import sys
4+
5+
if not getattr(sys.flags, "safe_path", False) and not sys.flags.isolated and sys.path:
6+
del sys.path[0]
7+
8+
import importlib.machinery
9+
import importlib.util
10+
import os
11+
12+
spec = importlib.machinery.PathFinder.find_spec(
13+
"_rules_python_bootstrap", [os.path.dirname(os.path.dirname(__file__))]
14+
)
15+
package = importlib.util.module_from_spec(spec)
16+
sys.modules[spec.name] = package
17+
spec.loader.exec_module(package)
18+
19+
if __name__ == "__main__":
20+
from _rules_python_bootstrap import entry
21+
22+
if sys.argv[1] == "directory":
23+
sys.exit(entry.shell_directory_main(sys.argv[2:]))
24+
elif sys.argv[1] == "prepare-archive":
25+
entry.prepare_archive(sys.argv[2], sys.argv[3], cached=sys.argv[4] == "1")
26+
else:
27+
raise ValueError("Unknown bootstrap entry: " + sys.argv[1])

0 commit comments

Comments
 (0)