Skip to content

Commit 4a262fa

Browse files
authored
fix(pypi): fix the whl selection algorithm after #2069 (#2078)
It seems that a few things broke in recent commits: - We are not using the `MODULE.bazel.lock` file and it seems that it is easy to miss when the components in the PyPI extension stop integrating well together. This happened during the switch to `{abi}_{os}_{plat}` target platform passing within the code. - The logger code stopped working in the extension after the recent additions to add the `rule_name`. - `repo_utils.getenv` was always getting `PATH` env var on bazel `6.x`. This PR fixes both cases and updates docs to serve as a better reminder. By fixing the `select_whls` code and we can just rely on target platform triples (ABI, OS, CPU). This gets one step closer to maybe supporting optional `python_version` which would address #1708. Whilst at it we are also adding different status messages for building the wheel from `sdist` vs just extracting or downloading the wheel. Tests: - Added more unit tests and brought them in line with the rest of the code. - Checked manually for differences between the `MODULE.bazel.lock` files in our `rules_python` extension before #2069 and after this PR and there are no differences except in the `experimental_target_platforms` attribute in `whl_library`. Before this PR you would see that we do not select any wheels for e.g. `MarkupSafe` and we are always building from `sdist`. Work towards #260.
1 parent ae2eb70 commit 4a262fa

File tree

8 files changed

+110
-119
lines changed

8 files changed

+110
-119
lines changed

CHANGELOG.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,8 @@ A brief description of the categories of changes:
2525
[x.x.x]: https://github.com/bazelbuild/rules_python/releases/tag/x.x.x
2626

2727
### Changed
28-
* Nothing yet
28+
* (whl_library) A better log message when the wheel is built from an sdist or
29+
when the wheel is downloaded using `download_only` feature to aid debugging.
2930

3031
### Fixed
3132
* (rules) Signals are properly received when using {obj}`--bootstrap_impl=script`

python/private/pypi/extension.bzl

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,7 @@ You cannot use both the additive_build_content and additive_build_content_file a
9999
)
100100

101101
def _create_whl_repos(module_ctx, pip_attr, whl_map, whl_overrides, group_map, simpleapi_cache, exposed_packages):
102-
logger = repo_utils.logger(module_ctx)
102+
logger = repo_utils.logger(module_ctx, "pypi:create_whl_repos")
103103
python_interpreter_target = pip_attr.python_interpreter_target
104104
is_hub_reproducible = True
105105

@@ -195,7 +195,6 @@ def _create_whl_repos(module_ctx, pip_attr, whl_map, whl_overrides, group_map, s
195195
logger = logger,
196196
),
197197
get_index_urls = get_index_urls,
198-
python_version = major_minor,
199198
logger = logger,
200199
)
201200

python/private/pypi/parse_requirements.bzl

Lines changed: 5 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -38,9 +38,7 @@ def parse_requirements(
3838
requirements_by_platform = {},
3939
extra_pip_args = [],
4040
get_index_urls = None,
41-
python_version = None,
42-
logger = None,
43-
fail_fn = fail):
41+
logger = None):
4442
"""Get the requirements with platforms that the requirements apply to.
4543
4644
Args:
@@ -53,10 +51,7 @@ def parse_requirements(
5351
get_index_urls: Callable[[ctx, list[str]], dict], a callable to get all
5452
of the distribution URLs from a PyPI index. Accepts ctx and
5553
distribution names to query.
56-
python_version: str or None. This is needed when the get_index_urls is
57-
specified. It should be of the form "3.x.x",
5854
logger: repo_utils.logger or None, a simple struct to log diagnostic messages.
59-
fail_fn (Callable[[str], None]): A failure function used in testing failure cases.
6055
6156
Returns:
6257
A tuple where the first element a dict of dicts where the first key is
@@ -137,10 +132,6 @@ def parse_requirements(
137132

138133
index_urls = {}
139134
if get_index_urls:
140-
if not python_version:
141-
fail_fn("'python_version' must be provided")
142-
return None
143-
144135
index_urls = get_index_urls(
145136
ctx,
146137
# Use list({}) as a way to have a set
@@ -168,9 +159,8 @@ def parse_requirements(
168159

169160
for r in sorted(reqs.values(), key = lambda r: r.requirement_line):
170161
whls, sdist = _add_dists(
171-
r,
172-
index_urls.get(whl_name),
173-
python_version = python_version,
162+
requirement = r,
163+
index_urls = index_urls.get(whl_name),
174164
logger = logger,
175165
)
176166

@@ -238,15 +228,14 @@ def host_platform(ctx):
238228
repo_utils.get_platforms_cpu_name(ctx),
239229
)
240230

241-
def _add_dists(requirement, index_urls, python_version, logger = None):
231+
def _add_dists(*, requirement, index_urls, logger = None):
242232
"""Populate dists based on the information from the PyPI index.
243233
244234
This function will modify the given requirements_by_platform data structure.
245235
246236
Args:
247237
requirement: The result of parse_requirements function.
248238
index_urls: The result of simpleapi_download.
249-
python_version: The version of the python interpreter.
250239
logger: A logger for printing diagnostic info.
251240
"""
252241
if not index_urls:
@@ -289,18 +278,6 @@ def _add_dists(requirement, index_urls, python_version, logger = None):
289278
]))
290279

291280
# Filter out the wheels that are incompatible with the target_platforms.
292-
whls = select_whls(
293-
whls = whls,
294-
want_abis = [
295-
"none",
296-
"abi3",
297-
"cp" + python_version.replace(".", ""),
298-
# Older python versions have wheels for the `*m` ABI.
299-
"cp" + python_version.replace(".", "") + "m",
300-
],
301-
want_platforms = requirement.target_platforms,
302-
want_python_version = python_version,
303-
logger = logger,
304-
)
281+
whls = select_whls(whls = whls, want_platforms = requirement.target_platforms, logger = logger)
305282

306283
return whls, sdist

python/private/pypi/whl_library.bzl

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -231,9 +231,16 @@ def _whl_library_impl(rctx):
231231
args = _parse_optional_attrs(rctx, args, extra_pip_args)
232232

233233
if not whl_path:
234+
if rctx.attr.urls:
235+
op_tmpl = "whl_library.BuildWheelFromSource({name}, {requirement})"
236+
elif rctx.attr.download_only:
237+
op_tmpl = "whl_library.DownloadWheel({name}, {requirement})"
238+
else:
239+
op_tmpl = "whl_library.ResolveRequirement({name}, {requirement})"
240+
234241
repo_utils.execute_checked(
235242
rctx,
236-
op = "whl_library.ResolveRequirement({}, {})".format(rctx.attr.name, rctx.attr.requirement),
243+
op = op_tmpl.format(name = rctx.attr.name, requirement = rctx.attr.requirement),
237244
arguments = args,
238245
environment = environment,
239246
quiet = rctx.attr.quiet,

python/private/pypi/whl_target_platforms.bzl

Lines changed: 32 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -46,15 +46,13 @@ _OS_PREFIXES = {
4646
"win": "windows",
4747
} # buildifier: disable=unsorted-dict-items
4848

49-
def select_whls(*, whls, want_python_version = "3.0", want_abis = [], want_platforms = [], logger = None):
49+
def select_whls(*, whls, want_platforms = [], logger = None):
5050
"""Select a subset of wheels suitable for target platforms from a list.
5151
5252
Args:
5353
whls(list[struct]): A list of candidates which have a `filename`
5454
attribute containing the `whl` filename.
55-
want_python_version(str): An optional parameter to filter whls by python version. Defaults to '3.0'.
56-
want_abis(list[str]): A list of ABIs that are supported.
57-
want_platforms(str): The platforms
55+
want_platforms(str): The platforms in "{abi}_{os}_{cpu}" or "{os}_{cpu}" format.
5856
logger: A logger for printing diagnostic messages.
5957
6058
Returns:
@@ -64,9 +62,34 @@ def select_whls(*, whls, want_python_version = "3.0", want_abis = [], want_platf
6462
if not whls:
6563
return []
6664

67-
version_limit = -1
68-
if want_python_version:
69-
version_limit = int(want_python_version.split(".")[1])
65+
want_abis = {
66+
"abi3": None,
67+
"none": None,
68+
}
69+
70+
_want_platforms = {}
71+
version_limit = None
72+
73+
for p in want_platforms:
74+
if not p.startswith("cp3"):
75+
fail("expected all platforms to start with ABI, but got: {}".format(p))
76+
77+
abi, _, os_cpu = p.partition("_")
78+
_want_platforms[os_cpu] = None
79+
_want_platforms[p] = None
80+
81+
version_limit_candidate = int(abi[3:])
82+
if not version_limit:
83+
version_limit = version_limit_candidate
84+
if version_limit and version_limit != version_limit_candidate:
85+
fail("Only a single python version is supported for now")
86+
87+
# For some legacy implementations the wheels may target the `cp3xm` ABI
88+
_want_platforms["{}m_{}".format(abi, os_cpu)] = None
89+
want_abis[abi] = None
90+
want_abis[abi + "m"] = None
91+
92+
want_platforms = sorted(_want_platforms)
7093

7194
candidates = {}
7295
for whl in whls:
@@ -101,7 +124,7 @@ def select_whls(*, whls, want_python_version = "3.0", want_abis = [], want_platf
101124
logger.trace(lambda: "Discarding the whl because the whl abi did not match")
102125
continue
103126

104-
if version_limit != -1 and whl_version_min > version_limit:
127+
if whl_version_min > version_limit:
105128
if logger:
106129
logger.trace(lambda: "Discarding the whl because the whl supported python version is too high")
107130
continue
@@ -110,7 +133,7 @@ def select_whls(*, whls, want_python_version = "3.0", want_abis = [], want_platf
110133
if parsed.platform_tag == "any":
111134
compatible = True
112135
else:
113-
for p in whl_target_platforms(parsed.platform_tag):
136+
for p in whl_target_platforms(parsed.platform_tag, abi_tag = parsed.abi_tag.strip("m") if parsed.abi_tag.startswith("cp") else None):
114137
if p.target_platform in want_platforms:
115138
compatible = True
116139
break

python/private/repo_utils.bzl

Lines changed: 18 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -42,22 +42,23 @@ def _debug_print(rctx, message_cb):
4242
if _is_repo_debug_enabled(rctx):
4343
print(message_cb()) # buildifier: disable=print
4444

45-
def _logger(rctx):
45+
def _logger(ctx, name = None):
4646
"""Creates a logger instance for printing messages.
4747
4848
Args:
49-
rctx: repository_ctx object. If the attribute `_rule_name` is
50-
present, it will be included in log messages.
49+
ctx: repository_ctx or module_ctx object. If the attribute
50+
`_rule_name` is present, it will be included in log messages.
51+
name: name for the logger. Optional for repository_ctx usage.
5152
5253
Returns:
5354
A struct with attributes logging: trace, debug, info, warn, fail.
5455
"""
55-
if _is_repo_debug_enabled(rctx):
56+
if _is_repo_debug_enabled(ctx):
5657
verbosity_level = "DEBUG"
5758
else:
5859
verbosity_level = "WARN"
5960

60-
env_var_verbosity = rctx.os.environ.get(REPO_VERBOSITY_ENV_VAR)
61+
env_var_verbosity = _getenv(ctx, REPO_VERBOSITY_ENV_VAR)
6162
verbosity_level = env_var_verbosity or verbosity_level
6263

6364
verbosity = {
@@ -66,18 +67,23 @@ def _logger(rctx):
6667
"TRACE": 3,
6768
}.get(verbosity_level, 0)
6869

70+
if hasattr(ctx, "attr"):
71+
# This is `repository_ctx`.
72+
name = name or "{}(@@{})".format(getattr(ctx.attr, "_rule_name", "?"), ctx.name)
73+
elif not name:
74+
fail("The name has to be specified when using the logger with `module_ctx`")
75+
6976
def _log(enabled_on_verbosity, level, message_cb_or_str):
7077
if verbosity < enabled_on_verbosity:
7178
return
72-
rule_name = getattr(rctx.attr, "_rule_name", "?")
79+
7380
if type(message_cb_or_str) == "string":
7481
message = message_cb_or_str
7582
else:
7683
message = message_cb_or_str()
7784

78-
print("\nrules_python:{}(@@{}) {}:".format(
79-
rule_name,
80-
rctx.name,
85+
print("\nrules_python:{} {}:".format(
86+
name,
8187
level.upper(),
8288
), message) # buildifier: disable=print
8389

@@ -278,12 +284,9 @@ def _which_describe_failure(binary_name, path):
278284
path = path,
279285
)
280286

281-
def _getenv(rctx, name, default = None):
282-
# Bazel 7+ API
283-
if hasattr(rctx, "getenv"):
284-
return rctx.getenv(name, default)
285-
else:
286-
return rctx.os.environ.get("PATH", default)
287+
def _getenv(ctx, name, default = None):
288+
# Bazel 7+ API has ctx.getenv
289+
return getattr(ctx, "getenv", ctx.os.environ.get)(name, default)
287290

288291
def _args_to_str(arguments):
289292
return " ".join([_arg_repr(a) for a in arguments])

tests/pypi/parse_requirements/parse_requirements_tests.bzl

Lines changed: 0 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -197,20 +197,6 @@ def _test_select_requirement_none_platform(env):
197197

198198
_tests.append(_test_select_requirement_none_platform)
199199

200-
def _test_fail_no_python_version(env):
201-
errors = []
202-
parse_requirements(
203-
ctx = _mock_ctx(),
204-
requirements_by_platform = {
205-
"requirements_lock": [""],
206-
},
207-
get_index_urls = lambda _, __: {},
208-
fail_fn = errors.append,
209-
)
210-
env.expect.that_str(errors[0]).equals("'python_version' must be provided")
211-
212-
_tests.append(_test_fail_no_python_version)
213-
214200
def parse_requirements_test_suite(name):
215201
"""Create the test suite.
216202

0 commit comments

Comments
 (0)