Skip to content

Commit 38c5fc0

Browse files
docs: add example for a complex multi-platform pypi configuration (#3292)
The core PyPI docs and API reference docs have the basics for setting up a multi-platform Bazel build, but there's a lot of cross-referencing and reading between the lines necessary. Create a how to guide specifically on how to do it to better explain the nuances. --------- Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
1 parent d24a9dc commit 38c5fc0

File tree

3 files changed

+201
-0
lines changed

3 files changed

+201
-0
lines changed
Lines changed: 194 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,194 @@
1+
:::{default-domain} bzl
2+
:::
3+
4+
# How-to: Multi-Platform PyPI Dependencies
5+
6+
When developing applications that need to run on a wide variety of platforms,
7+
managing PyPI dependencies can become complex. You might need different sets of
8+
dependencies for different combinations of Python version, threading model,
9+
operating system, CPU architecture, libc, and even hardware accelerators like
10+
GPUs.
11+
12+
This guide demonstrates how to manage this complexity using `rules_python` with
13+
bzlmod. If you prefer to learn by example, complete example code is provided at
14+
the end.
15+
16+
In this how to guide, we configure for using 4 requirements files, each
17+
for a different variation using Python 3.14 on Linux:
18+
19+
* Regular (non-freethreaded) Python
20+
* Freethreaded Python
21+
* Regular Python for CUDA 12.9
22+
* Freethreaded Python for ARM and Musl
23+
24+
## Mapping requirements files to Bazel configuration settings
25+
26+
Unfortunately, a requirements file doesn't tell what it's compatible with,
27+
so we have to manually specify the Bazel configuration settings for it. To do
28+
that using rules_python, there are two steps: defining a platform, then
29+
associating a requirements file with the platform.
30+
31+
### Defining a platform
32+
33+
First, we define a "platform" using {obj}`pip.default`. This associates an
34+
arbitrary name with a list of Bazel {obj}`config_setting` targets. While any
35+
name can be used for a platform (its name has no inherent semantic meaning), it
36+
should encode all the relevant dimensions that distinguish a requirements file.
37+
For example, if a requirements file is specifically for the combination of CUDA
38+
12.9 and NumPy 2.0, then the platform name should represent that.
39+
40+
The convention is to follow the format of `{os}_{cpu}{threading}`, where:
41+
42+
* `{os}` is the operating system (`linux`, `osx`, `windows`).
43+
* `{cpu}` is the architecture (`x86_64`, `aarch64`).
44+
* `{threading}` is `_freethreaded` for a freethreaded Python runtime, or an
45+
empty string for the regular runtime.
46+
47+
Additional dimensions should be appended and separated with an underscore (e.g.
48+
`linux_x86_64_musl_cuda12.9_numpy2`).
49+
50+
The platform name should not include the Python version. That is handled by
51+
`pip.parse.python_version` separately.
52+
53+
:::{note}
54+
The term _platform_ here has nothing to do with Bazel's `platform()` rule.
55+
:::
56+
57+
#### Defining custom settings
58+
59+
Because {obj}`pip.parse.config_settings` is a list of arbitrary `config_setting`
60+
targets, you can define your own flags or implement custom config matching
61+
logic. This allows you to model settings that aren't inherently part of
62+
rules_python.
63+
64+
This is typically done using [bazel_skylib flags](https://bazel.build/extending/config), but any [Starlark
65+
defined build setting](https://bazel.build/extending/config) can be used. Just
66+
remember to use `config_setting()` to match a particular value of the flag.
67+
68+
In our example below, we define a custom flag for CUDA version.
69+
70+
#### Predefined and common build settings
71+
72+
rules_python has some predefined build settings you can use. Commonly used ones
73+
are:
74+
75+
* {obj}`@rules_python//python/config_settings:py_linux_libc`
76+
* {obj}`@rules_python//python/config_settings:py_freethreaded`
77+
78+
Additionally, [Bazel @platforms](https://github.com/bazelbuild/platforms)
79+
contains commonly used settings for OS and CPU:
80+
81+
* `@platforms//os:windows`
82+
* `@platforms//os:linux`
83+
* `@platforms//os:osx`
84+
* `@platforms//cpu:x86_64`
85+
* `@platforms//cpu:aarch64`
86+
87+
Note that these are the raw flag names. In order to use them with `pip.default`,
88+
you must use {obj}`config_setting()` to match a particular value for them.
89+
90+
### Associating Requirements to Platforms
91+
92+
Next, we associate a requirements file with a platform using
93+
{obj}`pip.parse.requirements_by_platform`. This is a dictionary attribute where
94+
the keys are requirements files and the value is a platform name. The platform
95+
value can use a trailing or leading `*` to match multiple platforms. It can also
96+
specify multiple platform names using commas to separate them.
97+
98+
Note that the Python version is _not_ part of the platform name.
99+
100+
Under the hood, `pip.parse` merges all the requirements (for a `hub_name`) and
101+
constructs `select()` expressions to route to the appropriate dependencies.
102+
103+
### Using it in practice
104+
105+
Finally, to make use of what we've configured, perform a build and set
106+
command line flags to the appropriate values.
107+
108+
```shell
109+
# Build for CUDA
110+
bazel build --//:cuda_version=12.9 //:binary
111+
112+
# Build for ARM with musl
113+
bazel build --@rules_python//python/config_settings:py_linux_libc=musl \
114+
--cpu=aarch64 //:binary
115+
116+
# Build for freethreaded
117+
bazel build --@rules_python//python/config_settings:py_freethreaded=yes //:binary
118+
```
119+
120+
Note that certain combinations of flags may result in an error or undefined
121+
behavior. For example, trying to set both freethreaded and CUDA at the same
122+
time would result in an error because no requirements file was registered
123+
to match that combination.
124+
125+
## Multiple Python Versions
126+
127+
Having multiple Python versions is fully supported. Simply add a `pip.parse()`
128+
call and set `python_version` appropriately.
129+
130+
## Multiple hubs
131+
132+
Having multiple `pip.parse` calls with different `hub_name` values is fully
133+
supported. Each hub only contains the requirements registered to it.
134+
135+
## Complete Example
136+
137+
Here is a complete example that puts all the pieces together.
138+
139+
```starlark
140+
# File: BUILD.bazel
141+
load("@bazel_skylib//rules:common_settings.bzl", "string_flag")
142+
143+
# A custom flag for controlling the CUDA version
144+
string_flag(
145+
name = "cuda_version",
146+
build_setting_default = "none",
147+
)
148+
149+
config_setting(
150+
name = "is_cuda_12_9",
151+
flag_values = {":cuda_version": "12.9"},
152+
)
153+
154+
# A config_setting that uses the built-in libc flag from rules_python
155+
config_setting(
156+
name = "is_musl",
157+
flag_values = {"@rules_python//python/config_settings:py_linux_libc": "muslc"},
158+
)
159+
160+
# File: MODULE.bazel
161+
pip = use_extension("@rules_python//python/extensions:pip.bzl", "pip")
162+
163+
# A custom platform for CUDA on glibc linux
164+
pip.default(
165+
platform = "linux_x86_64_cuda12.9",
166+
os = "linux",
167+
cpu = "x86_64",
168+
config_settings = ["@//:is_cuda_12_9"],
169+
)
170+
171+
# A custom platform for musl on linux
172+
pip.default(
173+
platform = "linux_aarch64_musl",
174+
os = "linux",
175+
cpu = "aarch64",
176+
config_settings = ["@//:is_musl"],
177+
)
178+
179+
pip.parse(
180+
hub_name = "my_deps",
181+
python_version = "3.14",
182+
requirements_by_platform = {
183+
# Map to default platform names
184+
"//:py3.14-regular-linux-x86-glibc-cpu.txt": "linux_x86_64",
185+
"//:py3.14-freethreaded-linux-x86-glibc-cpu.txt": "linux_x86_64_freethreaded",
186+
187+
# Map to our custom platform names
188+
"//:py3.14-regular-linux-x86-glibc-cuda12.9.txt": "linux_x86_64_cuda12.9",
189+
"//:py3.14-freethreaded-linux-arm-musl-cpu.txt": "linux_aarch64_musl",
190+
},
191+
)
192+
193+
use_repo(pip, "my_deps")
194+
```

docs/pypi/index.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ Using PyPI packages (aka "pip install") involves the following main steps:
1111

1212
With the advanced topics covered separately:
1313
* Dealing with [circular dependencies](./circular-dependencies).
14+
* Handling [multi-platform dependencies](../howto/multi-platform-pypi-deps).
1415

1516
```{toctree}
1617
lock
@@ -22,6 +23,9 @@ use
2223
## Advanced topics
2324

2425
```{toctree}
26+
:maxdepth: 1
27+
2528
circular-dependencies
2629
patch
30+
../howto/multi-platform-pypi-deps
2731
```

python/private/pypi/pip_repository.bzl

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -266,6 +266,9 @@ code will be re-evaluated when any of files in the default changes.
266266
Those dependencies become available in a generated `requirements.bzl` file.
267267
You can instead check this `requirements.bzl` file into your repo, see the "vendoring" section below.
268268
269+
For advanced use-cases, such as handling multi-platform dependencies, see the
270+
[How-to: Multi-Platform PyPI Dependencies guide](/howto/multi-platform-pypi-deps).
271+
269272
In your WORKSPACE file:
270273
271274
```starlark

0 commit comments

Comments
 (0)