Skip to content

Commit a1a90d2

Browse files
committed
make the changes fully backwards compatible and ensure that the WORKSPACE code works
1 parent 775cf8b commit a1a90d2

File tree

8 files changed

+177
-135
lines changed

8 files changed

+177
-135
lines changed

CHANGELOG.md

+6-6
Original file line numberDiff line numberDiff line change
@@ -33,12 +33,12 @@ A brief description of the categories of changes:
3333

3434
### Added
3535

36-
* (pip) Allow specifying the requirements by (os, arch) and add extra validations when
37-
parsing the inputs. This should be a non-breaking change for most users
38-
unless they have been passing `requirements_linux` together with
39-
`extra_pip_args = ["--platform=manylinux_2_4_x86_64"]`, in which case they
40-
would have to change their code to use `requirements_lock` attribute which
41-
itself is a trivial change.
36+
* (pip) Allow specifying the requirements by (os, arch) and add extra
37+
validations when parsing the inputs. This is a non-breaking change for most
38+
users unless they have been passing multiple `requirements_*` files together
39+
with `extra_pip_args = ["--platform=manylinux_2_4_x86_64"]`, that was an
40+
invalid usage previously but we were not failing the build. From now on this
41+
is explicitly disallowed.
4242

4343
## [0.32.2] - 2024-05-14
4444

docs/sphinx/pip.md

+52-20
Original file line numberDiff line numberDiff line change
@@ -19,11 +19,41 @@ load("@pip_deps//:requirements.bzl", "install_deps")
1919
install_deps()
2020
```
2121

22+
For `bzlmod` an equivalent `MODULE.bazel` would look like:
23+
```starlark
24+
pip = use_extension("//python/extensions:pip.bzl", "pip")
25+
pip.parse(
26+
hub_name = "pip_deps",
27+
requirements_lock = ":requirements.txt",
28+
)
29+
use_repo(pip, "pip_deps")
30+
```
31+
2232
You can then reference installed dependencies from a `BUILD` file with:
2333

2434
```starlark
2535
load("@pip_deps//:requirements.bzl", "requirement")
2636

37+
py_library(
38+
name = "bar",
39+
...
40+
deps = [
41+
"//my/other:dep",
42+
"@pip_deps//requests",
43+
"@pip_deps//numpy",
44+
],
45+
)
46+
```
47+
48+
The rules also provide a convenience macro for translating the entries in the
49+
`requirements.txt` file (e.g. `opencv-python`) to the right bazel label (e.g.
50+
`@pip_deps//opencv_python`). The convention of bazel labels is lowercase
51+
`snake_case`, but you can use the helper to avoid depending on this convention
52+
as follows:
53+
54+
```starlark
55+
load("@pip_deps//:requirements.bzl", "requirement")
56+
2757
py_library(
2858
name = "bar",
2959
...
@@ -35,33 +65,35 @@ py_library(
3565
)
3666
```
3767

38-
In addition to the `requirement` macro, which is used to access the generated `py_library`
39-
target generated from a package's wheel, The generated `requirements.bzl` file contains
40-
functionality for exposing [entry points][whl_ep] as `py_binary` targets as well.
68+
If you would like to access [entry points][whl_ep], see the `py_console_script_binary` rule documentation.
4169

4270
[whl_ep]: https://packaging.python.org/specifications/entry-points/
4371

44-
```starlark
45-
load("@pip_deps//:requirements.bzl", "entry_point")
46-
47-
alias(
48-
name = "pip-compile",
49-
actual = entry_point(
50-
pkg = "pip-tools",
51-
script = "pip-compile",
52-
),
53-
)
54-
```
72+
(per-os-arch-requirements)=
73+
## Requirements for a specific OS/Architecture
5574

56-
Note that for packages whose name and script are the same, only the name of the package
57-
is needed when calling the `entry_point` macro.
75+
IN some cases you may need to use different requirements files for different OS, Arch combinations. This is enabled via the `requirements_by_platform` attribute in `pip.parse` extension and the `pip_parse` repository rule. The keys of the dictionary are labels to the file and the values are a list of comma separated target (os, arch) tuples.
5876

77+
For example:
5978
```starlark
60-
load("@pip_deps//:requirements.bzl", "entry_point")
79+
# ...
80+
requirements_by_platform = {
81+
"requirements_linux_x86_64.txt": "linux_x86_64",
82+
"requirements_osx.txt": "osx_x86_64,osx_aarch64",
83+
"requirements_linux_exotic.txt": "linux_exotic",
84+
},
85+
# For the list of standard platforms that the rules_python has toolchains for, default to
86+
# the following requirements file.
87+
"requirements_lock" = "requirements_lock.txt",
88+
```
6189

62-
alias(
63-
name = "flake8",
64-
actual = entry_point("flake8"),
90+
An alternative way is to use per-OS requirement attributes.
91+
```starlark
92+
# ...
93+
requirements_darwin = "requirements_darwin.txt",
94+
# For the list of standard platforms that the rules_python has toolchains for, default to
95+
# the following requirements file.
96+
"requirements_lock" = "requirements_lock.txt",
6597
)
6698
```
6799

python/pip_install/pip_repository.bzl

+3-3
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ load("//python/pip_install/private:srcs.bzl", "PIP_INSTALL_PY_SRCS")
2424
load("//python/private:auth.bzl", "AUTH_ATTRS", "get_auth")
2525
load("//python/private:envsubst.bzl", "envsubst")
2626
load("//python/private:normalize_name.bzl", "normalize_name")
27-
load("//python/private:parse_requirements.bzl", "parse_requirements", "select_requirement")
27+
load("//python/private:parse_requirements.bzl", "host_platform", "parse_requirements", "select_requirement")
2828
load("//python/private:parse_whl_name.bzl", "parse_whl_name")
2929
load("//python/private:patch_whl.bzl", "patch_whl")
3030
load("//python/private:render_pkg_aliases.bzl", "render_pkg_aliases", "whl_alias")
@@ -284,11 +284,11 @@ def _pip_repository_impl(rctx):
284284
)
285285
selected_requirements = {}
286286
options = None
287+
repository_platform = host_platform(rctx.os)
287288
for name, requirements in requirements_by_platform.items():
288289
r = select_requirement(
289290
requirements,
290-
host_os = rctx.os.name,
291-
host_cpu = rctx.os.arch,
291+
platform = repository_platform,
292292
)
293293
if not r:
294294
continue

python/private/bzlmod/pip.bzl

+5-6
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ load(
2424
)
2525
load("//python/private:auth.bzl", "AUTH_ATTRS")
2626
load("//python/private:normalize_name.bzl", "normalize_name")
27-
load("//python/private:parse_requirements.bzl", "parse_requirements", "select_requirement")
27+
load("//python/private:parse_requirements.bzl", "host_platform", "parse_requirements", "select_requirement")
2828
load("//python/private:parse_whl_name.bzl", "parse_whl_name")
2929
load("//python/private:pypi_index.bzl", "simpleapi_download")
3030
load("//python/private:render_pkg_aliases.bzl", "whl_alias")
@@ -162,7 +162,7 @@ def _create_whl_repos(module_ctx, pip_attr, whl_map, whl_overrides, group_map, s
162162

163163
requirements_by_platform = parse_requirements(
164164
module_ctx,
165-
requirements_by_platform = pip_attr.experimental_requirements_by_platform,
165+
requirements_by_platform = pip_attr.requirements_by_platform,
166166
requirements_linux = pip_attr.requirements_linux,
167167
requirements_lock = pip_attr.requirements_lock,
168168
requirements_osx = pip_attr.requirements_darwin,
@@ -195,11 +195,11 @@ def _create_whl_repos(module_ctx, pip_attr, whl_map, whl_overrides, group_map, s
195195
parallel_download = pip_attr.parallel_download,
196196
)
197197

198+
repository_platform = host_platform(module_ctx.os)
198199
for whl_name, requirements in requirements_by_platform.items():
199200
requirement = select_requirement(
200201
requirements,
201-
host_os = module_ctx.os.name,
202-
host_cpu = module_ctx.os.arch,
202+
platform = repository_platform,
203203
)
204204
if not requirement:
205205
# Sometimes the package is not present for host platform if there
@@ -284,8 +284,7 @@ def _create_whl_repos(module_ctx, pip_attr, whl_map, whl_overrides, group_map, s
284284
# Older python versions have wheels for the `*m` ABI.
285285
"cp" + major_minor.replace(".", "") + "m",
286286
],
287-
want_os = module_ctx.os.name,
288-
want_cpu = module_ctx.os.arch,
287+
want_platform = repository_platform,
289288
) or sdist
290289

291290
if distribution:

python/private/normalize_platform.bzl

-54
Original file line numberDiff line numberDiff line change
@@ -11,57 +11,3 @@
1111
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
1212
# See the License for the specific language governing permissions and
1313
# limitations under the License.
14-
15-
"""
16-
A small function to normalize the platform name.
17-
18-
This includes the vendored _translate_cpu and _translate_os from
19-
@platforms//host:extension.bzl at version 0.0.9 so that we don't
20-
force the users to depend on it.
21-
"""
22-
23-
def _translate_cpu(arch):
24-
if arch in ["i386", "i486", "i586", "i686", "i786", "x86"]:
25-
return "x86_32"
26-
if arch in ["amd64", "x86_64", "x64"]:
27-
return "x86_64"
28-
if arch in ["ppc", "ppc64", "ppc64le"]:
29-
return "ppc"
30-
if arch in ["arm", "armv7l"]:
31-
return "arm"
32-
if arch in ["aarch64"]:
33-
return "aarch64"
34-
if arch in ["s390x", "s390"]:
35-
return "s390x"
36-
if arch in ["mips64el", "mips64"]:
37-
return "mips64"
38-
if arch in ["riscv64"]:
39-
return "riscv64"
40-
return None
41-
42-
def _translate_os(os):
43-
if os.startswith("mac os"):
44-
return "osx"
45-
if os.startswith("freebsd"):
46-
return "freebsd"
47-
if os.startswith("openbsd"):
48-
return "openbsd"
49-
if os.startswith("linux"):
50-
return "linux"
51-
if os.startswith("windows"):
52-
return "windows"
53-
return None
54-
55-
def normalize_platform(*, os, cpu):
56-
"""Normalize the platform.
57-
58-
Args:
59-
os: The operating system name.
60-
cpu: The CPU name.
61-
62-
Returns: A normalized string
63-
"""
64-
return "{}_{}".format(
65-
_translate_os(os),
66-
_translate_cpu(cpu),
67-
)

python/private/parse_requirements.bzl

+80-23
Original file line numberDiff line numberDiff line change
@@ -28,10 +28,45 @@ behavior.
2828

2929
load("//python/pip_install:requirements_parser.bzl", "parse")
3030
load(":normalize_name.bzl", "normalize_name")
31-
load(":normalize_platform.bzl", "normalize_platform")
3231
load(":pypi_index_sources.bzl", "get_simpleapi_sources")
3332
load(":whl_target_platforms.bzl", "whl_target_platforms")
3433

34+
# This includes the vendored _translate_cpu and _translate_os from
35+
# @platforms//host:extension.bzl at version 0.0.9 so that we don't
36+
# force the users to depend on it.
37+
38+
def _translate_cpu(arch):
39+
if arch in ["i386", "i486", "i586", "i686", "i786", "x86"]:
40+
return "x86_32"
41+
if arch in ["amd64", "x86_64", "x64"]:
42+
return "x86_64"
43+
if arch in ["ppc", "ppc64", "ppc64le"]:
44+
return "ppc"
45+
if arch in ["arm", "armv7l"]:
46+
return "arm"
47+
if arch in ["aarch64"]:
48+
return "aarch64"
49+
if arch in ["s390x", "s390"]:
50+
return "s390x"
51+
if arch in ["mips64el", "mips64"]:
52+
return "mips64"
53+
if arch in ["riscv64"]:
54+
return "riscv64"
55+
return arch
56+
57+
def _translate_os(os):
58+
if os.startswith("mac os"):
59+
return "osx"
60+
if os.startswith("freebsd"):
61+
return "freebsd"
62+
if os.startswith("openbsd"):
63+
return "openbsd"
64+
if os.startswith("linux"):
65+
return "linux"
66+
if os.startswith("windows"):
67+
return "windows"
68+
return os
69+
3570
# TODO @aignas 2024-05-13: consider using the same platform tags as are used in
3671
# the //python:versions.bzl
3772
DEFAULT_PLATFORMS = [
@@ -104,7 +139,7 @@ def parse_requirements(
104139
requirements_windows (label): The requirements file for windows OS.
105140
extra_pip_args (string list): Extra pip arguments to perform extra validations and to
106141
be joined with args fined in files.
107-
fail_fn (fn): A failure function used in testing failure cases.
142+
fail_fn (Callable[[str], None]): A failure function used in testing failure cases.
108143
109144
Returns:
110145
A tuple where the first element a dict of dicts where the first key is
@@ -125,29 +160,36 @@ def parse_requirements(
125160
requirements_windows or
126161
requirements_by_platform
127162
):
128-
fail_fn("""\
129-
A requirements_lock attribute must be specified, or a platform-specific lockfile using one of the requirements_* or requirements_by_platform attributes.
130-
""")
163+
fail_fn(
164+
"A 'requirements_lock' attribute must be specified, a platform-specific lockfiles " +
165+
"via 'requirements_by_platform' or an os-specific lockfiles must be specified " +
166+
"via 'requirements_*' attributes",
167+
)
131168
return None
132169

133170
platforms = _platforms_from_args(extra_pip_args)
134171

135-
if platforms and (
136-
not requirements_lock or
137-
requirements_linux or
138-
requirements_osx or
139-
requirements_windows or
140-
requirements_by_platform
141-
):
142-
# If the --platform argument is used, check that we are using
143-
# `requirements_lock` file instead of the OS specific ones as that is
144-
# the only correct way to use the API.
145-
fail_fn("only 'requirements_lock' can be used when using '--platform' pip argument")
146-
return None
147-
148172
if platforms:
173+
lock_files = [
174+
f
175+
for f in [
176+
requirements_lock,
177+
requirements_linux,
178+
requirements_osx,
179+
requirements_windows,
180+
] + list(requirements_by_platform.keys())
181+
if f
182+
]
183+
184+
if len(lock_files) > 1:
185+
# If the --platform argument is used, check that we are using
186+
# a single `requirements_lock` file instead of the OS specific ones as that is
187+
# the only correct way to use the API.
188+
fail_fn("only a single 'requirements_lock' file can be used when using '--platform' pip argument, consider specifying it via 'requirements_lock' attribute")
189+
return None
190+
149191
files_by_platform = [
150-
(requirements_lock, platforms),
192+
(lock_files[0], platforms),
151193
]
152194
else:
153195
files_by_platform = {
@@ -257,21 +299,20 @@ A requirements_lock attribute must be specified, or a platform-specific lockfile
257299
for whl_name, reqs in requirements_by_platform.items()
258300
}
259301

260-
def select_requirement(requirements, *, host_os, host_cpu):
302+
def select_requirement(requirements, *, platform):
261303
"""A simple function to get a requirement for a particular platform.
262304
263305
Args:
264306
requirements (list[struct]): The list of requirements as returned by
265307
the `parse_requirements` function above.
266-
host_os (str): The host OS.
267-
host_cpu (str): The host CPU.
308+
platform (str): The host platform. Usually an output of the
309+
`host_platform` function.
268310
269311
Returns:
270312
None if not found or a struct returned as one of the values in the
271313
parse_requirements function. The requirement that should be downloaded
272314
by the host platform will be returned.
273315
"""
274-
platform = normalize_platform(os = host_os, cpu = host_cpu)
275316
maybe_requirement = [
276317
req
277318
for req in requirements
@@ -287,3 +328,19 @@ def select_requirement(requirements, *, host_os, host_cpu):
287328
return None
288329

289330
return maybe_requirement[0]
331+
332+
def host_platform(repository_os):
333+
"""Return a string representation of the repository OS.
334+
335+
Args:
336+
repository_os (struct): The `module_ctx.os` or `repository_ctx.os` attribute.
337+
See https://bazel.build/rules/lib/builtins/repository_os.html
338+
339+
Returns:
340+
The string representation of the platform that we can later used in the `pip`
341+
machinery.
342+
"""
343+
return "{}_{}".format(
344+
_translate_os(repository_os.name.lower()),
345+
_translate_cpu(repository_os.arch.lower()),
346+
)

0 commit comments

Comments
 (0)