Skip to content

Commit b3862ec

Browse files
authored
docs: give some general guidance on how to define custom toolchains (#2220)
From the discussion in #2216, it's clear that some better docs are needed to help people figure out how to define a toolchain and what the different pieces me. This gives some more explanation of the toolchain types, what they do, and how to define them. Along the way: * Add some more bazel objects to the inventory * Fix attribute lookups in the bazel inventory * Allow using parens in crossrefs, e.g. `foo()`
1 parent 3f20b4b commit b3862ec

File tree

7 files changed

+246
-11
lines changed

7 files changed

+246
-11
lines changed

docs/api/rules_python/python/cc/index.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,3 +25,10 @@ This target provides:
2525

2626
* `CcInfo`: The C++ information about the Python libraries.
2727
:::
28+
29+
:::{bzl:target} toolchain_type
30+
31+
Toolchain type identifier for the Python C toolchain.
32+
33+
This toolchain type is typically implemented by {obj}`py_cc_toolchain`.
34+
:::

docs/api/rules_python/python/index.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,14 @@
88
:::{bzl:target} toolchain_type
99

1010
Identifier for the toolchain type for the target platform.
11+
12+
This toolchain type gives information about the runtime for the target platform.
13+
It is typically implemented by the {obj}`py_runtime` rule
14+
15+
::::{seealso}
16+
{any}`Custom Toolchains` for how to define custom toolchains
17+
::::
18+
1119
:::
1220

1321
:::{bzl:target} exec_tools_toolchain_type

docs/index.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ by buildifier.
5757
self
5858
getting-started
5959
pypi-dependencies
60-
toolchains
60+
Toolchains <toolchains>
6161
pip
6262
coverage
6363
precompiling

docs/toolchains.md

Lines changed: 197 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -161,9 +161,13 @@ Remember to call `use_repo()` to make repos visible to your module:
161161

162162
#### Toolchain usage in other rules
163163

164-
Python toolchains can be utilized in other bazel rules, such as `genrule()`, by adding the `toolchains=["@rules_python//python:current_py_toolchain"]` attribute. You can obtain the path to the Python interpreter using the `$(PYTHON2)` and `$(PYTHON3)` ["Make" Variables](https://bazel.build/reference/be/make-variables). See the
165-
{gh-path}`test_current_py_toolchain <tests/load_from_macro/BUILD.bazel>` target for an example.
166-
164+
Python toolchains can be utilized in other bazel rules, such as `genrule()`, by
165+
adding the `toolchains=["@rules_python//python:current_py_toolchain"]`
166+
attribute. You can obtain the path to the Python interpreter using the
167+
`$(PYTHON2)` and `$(PYTHON3)` ["Make"
168+
Variables](https://bazel.build/reference/be/make-variables). See the
169+
{gh-path}`test_current_py_toolchain <tests/load_from_macro/BUILD.bazel>` target
170+
for an example.
167171

168172
## Workspace configuration
169173

@@ -242,3 +246,193 @@ there is a toolchain misconfiguration somewhere.
242246
To aid migration off the Bazel-builtin toolchain, rules_python provides
243247
{obj}`@rules_python//python/runtime_env_toolchains:all`. This is an equivalent
244248
toolchain, but is implemented using rules_python's objects.
249+
250+
251+
## Custom toolchains
252+
253+
While rules_python provides toolchains by default, it is not required to use
254+
them, and you can define your own toolchains to use instead. This section
255+
gives an introduction for how to define them yourself.
256+
257+
:::{note}
258+
* Defining your own toolchains is an advanced feature.
259+
* APIs used for defining them are less stable and may change more often.
260+
:::
261+
262+
Under the hood, there are multiple toolchains that comprise the different
263+
information necessary to build Python targets. Each one has an
264+
associated _toolchain type_ that identifies it. We call the collection of these
265+
toolchains a "toolchain suite".
266+
267+
One of the underlying design goals of the toolchains is to support complex and
268+
bespoke environments. Such environments may use an arbitrary combination of
269+
{obj}`RBE`, cross-platform building, multiple Python versions,
270+
building Python from source, embeding Python (as opposed to building separate
271+
interpreters), using prebuilt binaries, or using binaries built from source. To
272+
that end, many of the attributes they accept, and fields they provide, are
273+
optional.
274+
275+
### Target toolchain type
276+
277+
The target toolchain type is {obj}`//python:toolchain_type`, and it
278+
is for _target configuration_ runtime information, e.g., the Python version
279+
and interpreter binary that a program will use.
280+
281+
The is typically implemented using {obj}`py_runtime()`, which
282+
provides the {obj}`PyRuntimeInfo` provider. For historical reasons from the
283+
Python 2 transition, `py_runtime` is wrapped in {obj}`py_runtime_pair`,
284+
which provides {obj}`ToolchainInfo` with the field `py3_runtime`, which is an
285+
instance of `PyRuntimeInfo`.
286+
287+
This toolchain type is intended to hold only _target configuration_ values. As
288+
such, when defining its associated {external:bzl:obj}`toolchain` target, only
289+
set {external:bzl:obj}`toolchain.target_compatible_with` and/or
290+
{external:bzl:obj}`toolchain.target_settings` constraints; there is no need to
291+
set {external:bzl:obj}`toolchain.exec_compatible_with`.
292+
293+
### Python C toolchain type
294+
295+
The Python C toolchain type ("py cc") is {obj}`//python/cc:toolchain_type`, and
296+
it has C/C++ information for the _target configuration_, e.g. the C headers that
297+
provide `Python.h`.
298+
299+
This is typically implemented using {obj}`py_cc_toolchain()`, which provides
300+
{obj}`ToolchainInfo` with the field `py_cc_toolchain` set, which is a
301+
{obj}`PyCcToolchainInfo` provider instance.
302+
303+
This toolchain type is intended to hold only _target configuration_ values
304+
relating to the C/C++ information for the Python runtime. As such, when defining
305+
its associated {external:obj}`toolchain` target, only set
306+
{external:bzl:obj}`toolchain.target_compatible_with` and/or
307+
{external:bzl:obj}`toolchain.target_settings` constraints; there is no need to
308+
set {external:bzl:obj}`toolchain.exec_compatible_with`.
309+
310+
### Exec tools toolchain type
311+
312+
The exec tools toolchain type is {obj}`//python:exec_tools_toolchain_type`,
313+
and it is for supporting tools for _building_ programs, e.g. the binary to
314+
precompile code at build time.
315+
316+
This toolchain type is intended to hold only _exec configuration_ values --
317+
usually tools (prebuilt or from-source) used to build Python targets.
318+
319+
This is typically implemented using {obj}`py_exec_tools_toolchain`, which
320+
provides {obj}`ToolchainInfo` with the field `exec_tools` set, which is an
321+
instance of {obj}`PyExecToolsInfo`.
322+
323+
The toolchain constraints of this toolchain type can be a bit more nuanced than
324+
the other toolchain types. Typically, you set
325+
{external:bzl:obj}`toolchain.target_settings` to the Python version the tools
326+
are for, and {external:bzl:obj}`toolchain.exec_compatible_with` to the platform
327+
they can run on. This allows the toolchain to first be considered based on the
328+
target configuration (e.g. Python version), then for one to be chosen based on
329+
finding one compatible with the available host platforms to run the tool on.
330+
331+
However, what `target_compatible_with`/`target_settings` and
332+
`exec_compatible_with` values to use depend on details of the tools being used.
333+
For example:
334+
* If you had a precompiler that supported any version of Python, then
335+
putting the Python version in `target_settings` is unnecessary.
336+
* If you had a prebuilt polyglot precompiler binary that could run on any
337+
platform, then setting `exec_compatible_with` is unnecessary.
338+
339+
This can work because, when the rules invoke these build tools, they pass along
340+
all necessary information so that the tool can be entirely independent of the
341+
target configuration being built for.
342+
343+
Alternatively, if you had a precompiler that only ran on linux, and only
344+
produced valid output for programs intended to run on linux, then _both_
345+
`exec_compatible_with` and `target_compatible_with` must be set to linux.
346+
347+
### Custom toolchain example
348+
349+
Here, we show an example for a semi-complicated toolchain suite, one that is:
350+
351+
* A CPython-based interpreter
352+
* For Python version 3.12.0
353+
* Using an in-build interpreter built from source
354+
* That only runs on Linux
355+
* Using a prebuilt precompiler that only runs on Linux, and only produces byte
356+
code valid for 3.12
357+
* With the exec tools interpreter disabled (unnecessary with a prebuild
358+
precompiler)
359+
* Providing C headers and libraries
360+
361+
Defining toolchains for this might look something like this:
362+
363+
```
364+
# File: toolchain_impls/BUILD
365+
load("@rules_python//python:py_cc_toolchain.bzl", "py_cc_toolchain")
366+
load("@rules_python//python:py_exec_tools_toolchain.bzl", "py_exec_tools_toolchain")
367+
load("@rules_python//python:py_runtime.bzl", "py_runtime")
368+
load("@rules_python//python:py_runtime_pair.bzl", "py_runtime_pair")
369+
370+
MAJOR = 3
371+
MINOR = 12
372+
MICRO = 0
373+
374+
py_runtime(
375+
name = "runtime",
376+
interpreter = ":python",
377+
interpreter_version_info = {
378+
"major": str(MAJOR),
379+
"minor": str(MINOR),
380+
"micro": str(MICRO),
381+
}
382+
implementation = "cpython"
383+
)
384+
py_runtime_pair(
385+
name = "runtime_pair",
386+
py3_runtime = ":runtime"
387+
)
388+
389+
py_cc_toolchain(
390+
name = "py_cc_toolchain_impl",
391+
headers = ":headers",
392+
libs = ":libs",
393+
python_version = "{}.{}".format(MAJOR, MINOR)
394+
)
395+
396+
py_exec_tools_toolchain(
397+
name = "exec_tools_toolchain_impl",
398+
exec_interpreter = "@rules_python/python:null_target",
399+
precompiler = "precompiler-cpython-3.12"
400+
)
401+
402+
cc_binary(name = "python3.12", ...)
403+
cc_library(name = "headers", ...)
404+
cc_library(name = "libs", ...)
405+
406+
# File: toolchains/BUILD
407+
# Putting toolchain() calls in a separate package from the toolchain
408+
# implementations minimizes Bazel loading overhead
409+
410+
toolchain(
411+
name = "runtime_toolchain",
412+
toolchain = "//toolchain_impl:runtime_pair",
413+
toolchain_type = "@rules_python//python:toolchain_type",
414+
target_compatible_with = ["@platforms/os:linux"]
415+
)
416+
toolchain(
417+
name = "py_cc_toolchain",
418+
toolchain = "//toolchain_impl:py_cc_toolchain_impl",
419+
toolchain_type = "@rules_python//python/cc:toolchain_type",
420+
target_compatible_with = ["@platforms/os:linux"]
421+
)
422+
423+
toolchain(
424+
name = "exec_tools_toolchain",
425+
toolchain = "//toolchain_impl:exec_tools_toolchain_impl",
426+
toolchain_type = "@rules_python//python:exec_tools_toolchain_type",
427+
target_settings = [
428+
"@rules_python//python/config_settings:is_python_3.12",
429+
],
430+
exec_comaptible_with = ["@platforms/os:linux"]
431+
)
432+
```
433+
434+
:::{note}
435+
The toolchain() calls should be in a separate BUILD file from everything else.
436+
This avoids Bazel having to perform unnecessary work when it discovers the list
437+
of available toolchains.
438+
:::

sphinxdocs/docs/sphinx-bzl.md

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -90,20 +90,29 @@ chevrons (`<>`):
9090
{obj}`the binary rule <py_binary>`
9191
```
9292

93-
Finally, specific types of objects (rules, functions, providers, etc) can be
93+
Specific types of objects (rules, functions, providers, etc) can be
9494
specified to help disambiguate short names:
9595

9696
```
9797
{function}`py_binary` # Refers to the wrapping macro
9898
{rule}`py_binary` # Refers to the underlying rule
9999
```
100100

101+
Finally, objects built into Bazel can be explicitly referenced by forcing
102+
a lookup outside the local project using `{external}`. For example, the symbol
103+
`toolchain` is a builtin Bazel function, but it could also be the name of a tag
104+
class in the local project. To force looking up the builtin Bazel `toolchain` rule,
105+
`{external:bzl:rule}` can be used, e.g.:
106+
107+
```
108+
{external:bzl:obj}`toolchain`
109+
```
110+
101111
Those are the basics of cross referencing. Sphinx has several additional
102112
syntaxes for finding and referencing objects; see
103113
[the MyST docs for supported
104114
syntaxes](https://myst-parser.readthedocs.io/en/latest/syntax/cross-referencing.html#reference-roles)
105115

106-
107116
### Cross reference roles
108117

109118
A cross reference role is the `obj` portion of `{bzl:obj}`. It affects what is

sphinxdocs/inventories/bazel_inventory.txt

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ ExecutionInfo bzl:type 1 rules/lib/providers/ExecutionInfo -
99
File bzl:type 1 rules/lib/File -
1010
Label bzl:type 1 rules/lib/Label -
1111
Name bzl:type 1 concepts/labels#target-names -
12+
RBE bzl:obj 1 remote/rbe -
1213
RunEnvironmentInfo bzl:type 1 rules/lib/providers/RunEnvironmentInfo -
1314
Target bzl:type 1 rules/lib/builtins/Target -
1415
ToolchainInfo bzl:type 1 rules/lib/providers/ToolchainInfo.html -
@@ -58,6 +59,7 @@ ctx.version_file bzl:obj 1 rules/lib/builtins/ctx#version_file -
5859
ctx.workspace_name bzl:obj 1 rules/lib/builtins/ctx#workspace_name -
5960
depset bzl:type 1 rules/lib/depset -
6061
dict bzl:type 1 rules/lib/dict -
62+
exec_compatible_with bzl:attribute 1 reference/be/common-definitions#common.exec_compatible_with -
6163
int bzl:type 1 rules/lib/int -
6264
label bzl:type 1 concepts/labels -
6365
list bzl:type 1 rules/lib/list -
@@ -131,8 +133,13 @@ runfiles.root_symlinks bzl:type 1 rules/lib/builtins/runfiles#root_symlinks -
131133
runfiles.symlinks bzl:type 1 rules/lib/builtins/runfiles#symlinks -
132134
str bzl:type 1 rules/lib/string -
133135
struct bzl:type 1 rules/lib/builtins/struct -
136+
target_compatible_with bzl:attribute 1 reference/be/common-definitions#common.target_compatible_with -
134137
testing bzl:obj 1 rules/lib/toplevel/testing -
135138
testing.ExecutionInfo bzl:function 1 rules/lib/toplevel/testing#ExecutionInfo -
136139
testing.TestEnvironment bzl:function 1 rules/lib/toplevel/testing#TestEnvironment -
137140
testing.analysis_test bzl:rule 1 rules/lib/toplevel/testing#analysis_test -
138-
toolchain_type bzl:type 1 ules/lib/builtins/toolchain_type.html -
141+
toolchain bzl:rule 1 reference/be/platforms-and-toolchains#toolchain -
142+
toolchain.exec_compatible_with bzl:rule 1 reference/be/platforms-and-toolchains#toolchain.exec_compatible_with -
143+
toolchain.target_settings bzl:attribute 1 reference/be/platforms-and-toolchains#toolchain.target_settings -
144+
toolchain.target_compatible_with bzl:attribute 1 reference/be/platforms-and-toolchains#toolchain.target_compatible_with -
145+
toolchain_type bzl:type 1 rules/lib/builtins/toolchain_type.html -

sphinxdocs/src/sphinx_bzl/bzl.py

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@
3434
from sphinx.util import inspect, logging
3535
from sphinx.util import nodes as sphinx_nodes
3636
from sphinx.util import typing as sphinx_typing
37-
from typing_extensions import override, TypeAlias
37+
from typing_extensions import TypeAlias, override
3838

3939
_logger = logging.getLogger(__name__)
4040
_LOG_PREFIX = f"[{_logger.name}] "
@@ -552,7 +552,9 @@ def before_content(self) -> None:
552552

553553
@override
554554
def transform_content(self, content_node: addnodes.desc_content) -> None:
555-
def first_child_with_class_name(root, class_name) -> typing.Union[None, docutils_nodes.Element]:
555+
def first_child_with_class_name(
556+
root, class_name
557+
) -> typing.Union[None, docutils_nodes.Element]:
556558
matches = root.findall(
557559
lambda node: isinstance(node, docutils_nodes.Element)
558560
and class_name in node["classes"]
@@ -1437,7 +1439,9 @@ class _BzlDomain(domains.Domain):
14371439
object_types = {
14381440
"arg": domains.ObjType("arg", "arg", "obj"), # macro/function arg
14391441
"aspect": domains.ObjType("aspect", "aspect", "obj"),
1440-
"attribute": domains.ObjType("attribute", "attribute", "obj"), # rule attribute
1442+
"attribute": domains.ObjType(
1443+
"attribute", "attribute", "attr", "obj"
1444+
), # rule attribute
14411445
"function": domains.ObjType("function", "func", "obj"),
14421446
"method": domains.ObjType("method", "method", "obj"),
14431447
"module-extension": domains.ObjType(
@@ -1509,7 +1513,9 @@ class _BzlDomain(domains.Domain):
15091513
}
15101514

15111515
@override
1512-
def get_full_qualified_name(self, node: docutils_nodes.Element) -> typing.Union[str, None]:
1516+
def get_full_qualified_name(
1517+
self, node: docutils_nodes.Element
1518+
) -> typing.Union[str, None]:
15131519
bzl_file = node.get("bzl:file")
15141520
symbol_name = node.get("bzl:symbol")
15151521
ref_target = node.get("reftarget")
@@ -1574,6 +1580,10 @@ def _find_entry_for_xref(
15741580
if target.startswith("--"):
15751581
target = target.strip("-")
15761582
object_type = "flag"
1583+
1584+
# Allow using parentheses, e.g. `foo()` or `foo(x=...)`
1585+
target, _, _ = target.partition("(")
1586+
15771587
# Elide the value part of --foo=bar flags
15781588
# Note that the flag value could contain `=`
15791589
if "=" in target:

0 commit comments

Comments
 (0)