Skip to content

Commit 3730803

Browse files
authored
feat: compile source files at build time (#1902)
This implements precompiling: performing Python source to byte code compilation at build time. This allows improved program startup time by allowing the byte code compilation step to be skipped at runtime. Precompiling is disabled by default, for now. A subsequent release will enable it by default. This allows the necessary flags and attributes to become available so users can opt-out prior to it being enabled by default. Similarly, `//python:features.bzl` is introduced to allow feature detection. This implementation is made to serve a variety of use cases, so there are several attributes and flags to control behavior. The main use cases being served are: * Large mono-repos that need to incrementally enable/disable precompiling. * Remote execution builds, where persistent workers aren't easily available. * Environments where toolchains are custom defined instead of using the ones created by rules_python. To that end, there are several attributes and flags to control behavior, and the toolchains allow customizing the tools used. Fixes #1761
1 parent 45363a1 commit 3730803

39 files changed

+1976
-74
lines changed

.bazelignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,4 +25,5 @@ examples/pip_parse_vendored/bazel-pip_parse_vendored
2525
examples/py_proto_library/bazel-py_proto_library
2626
tests/integration/compile_pip_requirements/bazel-compile_pip_requirements
2727
tests/integration/ignore_root_user_error/bazel-ignore_root_user_error
28+
tests/integration/local_toolchains/bazel-local_toolchains
2829
tests/integration/pip_repository_entry_points/bazel-pip_repository_entry_points

.bazelrc

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,8 @@
44
# (Note, we cannot use `common --deleted_packages` because the bazel version command doesn't support it)
55
# To update these lines, execute
66
# `bazel run @rules_bazel_integration_test//tools:update_deleted_packages`
7-
build --deleted_packages=examples/build_file_generation,examples/build_file_generation/random_number_generator,examples/bzlmod,examples/bzlmod/entry_points,examples/bzlmod/entry_points/tests,examples/bzlmod/libs/my_lib,examples/bzlmod/other_module,examples/bzlmod/other_module/other_module/pkg,examples/bzlmod/patches,examples/bzlmod/py_proto_library,examples/bzlmod/py_proto_library/example.com/another_proto,examples/bzlmod/py_proto_library/example.com/proto,examples/bzlmod/runfiles,examples/bzlmod/tests,examples/bzlmod/tests/dupe_requirements,examples/bzlmod/tests/other_module,examples/bzlmod/whl_mods,examples/bzlmod_build_file_generation,examples/bzlmod_build_file_generation/other_module/other_module/pkg,examples/bzlmod_build_file_generation/runfiles,examples/multi_python_versions/libs/my_lib,examples/multi_python_versions/requirements,examples/multi_python_versions/tests,examples/pip_parse,examples/pip_parse_vendored,examples/pip_repository_annotations,examples/py_proto_library,examples/py_proto_library/example.com/another_proto,examples/py_proto_library/example.com/proto,gazelle,gazelle/manifest,gazelle/manifest/generate,gazelle/manifest/hasher,gazelle/manifest/test,gazelle/modules_mapping,gazelle/python,gazelle/pythonconfig,tests/integration/compile_pip_requirements,tests/integration/compile_pip_requirements_test_from_external_repo,tests/integration/ignore_root_user_error,tests/integration/ignore_root_user_error/submodule,tests/integration/pip_parse,tests/integration/pip_parse/empty,tests/integration/pip_repository_entry_points,tests/integration/py_cc_toolchain_registered
8-
query --deleted_packages=examples/build_file_generation,examples/build_file_generation/random_number_generator,examples/bzlmod,examples/bzlmod/entry_points,examples/bzlmod/entry_points/tests,examples/bzlmod/libs/my_lib,examples/bzlmod/other_module,examples/bzlmod/other_module/other_module/pkg,examples/bzlmod/patches,examples/bzlmod/py_proto_library,examples/bzlmod/py_proto_library/example.com/another_proto,examples/bzlmod/py_proto_library/example.com/proto,examples/bzlmod/runfiles,examples/bzlmod/tests,examples/bzlmod/tests/dupe_requirements,examples/bzlmod/tests/other_module,examples/bzlmod/whl_mods,examples/bzlmod_build_file_generation,examples/bzlmod_build_file_generation/other_module/other_module/pkg,examples/bzlmod_build_file_generation/runfiles,examples/multi_python_versions/libs/my_lib,examples/multi_python_versions/requirements,examples/multi_python_versions/tests,examples/pip_parse,examples/pip_parse_vendored,examples/pip_repository_annotations,examples/py_proto_library,examples/py_proto_library/example.com/another_proto,examples/py_proto_library/example.com/proto,gazelle,gazelle/manifest,gazelle/manifest/generate,gazelle/manifest/hasher,gazelle/manifest/test,gazelle/modules_mapping,gazelle/python,gazelle/pythonconfig,tests/integration/compile_pip_requirements,tests/integration/compile_pip_requirements_test_from_external_repo,tests/integration/ignore_root_user_error,tests/integration/ignore_root_user_error/submodule,tests/integration/pip_parse,tests/integration/pip_parse/empty,tests/integration/pip_repository_entry_points,tests/integration/py_cc_toolchain_registered
7+
build --deleted_packages=examples/build_file_generation,examples/build_file_generation/random_number_generator,examples/bzlmod,examples/bzlmod_build_file_generation,examples/bzlmod_build_file_generation/other_module/other_module/pkg,examples/bzlmod_build_file_generation/runfiles,examples/bzlmod/entry_points,examples/bzlmod/entry_points/tests,examples/bzlmod/libs/my_lib,examples/bzlmod/other_module,examples/bzlmod/other_module/other_module/pkg,examples/bzlmod/patches,examples/bzlmod/py_proto_library,examples/bzlmod/py_proto_library/example.com/another_proto,examples/bzlmod/py_proto_library/example.com/proto,examples/bzlmod/runfiles,examples/bzlmod/tests,examples/bzlmod/tests/dupe_requirements,examples/bzlmod/tests/other_module,examples/bzlmod/whl_mods,examples/multi_python_versions/libs/my_lib,examples/multi_python_versions/requirements,examples/multi_python_versions/tests,examples/pip_parse,examples/pip_parse_vendored,examples/pip_repository_annotations,examples/py_proto_library,examples/py_proto_library/example.com/another_proto,examples/py_proto_library/example.com/proto,gazelle,gazelle/manifest,gazelle/manifest/generate,gazelle/manifest/hasher,gazelle/manifest/test,gazelle/modules_mapping,gazelle/python,gazelle/pythonconfig,tests/integration/compile_pip_requirements,tests/integration/compile_pip_requirements_test_from_external_repo,tests/integration/ignore_root_user_error,tests/integration/ignore_root_user_error/submodule,tests/integration/pip_parse,tests/integration/pip_parse/empty,tests/integration/pip_repository_entry_points,tests/integration/py_cc_toolchain_registered
8+
query --deleted_packages=examples/build_file_generation,examples/build_file_generation/random_number_generator,examples/bzlmod,examples/bzlmod_build_file_generation,examples/bzlmod_build_file_generation/other_module/other_module/pkg,examples/bzlmod_build_file_generation/runfiles,examples/bzlmod/entry_points,examples/bzlmod/entry_points/tests,examples/bzlmod/libs/my_lib,examples/bzlmod/other_module,examples/bzlmod/other_module/other_module/pkg,examples/bzlmod/patches,examples/bzlmod/py_proto_library,examples/bzlmod/py_proto_library/example.com/another_proto,examples/bzlmod/py_proto_library/example.com/proto,examples/bzlmod/runfiles,examples/bzlmod/tests,examples/bzlmod/tests/dupe_requirements,examples/bzlmod/tests/other_module,examples/bzlmod/whl_mods,examples/multi_python_versions/libs/my_lib,examples/multi_python_versions/requirements,examples/multi_python_versions/tests,examples/pip_parse,examples/pip_parse_vendored,examples/pip_repository_annotations,examples/py_proto_library,examples/py_proto_library/example.com/another_proto,examples/py_proto_library/example.com/proto,gazelle,gazelle/manifest,gazelle/manifest/generate,gazelle/manifest/hasher,gazelle/manifest/test,gazelle/modules_mapping,gazelle/python,gazelle/pythonconfig,tests/integration/compile_pip_requirements,tests/integration/compile_pip_requirements_test_from_external_repo,tests/integration/ignore_root_user_error,tests/integration/ignore_root_user_error/submodule,tests/integration/pip_parse,tests/integration/pip_parse/empty,tests/integration/pip_repository_entry_points,tests/integration/py_cc_toolchain_registered
99

1010
test --test_output=errors
1111

CHANGELOG.md

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,9 @@ A brief description of the categories of changes:
2222
[x.x.x]: https://github.com/bazelbuild/rules_python/releases/tag/x.x.x
2323

2424
### Changed
25+
* (toolchains) Optional toolchain dependency: `py_binary`, `py_test`, and
26+
`py_library` now depend on the `//python:exec_tools_toolchain_type` for build
27+
tools.
2528

2629
* (deps): Bumped `bazel_skylib` to 1.6.1.
2730
* (bzlmod): The `python` and internal `rules_python` extensions have been
@@ -37,6 +40,28 @@ A brief description of the categories of changes:
3740
for this package before will be deleted automatically.
3841

3942
### Added
43+
* (rules) Precompiling Python source at build time is available. but is
44+
disabled by default, for now. Set
45+
`@rules_python//python/config_settings:precompile=enabled` to enable it
46+
by default. A subsequent release will enable it by default. See the
47+
[Precompiling docs][precompile-docs] and API reference docs for more
48+
information on precompiling. Note this requires Bazel 7+ and the Pystar rule
49+
implementation enabled.
50+
([#1761](https://github.com/bazelbuild/rules_python/issues/1761))
51+
* (rules) Attributes and flags to control precompile behavior: `precompile`,
52+
`precompile_optimize_level`, `precompile_source_retention`,
53+
`precompile_invalidation_mode`, and `pyc_collection`
54+
* (toolchains) The target runtime toolchain (`//python:toolchain_type`) has
55+
two new optional attributes: `pyc_tag` (tells the pyc filename infix to use) and
56+
`implementation_name` (tells the Python implementation name).
57+
* (toolchains) A toolchain type for build tools has been added:
58+
`//python:exec_tools_toolchain_type`.
59+
* (providers) `PyInfo` has two new attributes: `direct_pyc_files` and
60+
`transitive_pyc_files`, which tell the pyc files a target makes available
61+
directly and transitively, respectively.
62+
* `//python:features.bzl` added to allow easy feature-detection in the future.
63+
64+
[precompile-docs]: /precompiling
4065

4166
## [0.32.2] - 2024-05-14
4267

docs/sphinx/index.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ pypi-dependencies
6060
toolchains
6161
pip
6262
coverage
63+
precompiling
6364
gazelle
6465
Contributing <contributing>
6566
support

docs/sphinx/precompiling.md

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
# Precompiling
2+
3+
Precompiling is compiling Python source files (`.py` files) into byte code (`.pyc`
4+
files) at build
5+
time instead of runtime. Doing it at build time can improve performance by
6+
skipping that work at runtime.
7+
8+
Precompiling is enabled by default, so there typically isn't anything special
9+
you must do to use it.
10+
11+
12+
## Overhead of precompiling
13+
14+
While precompiling helps runtime performance, it has two main costs:
15+
1. Increasing the size (count and disk usage) of runfiles. It approximately
16+
double the count of the runfiles because for every `.py` file, there is also
17+
a `.pyc` file. Compiled files are generally around the same size as the
18+
source files, so it approximately doubles the disk usage.
19+
2. Precompiling requires running an extra action at build time. While
20+
compiling itself isn't that expensive, the overhead can become noticable
21+
as more files need to be compiled.
22+
23+
## Binary-level opt-in
24+
25+
Because of the costs of precompiling, it may not be feasible to globally enable it
26+
for your repo for everything. For example, some binaries may be
27+
particularly large, and doubling the number of runfiles isn't doable.
28+
29+
If this is the case, there's an alternative way to more selectively and
30+
incrementally control precompiling on a per-binry basis.
31+
32+
To use this approach, the two basic steps are:
33+
1. Disable pyc files from being automatically added to runfiles:
34+
`--@rules_python//python/config_settings:precompile_add_to_runfiles=decided_elsewhere`,
35+
2. Set the `pyc_collection` attribute on the binaries/tests that should or should
36+
not use precompiling.
37+
38+
The default for the `pyc_collection` attribute is controlled by a flag, so you
39+
can use an opt-in or opt-out approach by setting the flag:
40+
* targets must opt-out: `--@rules_python//python/config_settings:pyc_collection=include_pyc`,
41+
* targets must opt-in: `--@rules_python//python/config_settings:pyc_collection=disabled`,
42+
43+
## Advanced precompiler customization
44+
45+
The default implementation of the precompiler is a persistent, multiplexed,
46+
sandbox-aware, cancellation-enabled, json-protocol worker that uses the same
47+
interpreter as the target toolchain. This works well for local builds, but may
48+
not work as well for remote execution builds. To customize the precompiler, two
49+
mechanisms are available:
50+
51+
* The exec tools toolchain allows customizing the precompiler binary used with
52+
the `precompiler` attribute. Arbitrary binaries are supported.
53+
* The execution requirements can be customized using
54+
`--@rules_python//tools/precompiler:execution_requirements`. This is a list
55+
flag that can be repeated. Each entry is a key=value that is added to the
56+
execution requirements of the `PyPrecompile` action. Note that this flag
57+
is specific to the rules_python precompiler. If a custom binary is used,
58+
this flag will have to be propagated from the custom binary using the
59+
`testing.ExecutionInfo` provider; refer to the `py_interpreter_program` an
60+
61+
The default precompiler implementation is an asynchronous/concurrent
62+
implementation. If you find it has bugs or hangs, please report them. In the
63+
meantime, the flag `--worker_extra_flag=PyPrecompile=--worker_impl=serial` can
64+
be used to switch to a synchronous/serial implementation that may not perform
65+
as well, but is less likely to have issues.
66+
67+
The `execution_requirements` keys of most relevance are:
68+
* `supports-workers`: 1 or 0, to indicate if a regular persistent worker is
69+
desired.
70+
* `supports-multiplex-workers`: 1 o 0, to indicate if a multiplexed persistent
71+
worker is desired.
72+
* `requires-worker-protocol`: json or proto; the rules_python precompiler
73+
currently only supports json.
74+
* `supports-multiplex-sandboxing`: 1 or 0, to indicate if sanboxing is of the
75+
worker is supported.
76+
* `supports-worker-cancellation`: 1 or 1, to indicate if requests to the worker
77+
can be cancelled.
78+
79+
Note that any execution requirements values can be specified in the flag.
80+
81+
## Known issues, caveats, and idiosyncracies
82+
83+
* Precompiling requires Bazel 7+ with the Pystar rule implementation enabled.
84+
* Mixing rules_python PyInfo with Bazel builtin PyInfo will result in pyc files
85+
being dropped.
86+
* Precompiled files may not be used in certain cases prior to Python 3.11. This
87+
occurs due Python adding the directory of the binary's main `.py` file, which
88+
causes the module to be found in the workspace source directory instead of
89+
within the binary's runfiles directory (where the pyc files are). This can
90+
usually be worked around by removing `sys.path[0]` (or otherwise ensuring the
91+
runfiles directory comes before the repos source directory in `sys.path`).
92+
* The pyc filename does not include the optimization level (e.g.
93+
`foo.cpython-39.opt-2.pyc`). This works fine (it's all byte code), but also
94+
means the interpreter `-O` argument can't be used -- doing so will cause the
95+
interpreter to look for the non-existent `opt-N` named files.

python/BUILD.bazel

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,11 @@ bzl_library(
7171
],
7272
)
7373

74+
bzl_library(
75+
name = "features_bzl",
76+
srcs = ["features.bzl"],
77+
)
78+
7479
bzl_library(
7580
name = "packaging_bzl",
7681
srcs = ["packaging.bzl"],
@@ -292,6 +297,11 @@ alias(
292297
actual = "@bazel_tools//tools/python:toolchain_type",
293298
)
294299

300+
toolchain_type(
301+
name = "exec_tools_toolchain_type",
302+
visibility = ["//visibility:public"],
303+
)
304+
295305
# Definitions for a Python toolchain that, at execution time, attempts to detect
296306
# a platform runtime having the appropriate major Python version. Consider this
297307
# a toolchain of last resort.

python/config_settings/BUILD.bazel

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,11 @@
1+
load("@bazel_skylib//rules:common_settings.bzl", "string_flag")
2+
load(
3+
"//python/private:flags.bzl",
4+
"PrecompileAddToRunfilesFlag",
5+
"PrecompileFlag",
6+
"PrecompileSourceRetentionFlag",
7+
"PycCollectionFlag",
8+
)
19
load(":config_settings.bzl", "construct_config_settings")
210

311
filegroup(
@@ -12,3 +20,35 @@ filegroup(
1220
construct_config_settings(
1321
name = "construct_config_settings",
1422
)
23+
24+
string_flag(
25+
name = "precompile",
26+
build_setting_default = PrecompileFlag.AUTO,
27+
values = sorted(PrecompileFlag.__members__.values()),
28+
# NOTE: Only public because its an implicit dependency
29+
visibility = ["//visibility:public"],
30+
)
31+
32+
string_flag(
33+
name = "precompile_source_retention",
34+
build_setting_default = PrecompileSourceRetentionFlag.KEEP_SOURCE,
35+
values = sorted(PrecompileSourceRetentionFlag.__members__.values()),
36+
# NOTE: Only public because its an implicit dependency
37+
visibility = ["//visibility:public"],
38+
)
39+
40+
string_flag(
41+
name = "precompile_add_to_runfiles",
42+
build_setting_default = PrecompileAddToRunfilesFlag.ALWAYS,
43+
values = sorted(PrecompileAddToRunfilesFlag.__members__.values()),
44+
# NOTE: Only public because its an implicit dependency
45+
visibility = ["//visibility:public"],
46+
)
47+
48+
string_flag(
49+
name = "pyc_collection",
50+
build_setting_default = PycCollectionFlag.DISABLED,
51+
values = sorted(PycCollectionFlag.__members__.values()),
52+
# NOTE: Only public because its an implicit dependency
53+
visibility = ["//visibility:public"],
54+
)
Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# Copyright 2023 The Bazel Authors. All rights reserved.
1+
# Copyright 2024 The Bazel Authors. All rights reserved.
22
#
33
# Licensed under the Apache License, Version 2.0 (the "License");
44
# you may not use this file except in compliance with the License.
@@ -11,10 +11,8 @@
1111
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
1212
# See the License for the specific language governing permissions and
1313
# limitations under the License.
14-
"""Constants for referring to platforms."""
14+
"""Allows detecting of rules_python features that aren't easily detected."""
1515

16-
# Explicit Label() calls are required so that it resolves in @rules_python
17-
# context instead of e.g. the @rules_testing context.
18-
MAC = Label("//tests/support:mac")
19-
LINUX = Label("//tests/support:linux")
20-
WINDOWS = Label("//tests/support:windows")
16+
features = struct(
17+
precompile = True,
18+
)

python/private/BUILD.bazel

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,11 +86,25 @@ bzl_library(
8686
],
8787
)
8888

89+
bzl_library(
90+
name = "enum_bzl",
91+
srcs = ["enum.bzl"],
92+
)
93+
8994
bzl_library(
9095
name = "envsubst_bzl",
9196
srcs = ["envsubst.bzl"],
9297
)
9398

99+
bzl_library(
100+
name = "flags_bzl",
101+
srcs = ["flags.bzl"],
102+
deps = [
103+
":enum_bzl",
104+
"@bazel_skylib//rules:common_settings",
105+
],
106+
)
107+
94108
bzl_library(
95109
name = "full_version_bzl",
96110
srcs = ["full_version.bzl"],
@@ -166,6 +180,18 @@ bzl_library(
166180
],
167181
)
168182

183+
bzl_library(
184+
name = "py_exec_tools_toolchain_bzl",
185+
srcs = ["py_exec_tools_toolchain.bzl"],
186+
deps = ["//python/private/common:providers_bzl"],
187+
)
188+
189+
bzl_library(
190+
name = "py_interpreter_program_bzl",
191+
srcs = ["py_interpreter_program.bzl"],
192+
deps = ["@bazel_skylib//rules:common_settings"],
193+
)
194+
169195
bzl_library(
170196
name = "py_package_bzl",
171197
srcs = ["py_package.bzl"],
@@ -192,6 +218,7 @@ bzl_library(
192218
name = "py_toolchain_suite_bzl",
193219
srcs = ["py_toolchain_suite.bzl"],
194220
deps = [
221+
":toolchain_types_bzl",
195222
"@bazel_skylib//lib:selects",
196223
],
197224
)
@@ -256,6 +283,11 @@ bzl_library(
256283
],
257284
)
258285

286+
bzl_library(
287+
name = "toolchain_types_bzl",
288+
srcs = ["toolchain_types.bzl"],
289+
)
290+
259291
bzl_library(
260292
name = "util_bzl",
261293
srcs = ["util.bzl"],

python/private/common/BUILD.bazel

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,10 @@ bzl_library(
3232
":providers_bzl",
3333
":py_internal_bzl",
3434
":semantics_bzl",
35+
"//python/private:enum_bzl",
36+
"//python/private:flags_bzl",
3537
"//python/private:reexports_bzl",
38+
"@bazel_skylib//rules:common_settings",
3639
],
3740
)
3841

@@ -46,9 +49,12 @@ bzl_library(
4649
name = "common_bazel_bzl",
4750
srcs = ["common_bazel.bzl"],
4851
deps = [
52+
":attributes_bzl",
4953
":common_bzl",
5054
":providers_bzl",
5155
":py_internal_bzl",
56+
"//python/private:py_interpreter_program_bzl",
57+
"//python/private:toolchain_types_bzl",
5258
"@bazel_skylib//lib:paths",
5359
],
5460
)
@@ -124,8 +130,11 @@ bzl_library(
124130
":common_bzl",
125131
":providers_bzl",
126132
":py_internal_bzl",
133+
"//python/private:flags_bzl",
127134
"//python/private:rules_cc_srcs_bzl",
135+
"//python/private:toolchain_types_bzl",
128136
"@bazel_skylib//lib:dicts",
137+
"@bazel_skylib//rules:common_settings",
129138
],
130139
)
131140

@@ -143,7 +152,10 @@ bzl_library(
143152
":common_bzl",
144153
":providers_bzl",
145154
":py_internal_bzl",
155+
"//python/private:flags_bzl",
156+
"//python/private:toolchain_types_bzl",
146157
"@bazel_skylib//lib:dicts",
158+
"@bazel_skylib//rules:common_settings",
147159
],
148160
)
149161

0 commit comments

Comments
 (0)