|
| 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 | +``` |
0 commit comments