Skip to content

Commit 11d48ea

Browse files
committed
docs: various howto guides
1 parent 998e22e commit 11d48ea

File tree

7 files changed

+366
-0
lines changed

7 files changed

+366
-0
lines changed

docs/howto/build-a-wheel.md

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
:::{default-domain} bzl
2+
:::
3+
4+
# How to build a wheel
5+
6+
This guide explains how to use the `py_wheel` rule to build a wheel
7+
file from a `py_library`.
8+
9+
## Basic usage
10+
11+
The `py_wheel` rule takes any file-providing target as input and put its files
12+
into a wheel. Because `py_library` provides its source files, simple cases can
13+
pass `py_library` directly to `py_wheel`:
14+
15+
```starlark
16+
# BUILD.bazel
17+
18+
py_library(
19+
name = "my_project_lib",
20+
srcs = glob(["my_project/**/*.py"]),
21+
# ...
22+
)
23+
24+
py_wheel(
25+
name = "my_project_wheel",
26+
distribution = "my-project",
27+
version = "0.1.0",
28+
deps = [":my_project_lib"],
29+
)
30+
```
31+
32+
The above will include the *default outputs* of the `py_library`, which are the
33+
direct `.py` files listed in the py library. It does **not** include transitive
34+
dependencies.
35+
36+
## Including and filtering transitive dependencies
37+
38+
39+
Use the `py_package` rule to include and filter the transitive parts of
40+
a `py_library` target.
41+
42+
The `py_package` rule has a `packages` attribute that takes a list of dotted
43+
Python package names to include. All files and dependencies of those packages
44+
are included.
45+
46+
Here is an example:
47+
48+
```starlark
49+
# BUILD.bazel
50+
51+
py_library(
52+
name = "my_project_lib",
53+
srcs = glob(["my_project/**/*.py"]),
54+
deps = ["@pypi//some_dep"],
55+
)
56+
57+
py_package(
58+
name = "my_project_package",
59+
# This will only include files for the "my_package" package; other files
60+
# will be excluded.
61+
packages = ["my_project"],
62+
)
63+
64+
py_wheel(
65+
name = "my_project_wheel",
66+
distribution = "my-project",
67+
version = "0.1.0",
68+
# The `py_wheel` rule takes the `py_package` target in the `deps`
69+
# attribute.
70+
deps = [":my_project_package"],
71+
)
72+
```
73+
74+
## Disabling `__init__.py` generation
75+
76+
By default, Bazel automatically creates `__init__.py` files in directories to
77+
make them importable. This can sometimes be undesirable when building wheels
78+
because it interfers with namespace packages or makes directories importable
79+
that shouldn't be importable.
80+
81+
It's highly recommended to disable this behavior by setting a flag in your
82+
`.bazelrc` file:
83+
84+
```
85+
# .bazelrc
86+
build --incompatible_default_to_explicit_init_py=true
87+
```

docs/howto/get-python-version.md

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
:::{default-domain} bzl
2+
:::
3+
4+
# How to get the current Python version
5+
6+
This guide explains how to use a [toolchain](toolchains) to get the current Python
7+
version and, as an example, write it to a file.
8+
9+
You can create a simple rule that accesses the Python toolchain and retrieves
10+
the version string.
11+
12+
## The rule implementation
13+
14+
Create a file named `my_rule.bzl`:
15+
16+
```text
17+
# my_rule.bzl
18+
def _my_rule_impl(ctx):
19+
toolchain = ctx.toolchains["@rules_python//python:toolchain_type"]
20+
info = toolchain.py3_runtime.interpreter_version_info
21+
python_version = str(info.major) + "." + str(info.minor) + "." + str(info.micro)
22+
23+
output_file = ctx.actions.declare_file(ctx.attr.name + ".txt")
24+
ctx.actions.write(
25+
output = output_file,
26+
content = python_version,
27+
)
28+
29+
return [DefaultInfo(files = depset([output_file]))]
30+
31+
my_rule = rule(
32+
implementation = _my_rule_impl,
33+
attrs = {},
34+
toolchains = ["@rules_python//python:toolchain_type"],
35+
)
36+
```
37+
38+
## Using the rule
39+
40+
In your `BUILD.bazel` file, you can use the rule like this:
41+
42+
```text
43+
# BUILD.bazel
44+
load(":my_rule.bzl", "my_rule")
45+
46+
my_rule(
47+
name = "show_python_version",
48+
)
49+
```
50+
51+
When you build this target, it will generate a file named
52+
`show_python_version.txt` containing the Python version (e.g., `3.9`).
53+
54+
```text
55+
bazel build :show_python_version
56+
cat bazel-bin/show_python_version.txt
57+
```

docs/howto/index.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
:::{default-domain} bzl
2+
:::
3+
4+
# How-to Guides
5+
6+
This section contains a collection of how-to guides for accomplishing specific tasks with `rules_python`.
7+
8+
```{toctree}
9+
:maxdepth: 1
10+
:glob:
11+
12+
*
13+
```

docs/howto/linking-libpython.md

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
:::{default-domain} bzl
2+
:::
3+
4+
# How to link to libpython
5+
6+
This guide explains how to use the Python [toolchain](toolchains) to get the linker
7+
flags required for linking against `libpython`. This is often necessary when
8+
embedding Python in a C/C++ application.
9+
10+
Currently, the `:current_py_cc_libs` target does *not* include `-lpython` et al
11+
linker flags. This is intentional because it forces dynamic linking (via the
12+
dynamic linker processing `DT_NEEDED` entries), which prevents users who want
13+
to load it in some more custom way.
14+
15+
## Exposing linker flags in a rule
16+
17+
You can create a rule that gets the Python version from the toolchain and
18+
constructs the correct linker flag. This rule can then provide the flag to
19+
other C/C++ rules via the `CcInfo` provider.
20+
21+
Here's an example of a rule that creates the `-lpython<version>` flag:
22+
23+
```text
24+
# python_libs.bzl
25+
load("@bazel_tools//tools/cpp:toolchain_utils.bzl", "cc_common")
26+
27+
def _python_libs_impl(ctx):
28+
toolchain = ctx.toolchains["@rules_python//python:toolchain_type"]
29+
info = toolchain.py3_runtime.interpreter_version_info
30+
link_flag = "-lpython{}.{}".format(info.major, info.minor)
31+
32+
cc_info = CcInfo(
33+
linking_context = cc_common.create_linking_context(
34+
user_link_flags = [link_flag],
35+
),
36+
)
37+
return [cc_info]
38+
39+
python_libs = rule(
40+
implementation = _python_libs_impl,
41+
toolchains = ["@rules_python//python:toolchain_type"],
42+
)
43+
```
44+
45+
## Using the rule
46+
47+
In your `BUILD.bazel` file, define a target using this rule and add it to the
48+
`deps` of your `cc_binary` or `cc_library`.
49+
50+
```text
51+
# BUILD.bazel
52+
load(":python_libs.bzl", "python_libs")
53+
54+
python_libs(
55+
name = "py_libs",
56+
)
57+
58+
cc_binary(
59+
name = "my_app",
60+
srcs = ["my_app.c"],
61+
deps = [
62+
":py_libs",
63+
# Other dependencies
64+
],
65+
)
66+
```

docs/howto/pypi-headers.md

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
:::{default-domain} bzl
2+
:::
3+
4+
# How to expose headers from a PyPI package
5+
6+
When you depend on a PyPI package that includes C headers (like `numpy`), you
7+
need to make those headers available to your `cc_library` or
8+
`cc_binary` targets.
9+
10+
The recommended way to do this is to inject a `BUILD.bazel` file into the
11+
external repository for the package. This `BUILD` file will create
12+
a `cc_library` target that exposes the header files.
13+
14+
First, create a `.bzl` file that has the extra logic we'll inject. Putting it
15+
in a separate bzl file avoids having to redownload and extract the whl file
16+
when our logic changes.
17+
18+
```bzl
19+
20+
# pypi_extra_targets.bzl
21+
load("@rules_cc//cc:cc_library.bzl", "cc_library")
22+
23+
def extra_numpy_targets():
24+
cc_library(
25+
name = "headers",
26+
hdrs = glob(["**/*.h"]),
27+
visibility = ["//visibility:public"],
28+
)
29+
```
30+
31+
## Bzlmod setup
32+
33+
In your `MODULE.bazel` file, use the `build_file_content` attribute of
34+
`pip.parse` to inject the `BUILD` file content for the `numpy` package.
35+
36+
```bazel
37+
# MODULE.bazel
38+
load("@rules_python//python/extensions:pip.bzl", "parse", "whl_mods")
39+
pip = use_extension("@rules_python//python/extensions:pip.bzl", "pip")
40+
whl_mods = use_extension("@rules_python//python/extensions:pip.bzl", "whl_mods")
41+
42+
43+
# Define a specific modification for a wheel
44+
whl_mods(
45+
name = "numpy_mods",
46+
whl_name = "numpy-1.0.0-py3-none-any.whl", # The exact wheel filename
47+
additive_build_content = """
48+
load("@//:pypi_extra_targets.bzl", "numpy_hdrs")
49+
50+
extra_numpy_targets()
51+
""",
52+
)
53+
pip.parse(
54+
hub_name = "pypi",
55+
wheel_name = "numpy",
56+
requirements_lock = "//:requirements.txt",
57+
build_file_content = {
58+
"numpy": 'load("//:pypi_extra_targets.bzl", "numpy_hdrs")\n\nnumpy_hdrs()',
59+
},
60+
whl_modifications = {
61+
"@numpy_mods//:numpy.json": "numpy",
62+
},
63+
extra_hub_aliases = {
64+
"numpy": ["headers"],
65+
}
66+
)
67+
```
68+
69+
## WORKSPACE setup
70+
71+
In your `WORKSPACE` file, use the `annotations` attribute of `pip_parse` to
72+
inject additional `BUILD` file content, then use `extra_hub_targets` to expose
73+
that target in the `@pypi` hub repo.
74+
75+
The {obj}`package_annotation` helper can be used to construct the value for the
76+
`annotations` attribute.
77+
78+
```starlark
79+
# WORKSPACE
80+
load("@rules_python//python:pip.bzl", "package_annotation", "pip_parse")
81+
82+
pip_parse(
83+
name = "pypi",
84+
requirements_lock = "//:requirements.txt",
85+
annotations = {
86+
"numpy": package_annotation(
87+
additive_build_content = """\
88+
load("@//:pypi_extra_targets.bzl", "numpy_hdrs")
89+
90+
extra_numpy_targets()
91+
"""
92+
),
93+
},
94+
extra_hub_targets = {
95+
"numpy": ["headers"],
96+
},
97+
)
98+
```
99+
100+
## Using the headers
101+
102+
In your `BUILD.bazel` file, you can now depend on the generated `headers`
103+
target.
104+
105+
```bazel
106+
# BUILD.bazel
107+
cc_library(
108+
name = "my_c_extension",
109+
srcs = ["my_c_extension.c"],
110+
deps = ["@pypi//numpy:headers"],
111+
)
112+
```

docs/howto/python-headers.md

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
:::{default-domain} bzl
2+
:::
3+
4+
# How to get Python headers for C extensions
5+
6+
When building a Python C extension, you need access to the Python header
7+
files. This guide shows how to get the necessary include paths from the Python
8+
[toolchain](toolchains).
9+
10+
The recommended way to get the headers is to depend on the
11+
`@rules_python//python/cc:current_py_cc_headers` target. This is a helper
12+
target that uses toolchain resolution to find the correct headers for the
13+
target platform.
14+
15+
## Using the headers
16+
17+
In your `BUILD.bazel` file, you can add `@rules_python//python/cc:current_py_cc_headers`
18+
to the `deps` of a `cc_library` or `cc_binary` target.
19+
20+
```bazel
21+
# BUILD.bazel
22+
cc_library(
23+
name = "my_c_extension",
24+
srcs = ["my_c_extension.c"],
25+
deps = ["@rules_python//python/cc:current_py_cc_headers"],
26+
)
27+
```
28+
29+
This setup ensures that your C extension code can find and use the Python
30+
headers during compilation.

docs/index.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,7 @@ precompiling
102102
gazelle
103103
REPL <repl>
104104
Extending <extending>
105+
How-to Guides <howto/index>
105106
Contributing <contributing>
106107
devguide
107108
support

0 commit comments

Comments
 (0)