Skip to content

Commit f068380

Browse files
committed
feat(whl_library): set target_compatible_with based on whl name
This adds a pure starlark function to further parse the platform_tag from the wheel and set the correct attributes for the target generated by the whl_library rule. This should make errors much more easy to understand when users try to build a docker image using incompatible libraries. This is currently hidden under an experimental flag described in the changelog.
1 parent d96214f commit f068380

File tree

10 files changed

+198
-1
lines changed

10 files changed

+198
-1
lines changed

CHANGELOG.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,13 @@ Breaking changes:
9797
* (utils) Added a `pip_utils` struct with a `normalize_name` function to allow users
9898
to find out how `rules_python` would normalize a PyPI distribution name.
9999

100+
* (whl_library) Added an `experimental_set_target_compatible_with` to `whl_library` and
101+
`pip.parse` module extension which will configure bazel constraints for `py_library`
102+
targets derived from a `whl` file via repository rules. In order to use it, you need to
103+
specificy `experimental_set_target_compatible_with = True` to either `pip.parse` in your
104+
`MODULE.bazel` or `install_deps` from the auto-generated `requirements.bzl` if you are
105+
using `WORKSPACE`.
106+
100107
## [0.26.0] - 2023-10-06
101108

102109
### Changed

examples/bzlmod/MODULE.bazel

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,8 @@ use_repo(pip, "whl_mods_hub")
8888
# Alternatively, `python_interpreter_target` can be used to directly specify
8989
# the Python interpreter to run to resolve dependencies.
9090
pip.parse(
91+
# Experimental flags to test behaviour on CI, not part of example
92+
experimental_set_target_compatible_with = True,
9193
hub_name = "pip",
9294
python_version = "3.9",
9395
requirements_lock = "//:requirements_lock_3_9.txt",
@@ -101,6 +103,8 @@ pip.parse(
101103
},
102104
)
103105
pip.parse(
106+
# Experimental flags to test behaviour on CI, not part of example
107+
experimental_set_target_compatible_with = True,
104108
hub_name = "pip",
105109
python_version = "3.10",
106110
requirements_lock = "//:requirements_lock_3_10.txt",

examples/bzlmod/other_module/MODULE.bazel

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,8 @@ use_repo(
4646

4747
pip = use_extension("@rules_python//python/extensions:pip.bzl", "pip")
4848
pip.parse(
49+
# Experimental flags to test behaviour on CI, not part of example
50+
experimental_set_target_compatible_with = True,
4951
hub_name = "other_module_pip",
5052
# NOTE: This version must be different than the root module's
5153
# default python version.

python/pip_install/pip_repository.bzl

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -668,6 +668,7 @@ def _whl_library_impl(rctx):
668668
],
669669
entry_points = entry_points,
670670
annotation = None if not rctx.attr.annotation else struct(**json.decode(rctx.read(rctx.attr.annotation))),
671+
set_target_compatible_with = rctx.attr.experimental_set_target_compatible_with,
671672
)
672673
rctx.file("BUILD.bazel", build_file_contents)
673674

@@ -709,6 +710,11 @@ whl_library_attrs = {
709710
),
710711
allow_files = True,
711712
),
713+
"experimental_set_target_compatible_with": attr.bool(
714+
default = False,
715+
doc = "A flag to set 'target_compatible_with' attribute for the py_library target. " +
716+
"This is detected from the whl file name.",
717+
),
712718
"repo": attr.string(
713719
mandatory = True,
714720
doc = "Pointer to parent repo name. Used to make these rules rerun if the parent repo changes.",

python/pip_install/private/generate_whl_library_build_bazel.bzl

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
"""Generate the BUILD.bazel contents for a repo defined by a whl_library."""
1616

1717
load("//python/private:normalize_name.bzl", "normalize_name")
18+
load("//python/private:parse_whl_name.bzl", "whl_target_compatible_with")
1819

1920
_WHEEL_FILE_LABEL = "whl"
2021
_PY_LIBRARY_LABEL = "pkg"
@@ -77,6 +78,7 @@ py_library(
7778
["site-packages/**/*"],
7879
exclude={data_exclude},
7980
),
81+
target_compatible_with = {target_compatible_with},
8082
# This makes this directory a top-level in the python import
8183
# search path for anything that depends on this.
8284
imports = ["site-packages"],
@@ -93,7 +95,8 @@ def generate_whl_library_build_bazel(
9395
data_exclude,
9496
tags,
9597
entry_points,
96-
annotation = None):
98+
annotation = None,
99+
set_target_compatible_with = False):
97100
"""Generate a BUILD file for an unzipped Wheel
98101
99102
Args:
@@ -104,6 +107,8 @@ def generate_whl_library_build_bazel(
104107
tags: list of tags to apply to generated py_library rules.
105108
entry_points: A dict of entry points to add py_binary rules for.
106109
annotation: The annotation for the build file.
110+
set_target_compatible_with: Set constraints for `py_library` based
111+
on the whl name.
107112
108113
Returns:
109114
A complete BUILD file as a string
@@ -178,6 +183,7 @@ def generate_whl_library_build_bazel(
178183
entry_point_prefix = _WHEEL_ENTRY_POINT_PREFIX,
179184
srcs_exclude = repr(srcs_exclude),
180185
data = repr(data),
186+
target_compatible_with = "None" if not set_target_compatible_with else repr(whl_target_compatible_with(whl_name)),
181187
),
182188
] + additional_content,
183189
)

python/private/bzlmod/pip.bzl

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,7 @@ def _create_whl_repos(module_ctx, pip_attr, whl_map, whl_overrides):
146146
pip_data_exclude = pip_attr.pip_data_exclude,
147147
enable_implicit_namespace_pkgs = pip_attr.enable_implicit_namespace_pkgs,
148148
environment = pip_attr.environment,
149+
experimental_set_target_compatible_with = pip_attr.experimental_set_target_compatible_with,
149150
)
150151

151152
if whl_name not in whl_map[hub_name]:
@@ -307,6 +308,11 @@ def _pip_impl(module_ctx):
307308

308309
def _pip_parse_ext_attrs():
309310
attrs = dict({
311+
"experimental_set_target_compatible_with": attr.bool(
312+
default = False,
313+
doc = "A flag to set 'target_compatible_with' attribute for the py_library target. " +
314+
"This is detected from the whl file name.",
315+
),
310316
"hub_name": attr.string(
311317
mandatory = True,
312318
doc = """

python/private/parse_whl_name.bzl

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,32 @@
1616
A starlark implementation of a Wheel filename parsing.
1717
"""
1818

19+
_LEGACY_ALIASES = {
20+
"manylinux1_i686": "manylinux_2_5_i686",
21+
"manylinux1_x86_64": "manylinux_2_5_x86_64",
22+
"manylinux2010_i686": "manylinux_2_12_i686",
23+
"manylinux2010_x86_64": "manylinux_2_12_x86_64",
24+
"manylinux2014_aarch64": "manylinux_2_17_aarch64",
25+
"manylinux2014_armv7l": "manylinux_2_17_armv7l",
26+
"manylinux2014_i686": "manylinux_2_17_i686",
27+
"manylinux2014_ppc64": "manylinux_2_17_ppc64",
28+
"manylinux2014_ppc64le": "manylinux_2_17_ppc64le",
29+
"manylinux2014_s390x": "manylinux_2_17_s390x",
30+
"manylinux2014_x86_64": "manylinux_2_17_x86_64",
31+
}
32+
33+
_ARCH = {
34+
"aarch64": "aarch64",
35+
"amd64": "x86_64",
36+
"arm64": "aarch64",
37+
"armv7l": "aarch32",
38+
"i686": "x86_32",
39+
"ppc64": "ppc",
40+
"ppc64le": "ppc64le",
41+
"s390x": "s390x",
42+
"x86_64": "x86_64",
43+
}
44+
1945
def parse_whl_name(file):
2046
"""Parse whl file name into a struct of constituents.
2147
@@ -70,3 +96,80 @@ def parse_whl_name(file):
7096
abi_tag = abi_tag,
7197
platform_tag = platform_tag,
7298
)
99+
100+
def _convert_from_legacy(platform_tag):
101+
return _LEGACY_ALIASES.get(platform_tag, platform_tag)
102+
103+
def whl_target_compatible_with(file):
104+
"""Parse whl file and return compatibility list.
105+
106+
Args:
107+
file (str): The file name of a wheel
108+
109+
Returns:
110+
A list that can be put into target_compatible_with
111+
"""
112+
parsed = parse_whl_name(file)
113+
114+
if parsed.platform_tag == "any" and parsed.abi_tag == "none":
115+
return []
116+
117+
# TODO @aignas 2023-11-16: add ABI handling
118+
119+
platform, _, _ = parsed.platform_tag.partition(".")
120+
platform = _convert_from_legacy(platform)
121+
122+
if platform.startswith("manylinux"):
123+
_, _, tail = platform.partition("_")
124+
125+
_glibc_major, _, tail = tail.partition("_") # Discard as this is currently unused
126+
_glibc_minor, _, arch = tail.partition("_") # Discard as this is currently unused
127+
128+
return [
129+
"@platforms//cpu:" + _ARCH.get(arch, arch),
130+
"@platforms//os:linux",
131+
]
132+
# TODO @aignas 2023-11-16: figure out when this happens, perhaps it is when
133+
# we build a wheel instead ourselves instead of downloading it from PyPI?
134+
135+
elif platform.startswith("linux_"):
136+
_, _, arch = platform.partition("_")
137+
138+
return [
139+
"@platforms//cpu:" + _ARCH.get(arch, arch),
140+
"@platforms//os:linux",
141+
]
142+
143+
elif platform.startswith("macosx"):
144+
_, _, tail = platform.partition("_")
145+
146+
_os_major, _, tail = tail.partition("_") # Discard as this is currently unused
147+
_os_minor, _, arch = tail.partition("_") # Discard as this is currently unused
148+
149+
if arch.startswith("universal"):
150+
return ["@platforms//os:osx"]
151+
else:
152+
return [
153+
"@platforms//cpu:" + _ARCH.get(arch, arch),
154+
"@platforms//os:osx",
155+
]
156+
elif platform.startswith("win"):
157+
if platform == "win32":
158+
return [
159+
"@platforms//cpu:x86_32",
160+
"@platforms//os:windows",
161+
]
162+
elif platform == "win64":
163+
return [
164+
"@platforms//cpu:x86_32",
165+
"@platforms//os:windows",
166+
]
167+
168+
_, _, arch = platform.partition("_")
169+
170+
return [
171+
"@platforms//cpu:" + _ARCH.get(arch, arch),
172+
"@platforms//os:windows",
173+
]
174+
175+
fail("Could not parse platform values for a wheel platform: '{}'".format(parsed))

tests/pip_install/whl_library/generate_build_bazel_tests.bzl

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ py_library(
5555
["site-packages/**/*"],
5656
exclude=["**/* *", "**/*.py", "**/*.pyc", "**/*.pyc.*", "**/*.dist-info/RECORD"],
5757
),
58+
target_compatible_with = None,
5859
# This makes this directory a top-level in the python import
5960
# search path for anything that depends on this.
6061
imports = ["site-packages"],
@@ -111,6 +112,7 @@ py_library(
111112
["site-packages/**/*"],
112113
exclude=["**/* *", "**/*.py", "**/*.pyc", "**/*.pyc.*", "**/*.dist-info/RECORD", "data_exclude_all"],
113114
),
115+
target_compatible_with = None,
114116
# This makes this directory a top-level in the python import
115117
# search path for anything that depends on this.
116118
imports = ["site-packages"],
@@ -190,6 +192,7 @@ py_library(
190192
["site-packages/**/*"],
191193
exclude=["**/* *", "**/*.py", "**/*.pyc", "**/*.pyc.*", "**/*.dist-info/RECORD"],
192194
),
195+
target_compatible_with = None,
193196
# This makes this directory a top-level in the python import
194197
# search path for anything that depends on this.
195198
imports = ["site-packages"],
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
load(":whl_target_compatible_with_tests.bzl", "whl_target_compatible_with_test_suite")
2+
3+
whl_target_compatible_with_test_suite(name = "whl_target_compatible_with_tests")
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
# Copyright 2023 The Bazel Authors. All rights reserved.
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
""
16+
17+
load("@rules_testing//lib:test_suite.bzl", "test_suite")
18+
load("//python/private:parse_whl_name.bzl", "whl_target_compatible_with") # buildifier: disable=bzl-visibility
19+
20+
_tests = []
21+
22+
def _test_compatible_with_all(env):
23+
got = whl_target_compatible_with("foo-1.2.3-py3-none-any.whl")
24+
env.expect.that_collection(got).contains_exactly([])
25+
26+
_tests.append(_test_compatible_with_all)
27+
28+
def _test_multiple_platforms(env):
29+
got = whl_target_compatible_with("bar-3.2.1-py3-abi3-manylinux1_x86_64.manylinux2_x86_64.whl")
30+
env.expect.that_collection(got).contains_exactly([
31+
"@platforms//os:linux",
32+
"@platforms//cpu:x86_64",
33+
])
34+
35+
_tests.append(_test_multiple_platforms)
36+
37+
def _test_real_numpy_wheel(env):
38+
got = whl_target_compatible_with("numpy-1.26.1-pp39-pypy39_pp73-macosx_10_9_x86_64.whl")
39+
env.expect.that_collection(got).contains_exactly([
40+
"@platforms//os:osx",
41+
"@platforms//cpu:x86_64",
42+
])
43+
44+
_tests.append(_test_real_numpy_wheel)
45+
46+
# TODO @aignas 2023-11-16: add handling for musllinux as simple linux for now.
47+
# TODO @aignas 2023-11-16: add macos universal testcase.
48+
# TODO @aignas 2023-11-16: add Windows testcases.
49+
# TODO @aignas 2023-11-16: add error handling when the wheel filename is something else.
50+
51+
def whl_target_compatible_with_test_suite(name):
52+
"""Create the test suite.
53+
54+
Args:
55+
name: the name of the test suite
56+
"""
57+
test_suite(name = name, basic_tests = _tests)

0 commit comments

Comments
 (0)