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