Skip to content

Commit 9382ed2

Browse files
authored
docs: various howto guides (bazel-contrib#3157)
Create a "How to" section of docs that explain how to accomplish specific tasks. While these things can be inferred from the various API references, those are large and overwhelming, which can make it hard to figure it out. Instead, provide how to guides from tasks that we've seen users ask about. Fixes bazel-contrib#3044
1 parent 6038ac4 commit 9382ed2

File tree

7 files changed

+363
-0
lines changed

7 files changed

+363
-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+
```starlark
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+
```starlark
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+
```starlark
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+
```starlark
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+
```starlark
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: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
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+
hub_name = "pypi_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+
whl_modifications = {
58+
"@pypi_mods//:numpy.json": "numpy",
59+
},
60+
extra_hub_aliases = {
61+
"numpy": ["headers"],
62+
}
63+
)
64+
```
65+
66+
## WORKSPACE setup
67+
68+
In your `WORKSPACE` file, use the `annotations` attribute of `pip_parse` to
69+
inject additional `BUILD` file content, then use `extra_hub_targets` to expose
70+
that target in the `@pypi` hub repo.
71+
72+
The {obj}`package_annotation` helper can be used to construct the value for the
73+
`annotations` attribute.
74+
75+
```starlark
76+
# WORKSPACE
77+
load("@rules_python//python:pip.bzl", "package_annotation", "pip_parse")
78+
79+
pip_parse(
80+
name = "pypi",
81+
requirements_lock = "//:requirements.txt",
82+
annotations = {
83+
"numpy": package_annotation(
84+
additive_build_content = """\
85+
load("@//:pypi_extra_targets.bzl", "numpy_hdrs")
86+
87+
extra_numpy_targets()
88+
"""
89+
),
90+
},
91+
extra_hub_targets = {
92+
"numpy": ["headers"],
93+
},
94+
)
95+
```
96+
97+
## Using the headers
98+
99+
In your `BUILD.bazel` file, you can now depend on the generated `headers`
100+
target.
101+
102+
```bazel
103+
# BUILD.bazel
104+
cc_library(
105+
name = "my_c_extension",
106+
srcs = ["my_c_extension.c"],
107+
deps = ["@pypi//numpy:headers"],
108+
)
109+
```

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/docs/index
103103
REPL <repl>
104104
Extending <extending>
105+
How-to Guides <howto/index>
105106
Contributing <contributing>
106107
devguide
107108
support

0 commit comments

Comments
 (0)