xref: /aosp_15_r20/external/bazelbuild-rules_python/docs/toolchains.md (revision 60517a1edbc8ecf509223e9af94a7adec7d736b8)
1:::{default-domain} bzl
2:::
3
4# Configuring Python toolchains and runtimes
5
6This documents how to configure the Python toolchain and runtimes for different
7use cases.
8
9## Bzlmod MODULE configuration
10
11How to configure `rules_python` in your MODULE.bazel file depends on how and why
12you're using Python. There are 4 basic use cases:
13
141. A root module that always uses Python. For example, you're building a
15   Python application.
162. A library module with dev-only uses of Python. For example, a Java project
17   that only uses Python as part of testing itself.
183. A library module without version constraints. For example, a rule set with
19   Python build tools, but defers to the user as to what Python version is used
20   for the tools.
214. A library module with version constraints. For example, a rule set with
22   Python build tools, and the module requires a specific version of Python
23   be used with its tools.
24
25### Root modules
26
27Root modules are always the top-most module. These are special in two ways:
28
291. Some `rules_python` bzlmod APIs are only respected by the root module.
302. The root module can force module overrides and specific module dependency
31   ordering.
32
33When configuring `rules_python` for a root module, you typically want to
34explicitly specify the Python version you want to use. This ensures that
35dependencies don't change the Python version out from under you. Remember that
36`rules_python` will set a version by default, but it will change regularly as
37it tracks a recent Python version.
38
39NOTE: If your root module only uses Python for development of the module itself,
40you should read the dev-only library module section.
41
42```
43bazel_dep(name="rules_python", version=...)
44python = use_extension("@rules_python//python/extensions:python.bzl", "python")
45
46python.toolchain(python_version = "3.12", is_default = True)
47```
48
49### Library modules
50
51A library module is a module that can show up in arbitrary locations in the
52bzlmod module graph -- it's unknown where in the breadth-first search order the
53module will be relative to other modules. For example, `rules_python` is a
54library module.
55
56#### Library modules with dev-only Python usage
57
58A library module with dev-only Python usage is usually one where Python is only
59used as part of its tests. For example, a module for Java rules might run some
60Python program to generate test data, but real usage of the rules don't need
61Python to work. To configure this, follow the root-module setup, but remember to
62specify `dev_dependency = True` to the bzlmod APIs:
63
64```
65# MODULE.bazel
66bazel_dep(name = "rules_python", version=..., dev_dependency = True)
67
68python = use_extension(
69    "@rules_python//python/extensions:python.bzl",
70    "python",
71    dev_dependency = True
72)
73
74python.toolchain(python_version = "3.12", is_default=True)
75```
76
77#### Library modules without version constraints
78
79A library module without version constraints is one where the version of Python
80used for the Python programs it runs isn't chosen by the module itself. Instead,
81it's up to the root module to pick an appropriate version of Python.
82
83For this case, configuration is simple: just depend on `rules_python` and use
84the normal `//python:py_binary.bzl` et al rules. There is no need to call
85`python.toolchain` -- rules_python ensures _some_ Python version is available,
86but more often the root module will specify some version.
87
88```
89# MODULE.bazel
90bazel_dep(name = "rules_python", version=...)
91```
92
93#### Library modules with version constraints
94
95A library module with version constraints is one where the module requires a
96specific Python version be used with its tools. This has some pros/cons:
97
98* It allows the library's tools to use a different version of Python than
99  the rest of the build. For example, a user's program could use Python 3.12,
100  while the library module's tools use Python 3.10.
101* It reduces the support burden for the library module because the library only needs
102  to test for the particular Python version they intend to run as.
103* It raises the support burden for the library module because the version of
104  Python being used needs to be regularly incremented.
105* It has higher build overhead because additional runtimes and libraries need
106  to be downloaded, and Bazel has to keep additional configuration state.
107
108To configure this, request the Python versions needed in MODULE.bazel and use
109the version-aware rules for `py_binary`.
110
111```
112# MODULE.bazel
113bazel_dep(name = "rules_python", version=...)
114
115python = use_extension("@rules_python//python/extensions:python.bzl", "python")
116python.toolchain(python_version = "3.12")
117
118# BUILD.bazel
119load("@python_versions//3.12:defs.bzl", "py_binary")
120
121py_binary(...)
122```
123
124### Pinning to a Python version
125
126Pinning to a version allows targets to force that a specific Python version is
127used, even if the root module configures a different version as a default. This
128is most useful for two cases:
129
1301. For submodules to ensure they run with the appropriate Python version
1312. To allow incremental, per-target, upgrading to newer Python versions,
132   typically in a mono-repo situation.
133
134To configure a submodule with the version-aware rules, request the particular
135version you need, then use the `@python_versions` repo to use the rules that
136force specific versions:
137
138```starlark
139python = use_extension("@rules_python//python/extensions:python.bzl", "python")
140
141python.toolchain(
142    python_version = "3.11",
143)
144use_repo(python, "python_versions")
145```
146
147Then use e.g. `load("@python_versions//3.11:defs.bzl", "py_binary")` to use
148the rules that force that particular version. Multiple versions can be specified
149and use within a single build.
150
151For more documentation, see the bzlmod examples under the {gh-path}`examples`
152folder.  Look for the examples that contain a `MODULE.bazel` file.
153
154### Other toolchain details
155
156The `python.toolchain()` call makes its contents available under a repo named
157`python_X_Y`, where X and Y are the major and minor versions. For example,
158`python.toolchain(python_version="3.11")` creates the repo `@python_3_11`.
159Remember to call `use_repo()` to make repos visible to your module:
160`use_repo(python, "python_3_11")`
161
162#### Toolchain usage in other rules
163
164Python toolchains can be utilized in other bazel rules, such as `genrule()`, by
165adding the `toolchains=["@rules_python//python:current_py_toolchain"]`
166attribute. You can obtain the path to the Python interpreter using the
167`$(PYTHON2)` and `$(PYTHON3)` ["Make"
168Variables](https://bazel.build/reference/be/make-variables). See the
169{gh-path}`test_current_py_toolchain <tests/load_from_macro/BUILD.bazel>` target
170for an example.
171
172### Overriding toolchain defaults and adding more versions
173
174One can perform various overrides for the registered toolchains from the root
175module. For example, the following use cases would be supported using the
176existing attributes:
177
178* Limiting the available toolchains for the entire `bzlmod` transitive graph
179  via {attr}`python.override.available_python_versions`.
180* Setting particular `X.Y.Z` Python versions when modules request `X.Y` version
181  via {attr}`python.override.minor_mapping`.
182* Per-version control of the coverage tool used using
183  {attr}`python.single_version_platform_override.coverage_tool`.
184* Adding additional Python versions via {bzl:obj}`python.single_version_override` or
185  {bzl:obj}`python.single_version_platform_override`.
186
187## Workspace configuration
188
189To import rules_python in your project, you first need to add it to your
190`WORKSPACE` file, using the snippet provided in the
191[release you choose](https://github.com/bazelbuild/rules_python/releases)
192
193To depend on a particular unreleased version, you can do the following:
194
195```starlark
196load("@bazel_tools//tools/build_defs/repo:http.bzl", "http_archive")
197
198
199# Update the SHA and VERSION to the lastest version available here:
200# https://github.com/bazelbuild/rules_python/releases.
201
202SHA="84aec9e21cc56fbc7f1335035a71c850d1b9b5cc6ff497306f84cced9a769841"
203
204VERSION="0.23.1"
205
206http_archive(
207    name = "rules_python",
208    sha256 = SHA,
209    strip_prefix = "rules_python-{}".format(VERSION),
210    url = "https://github.com/bazelbuild/rules_python/releases/download/{}/rules_python-{}.tar.gz".format(VERSION,VERSION),
211)
212
213load("@rules_python//python:repositories.bzl", "py_repositories")
214
215py_repositories()
216```
217
218### Workspace toolchain registration
219
220To register a hermetic Python toolchain rather than rely on a system-installed interpreter for runtime execution, you can add to the `WORKSPACE` file:
221
222```starlark
223load("@rules_python//python:repositories.bzl", "python_register_toolchains")
224
225python_register_toolchains(
226    name = "python_3_11",
227    # Available versions are listed in @rules_python//python:versions.bzl.
228    # We recommend using the same version your team is already standardized on.
229    python_version = "3.11",
230)
231
232load("@python_3_11//:defs.bzl", "interpreter")
233
234load("@rules_python//python:pip.bzl", "pip_parse")
235
236pip_parse(
237    ...
238    python_interpreter_target = interpreter,
239    ...
240)
241```
242
243After registration, your Python targets will use the toolchain's interpreter during execution, but a system-installed interpreter
244is still used to 'bootstrap' Python targets (see https://github.com/bazelbuild/rules_python/issues/691).
245You may also find some quirks while using this toolchain. Please refer to [python-build-standalone documentation's _Quirks_ section](https://gregoryszorc.com/docs/python-build-standalone/main/quirks.html).
246
247## Autodetecting toolchain
248
249The autodetecting toolchain is a deprecated toolchain that is built into Bazel.
250It's name is a bit misleading: it doesn't autodetect anything. All it does is
251use `python3` from the environment a binary runs within. This provides extremely
252limited functionality to the rules (at build time, nothing is knowable about
253the Python runtime).
254
255Bazel itself automatically registers `@bazel_tools//tools/python:autodetecting_toolchain`
256as the lowest priority toolchain. For WORKSPACE builds, if no other toolchain
257is registered, that toolchain will be used. For bzlmod builds, rules_python
258automatically registers a higher-priority toolchain; it won't be used unless
259there is a toolchain misconfiguration somewhere.
260
261To aid migration off the Bazel-builtin toolchain, rules_python provides
262{bzl:obj}`@rules_python//python/runtime_env_toolchains:all`. This is an equivalent
263toolchain, but is implemented using rules_python's objects.
264
265
266## Custom toolchains
267
268While rules_python provides toolchains by default, it is not required to use
269them, and you can define your own toolchains to use instead. This section
270gives an introduction for how to define them yourself.
271
272:::{note}
273* Defining your own toolchains is an advanced feature.
274* APIs used for defining them are less stable and may change more often.
275:::
276
277Under the hood, there are multiple toolchains that comprise the different
278information necessary to build Python targets. Each one has an
279associated _toolchain type_ that identifies it. We call the collection of these
280toolchains a "toolchain suite".
281
282One of the underlying design goals of the toolchains is to support complex and
283bespoke environments. Such environments may use an arbitrary combination of
284{obj}`RBE`, cross-platform building, multiple Python versions,
285building Python from source, embeding Python (as opposed to building separate
286interpreters), using prebuilt binaries, or using binaries built from source. To
287that end, many of the attributes they accept, and fields they provide, are
288optional.
289
290### Target toolchain type
291
292The target toolchain type is {obj}`//python:toolchain_type`, and it
293is for _target configuration_ runtime information, e.g., the Python version
294and interpreter binary that a program will use.
295
296The is typically implemented using {obj}`py_runtime()`, which
297provides the {obj}`PyRuntimeInfo` provider. For historical reasons from the
298Python 2 transition, `py_runtime` is wrapped in {obj}`py_runtime_pair`,
299which provides {obj}`ToolchainInfo` with the field `py3_runtime`, which is an
300instance of `PyRuntimeInfo`.
301
302This toolchain type is intended to hold only _target configuration_ values. As
303such, when defining its associated {external:bzl:obj}`toolchain` target, only
304set {external:bzl:obj}`toolchain.target_compatible_with` and/or
305{external:bzl:obj}`toolchain.target_settings` constraints; there is no need to
306set {external:bzl:obj}`toolchain.exec_compatible_with`.
307
308### Python C toolchain type
309
310The Python C toolchain type ("py cc") is {obj}`//python/cc:toolchain_type`, and
311it has C/C++ information for the _target configuration_, e.g. the C headers that
312provide `Python.h`.
313
314This is typically implemented using {obj}`py_cc_toolchain()`, which provides
315{obj}`ToolchainInfo` with the field `py_cc_toolchain` set, which is a
316{obj}`PyCcToolchainInfo` provider instance.
317
318This toolchain type is intended to hold only _target configuration_ values
319relating to the C/C++ information for the Python runtime. As such, when defining
320its associated {external:obj}`toolchain` target, only set
321{external:bzl:obj}`toolchain.target_compatible_with` and/or
322{external:bzl:obj}`toolchain.target_settings` constraints; there is no need to
323set {external:bzl:obj}`toolchain.exec_compatible_with`.
324
325### Exec tools toolchain type
326
327The exec tools toolchain type is {obj}`//python:exec_tools_toolchain_type`,
328and it is for supporting tools for _building_ programs, e.g. the binary to
329precompile code at build time.
330
331This toolchain type is intended to hold only _exec configuration_ values --
332usually tools (prebuilt or from-source) used to build Python targets.
333
334This is typically implemented using {obj}`py_exec_tools_toolchain`, which
335provides {obj}`ToolchainInfo` with the field `exec_tools` set, which is an
336instance of {obj}`PyExecToolsInfo`.
337
338The toolchain constraints of this toolchain type can be a bit more nuanced than
339the other toolchain types. Typically, you set
340{external:bzl:obj}`toolchain.target_settings` to the Python version the tools
341are for, and {external:bzl:obj}`toolchain.exec_compatible_with` to the platform
342they can run on. This allows the toolchain to first be considered based on the
343target configuration (e.g. Python version), then for one to be chosen based on
344finding one compatible with the available host platforms to run the tool on.
345
346However, what `target_compatible_with`/`target_settings` and
347`exec_compatible_with` values to use depend on details of the tools being used.
348For example:
349* If you had a precompiler that supported any version of Python, then
350  putting the Python version in `target_settings` is unnecessary.
351* If you had a prebuilt polyglot precompiler binary that could run on any
352  platform, then setting `exec_compatible_with` is unnecessary.
353
354This can work because, when the rules invoke these build tools, they pass along
355all necessary information so that the tool can be entirely independent of the
356target configuration being built for.
357
358Alternatively, if you had a precompiler that only ran on linux, and only
359produced valid output for programs intended to run on linux, then _both_
360`exec_compatible_with` and `target_compatible_with` must be set to linux.
361
362### Custom toolchain example
363
364Here, we show an example for a semi-complicated toolchain suite, one that is:
365
366* A CPython-based interpreter
367* For Python version 3.12.0
368* Using an in-build interpreter built from source
369* That only runs on Linux
370* Using a prebuilt precompiler that only runs on Linux, and only produces byte
371  code valid for 3.12
372* With the exec tools interpreter disabled (unnecessary with a prebuild
373  precompiler)
374* Providing C headers and libraries
375
376Defining toolchains for this might look something like this:
377
378```
379# File: toolchain_impls/BUILD
380load("@rules_python//python:py_cc_toolchain.bzl", "py_cc_toolchain")
381load("@rules_python//python:py_exec_tools_toolchain.bzl", "py_exec_tools_toolchain")
382load("@rules_python//python:py_runtime.bzl", "py_runtime")
383load("@rules_python//python:py_runtime_pair.bzl", "py_runtime_pair")
384
385MAJOR = 3
386MINOR = 12
387MICRO = 0
388
389py_runtime(
390    name = "runtime",
391    interpreter = ":python",
392    interpreter_version_info = {
393        "major": str(MAJOR),
394        "minor": str(MINOR),
395        "micro": str(MICRO),
396    }
397    implementation = "cpython"
398)
399py_runtime_pair(
400    name = "runtime_pair",
401    py3_runtime = ":runtime"
402)
403
404py_cc_toolchain(
405    name = "py_cc_toolchain_impl",
406    headers = ":headers",
407    libs = ":libs",
408    python_version = "{}.{}".format(MAJOR, MINOR)
409)
410
411py_exec_tools_toolchain(
412    name = "exec_tools_toolchain_impl",
413    exec_interpreter = "@rules_python/python:none",
414    precompiler = "precompiler-cpython-3.12"
415)
416
417cc_binary(name = "python3.12", ...)
418cc_library(name = "headers", ...)
419cc_library(name = "libs", ...)
420
421# File: toolchains/BUILD
422# Putting toolchain() calls in a separate package from the toolchain
423# implementations minimizes Bazel loading overhead
424
425toolchain(
426    name = "runtime_toolchain",
427    toolchain = "//toolchain_impl:runtime_pair",
428    toolchain_type = "@rules_python//python:toolchain_type",
429    target_compatible_with = ["@platforms/os:linux"]
430)
431toolchain(
432    name = "py_cc_toolchain",
433    toolchain = "//toolchain_impl:py_cc_toolchain_impl",
434    toolchain_type = "@rules_python//python/cc:toolchain_type",
435    target_compatible_with = ["@platforms/os:linux"]
436)
437
438toolchain(
439    name = "exec_tools_toolchain",
440    toolchain = "//toolchain_impl:exec_tools_toolchain_impl",
441    toolchain_type = "@rules_python//python:exec_tools_toolchain_type",
442    target_settings = [
443        "@rules_python//python/config_settings:is_python_3.12",
444    ],
445    exec_comaptible_with = ["@platforms/os:linux"]
446)
447```
448
449:::{note}
450The toolchain() calls should be in a separate BUILD file from everything else.
451This avoids Bazel having to perform unnecessary work when it discovers the list
452of available toolchains.
453:::
454