Skip to content

Commit be950f9

Browse files
aignasrickeylev
andauthored
refactor(pypi): A better error message when the wheel select hits no_match (#2519)
With this change we get the current values of the python configuration values printed in addition to the message printed previously. This should help us advise users who don't have their builds configured correctly. We are adding an extra `build_setting` which we can set in order to get an error message instead of a `DEBUG` warning. This has been documented as part of our config settings and in the `no_match_error` in the `select` statement. Example output now ```console $ bazel cquery --@rules_python//python/config_settings:python_version=3.12 @dev_pip//sphinx DEBUG: /home/aignas/src/github/aignas/rules_python/python/private/config_settings.bzl:193:14: The current configuration rules_python config flags is: @@//python/config_settings:pip_whl: "auto" @@//python/config_settings:pip_whl_glibc_version: "" @@//python/config_settings:pip_whl_muslc_version: "" @@//python/config_settings:pip_whl_osx_arch: "arch" @@//python/config_settings:pip_whl_osx_version: "" @@//python/config_settings:py_freethreaded: "no" @@//python/config_settings:py_linux_libc: "glibc" @@//python/config_settings:python_version: "3.12" If the value is missing, then the default value is being used, see documentation: https://rules-python.readthedocs.io/en/latest/api/rules_python/python/config_settings ERROR: /home/aignas/.cache/bazel/_bazel_aignas/6f0de8c9128ee8d5dbf27ba6dcc48bdd/external/+pip+dev_pip/sphinx/BUILD.bazel:6:12: configurable attribute "actual" in @@+pip+dev_pip//sphinx:_no_matching_repository doesn't match this configuration: No matching wheel for current configuration's Python version. The current build configuration's Python version doesn't match any of the Python wheels available for this distribution. This distribution supports the following Python configuration settings: //_config:is_cp3.11_py3_none_any //_config:is_cp3.13_py3_none_any To determine the current configuration's Python version, run: `bazel config <config id>` (shown further below) For the current configuration value see the debug message above that is printing the current flag values. If you can't see the message, then re-run the build to make it a failure instead by running the build with: --@@//python/config_settings:current_config=fail However, the command above will hide the `bazel config <config id>` message. This instance of @@+pip+dev_pip//sphinx:_no_matching_repository has configuration identifier 29ffcf8. To inspect its configuration, run: bazel config 29ffcf8. For more help, see https://bazel.build/docs/configurable-attributes#faq-select-choose-condition. ERROR: Analysis of target '@@+pip+dev_pip//sphinx:sphinx' failed; build aborted: Analysis failed INFO: Elapsed time: 0.112s INFO: 0 processes. ERROR: Build did NOT complete successfully ``` Fixes #2466 --------- Co-authored-by: Richard Levasseur <[email protected]>
1 parent e3c9406 commit be950f9

File tree

6 files changed

+175
-63
lines changed

6 files changed

+175
-63
lines changed

CHANGELOG.md

+3
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,9 @@ Unreleased changes template.
7070
* (pypi) Using {bzl:obj}`pip_parse.experimental_requirement_cycles` and
7171
{bzl:obj}`pip_parse.use_hub_alias_dependencies` together now works when
7272
using WORKSPACE files.
73+
* (pypi) The error messages when the wheel distributions do not match anything
74+
are now printing more details and include the currently active flag
75+
values. Fixes [#2466](https://github.com/bazelbuild/rules_python/issues/2466).
7376
* (py_proto_library) Fix import paths in Bazel 8.
7477

7578
[pep-695]: https://peps.python.org/pep-0695/

docs/api/rules_python/python/config_settings/index.md

+18
Original file line numberDiff line numberDiff line change
@@ -240,3 +240,21 @@ instead.
240240
:::
241241

242242
::::
243+
244+
::::{bzl:flag} current_config
245+
Fail the build if the current build configuration does not match the
246+
{obj}`pip.parse` defined wheels.
247+
248+
Values:
249+
* `fail`: Will fail in the build action ensuring that we get the error
250+
message no matter the action cache.
251+
* ``: (empty string) The default value, that will just print a warning.
252+
253+
:::{seealso}
254+
{obj}`pip.parse`
255+
:::
256+
257+
:::{versionadded} 1.1.0
258+
:::
259+
260+
::::

python/config_settings/BUILD.bazel

+9
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,15 @@ filegroup(
2929
construct_config_settings(
3030
name = "construct_config_settings",
3131
default_version = DEFAULT_PYTHON_VERSION,
32+
documented_flags = [
33+
":pip_whl",
34+
":pip_whl_glibc_version",
35+
":pip_whl_muslc_version",
36+
":pip_whl_osx_arch",
37+
":pip_whl_osx_version",
38+
":py_freethreaded",
39+
":py_linux_libc",
40+
],
3241
minor_mapping = MINOR_MAPPING,
3342
versions = PYTHON_VERSIONS,
3443
)

python/private/config_settings.bzl

+71-2
Original file line numberDiff line numberDiff line change
@@ -17,12 +17,21 @@
1717

1818
load("@bazel_skylib//lib:selects.bzl", "selects")
1919
load("@bazel_skylib//rules:common_settings.bzl", "BuildSettingInfo")
20+
load("//python/private:text_util.bzl", "render")
2021
load(":semver.bzl", "semver")
2122

2223
_PYTHON_VERSION_FLAG = Label("//python/config_settings:python_version")
2324
_PYTHON_VERSION_MAJOR_MINOR_FLAG = Label("//python/config_settings:python_version_major_minor")
2425

25-
def construct_config_settings(*, name, default_version, versions, minor_mapping): # buildifier: disable=function-docstring
26+
_DEBUG_ENV_MESSAGE_TEMPLATE = """\
27+
The current configuration rules_python config flags is:
28+
{flags}
29+
30+
If the value is missing, then the default value is being used, see documentation:
31+
{docs_url}/python/config_settings
32+
"""
33+
34+
def construct_config_settings(*, name, default_version, versions, minor_mapping, documented_flags): # buildifier: disable=function-docstring
2635
"""Create a 'python_version' config flag and construct all config settings used in rules_python.
2736
2837
This mainly includes the targets that are used in the toolchain and pip hub
@@ -33,6 +42,8 @@ def construct_config_settings(*, name, default_version, versions, minor_mapping)
3342
default_version: {type}`str` the default value for the `python_version` flag.
3443
versions: {type}`list[str]` A list of versions to build constraint settings for.
3544
minor_mapping: {type}`dict[str, str]` A mapping from `X.Y` to `X.Y.Z` python versions.
45+
documented_flags: {type}`list[str]` The labels of the documented settings
46+
that affect build configuration.
3647
"""
3748
_ = name # @unused
3849
_python_version_flag(
@@ -101,6 +112,25 @@ def construct_config_settings(*, name, default_version, versions, minor_mapping)
101112
visibility = ["//visibility:public"],
102113
)
103114

115+
_current_config(
116+
name = "current_config",
117+
build_setting_default = "",
118+
settings = documented_flags + [_PYTHON_VERSION_FLAG.name],
119+
visibility = ["//visibility:private"],
120+
)
121+
native.config_setting(
122+
name = "is_not_matching_current_config",
123+
# We use the rule above instead of @platforms//:incompatible so that the
124+
# printing of the current env always happens when the _current_config rule
125+
# is executed.
126+
#
127+
# NOTE: This should in practise only happen if there is a missing compatible
128+
# `whl_library` in the hub repo created by `pip.parse`.
129+
flag_values = {"current_config": "will-never-match"},
130+
# Only public so that PyPI hub repo can access it
131+
visibility = ["//visibility:public"],
132+
)
133+
104134
def _python_version_flag_impl(ctx):
105135
value = ctx.build_setting_value
106136
return [
@@ -122,7 +152,7 @@ _python_version_flag = rule(
122152
)
123153

124154
def _python_version_major_minor_flag_impl(ctx):
125-
input = ctx.attr._python_version_flag[config_common.FeatureFlagInfo].value
155+
input = _flag_value(ctx.attr._python_version_flag)
126156
if input:
127157
version = semver(input)
128158
value = "{}.{}".format(version.major, version.minor)
@@ -140,3 +170,42 @@ _python_version_major_minor_flag = rule(
140170
),
141171
},
142172
)
173+
174+
def _flag_value(s):
175+
if config_common.FeatureFlagInfo in s:
176+
return s[config_common.FeatureFlagInfo].value
177+
else:
178+
return s[BuildSettingInfo].value
179+
180+
def _print_current_config_impl(ctx):
181+
flags = "\n".join([
182+
"{}: \"{}\"".format(k, v)
183+
for k, v in sorted({
184+
str(setting.label): _flag_value(setting)
185+
for setting in ctx.attr.settings
186+
}.items())
187+
])
188+
189+
msg = ctx.attr._template.format(
190+
docs_url = "https://rules-python.readthedocs.io/en/latest/api/rules_python",
191+
flags = render.indent(flags).lstrip(),
192+
)
193+
if ctx.build_setting_value and ctx.build_setting_value != "fail":
194+
fail("Only 'fail' and empty build setting values are allowed for {}".format(
195+
str(ctx.label),
196+
))
197+
elif ctx.build_setting_value:
198+
fail(msg)
199+
else:
200+
print(msg) # buildifier: disable=print
201+
202+
return [config_common.FeatureFlagInfo(value = "")]
203+
204+
_current_config = rule(
205+
implementation = _print_current_config_impl,
206+
build_setting = config.string(flag = True),
207+
attrs = {
208+
"settings": attr.label_list(mandatory = True),
209+
"_template": attr.string(default = _DEBUG_ENV_MESSAGE_TEMPLATE),
210+
},
211+
)

python/private/pypi/pkg_aliases.bzl

+33-36
Original file line numberDiff line numberDiff line change
@@ -36,8 +36,6 @@ load(":whl_target_platforms.bzl", "whl_target_platforms")
3636
# it. It is more of an internal consistency check.
3737
_VERSION_NONE = (0, 0)
3838

39-
_CONFIG_SETTINGS_PKG = str(Label("//python/config_settings:BUILD.bazel")).partition(":")[0]
40-
4139
_NO_MATCH_ERROR_TEMPLATE = """\
4240
No matching wheel for current configuration's Python version.
4341
@@ -49,37 +47,18 @@ configuration settings:
4947
To determine the current configuration's Python version, run:
5048
`bazel config <config id>` (shown further below)
5149
52-
and look for one of:
53-
{settings_pkg}:python_version
54-
{settings_pkg}:pip_whl
55-
{settings_pkg}:pip_whl_glibc_version
56-
{settings_pkg}:pip_whl_muslc_version
57-
{settings_pkg}:pip_whl_osx_arch
58-
{settings_pkg}:pip_whl_osx_version
59-
{settings_pkg}:py_freethreaded
60-
{settings_pkg}:py_linux_libc
61-
62-
If the value is missing, then the default value is being used, see documentation:
63-
{docs_url}/python/config_settings"""
64-
65-
def _no_match_error(actual):
66-
if type(actual) != type({}):
67-
return None
68-
69-
if "//conditions:default" in actual:
70-
return None
71-
72-
return _NO_MATCH_ERROR_TEMPLATE.format(
73-
config_settings = render.indent(
74-
"\n".join(sorted([
75-
value
76-
for key in actual
77-
for value in (key if type(key) == "tuple" else [key])
78-
])),
79-
).lstrip(),
80-
settings_pkg = _CONFIG_SETTINGS_PKG,
81-
docs_url = "https://rules-python.readthedocs.io/en/latest/api/rules_python",
82-
)
50+
For the current configuration value see the debug message above that is
51+
printing the current flag values. If you can't see the message, then re-run the
52+
build to make it a failure instead by running the build with:
53+
--{current_flags}=fail
54+
55+
However, the command above will hide the `bazel config <config id>` message.
56+
"""
57+
58+
_LABEL_NONE = Label("//python:none")
59+
_LABEL_CURRENT_CONFIG = Label("//python/config_settings:current_config")
60+
_LABEL_CURRENT_CONFIG_NO_MATCH = Label("//python/config_settings:is_not_matching_current_config")
61+
_INCOMPATIBLE = "_no_matching_repository"
8362

8463
def pkg_aliases(
8564
*,
@@ -120,7 +99,26 @@ def pkg_aliases(
12099
}
121100

122101
actual = multiplatform_whl_aliases(aliases = actual, **kwargs)
123-
no_match_error = _no_match_error(actual)
102+
if type(actual) == type({}) and "//conditions:default" not in actual:
103+
native.alias(
104+
name = _INCOMPATIBLE,
105+
actual = select(
106+
{_LABEL_CURRENT_CONFIG_NO_MATCH: _LABEL_NONE},
107+
no_match_error = _NO_MATCH_ERROR_TEMPLATE.format(
108+
config_settings = render.indent(
109+
"\n".join(sorted([
110+
value
111+
for key in actual
112+
for value in (key if type(key) == "tuple" else [key])
113+
])),
114+
).lstrip(),
115+
current_flags = str(_LABEL_CURRENT_CONFIG),
116+
),
117+
),
118+
visibility = ["//visibility:private"],
119+
tags = ["manual"],
120+
)
121+
actual["//conditions:default"] = _INCOMPATIBLE
124122

125123
for name, target_name in target_names.items():
126124
if type(actual) == type(""):
@@ -134,10 +132,9 @@ def pkg_aliases(
134132
v: "@{repo}//:{target_name}".format(
135133
repo = repo,
136134
target_name = name,
137-
)
135+
) if repo != _INCOMPATIBLE else repo
138136
for v, repo in actual.items()
139137
},
140-
no_match_error = no_match_error,
141138
)
142139
else:
143140
fail("The `actual` arg must be a dictionary or a string")

tests/pypi/pkg_aliases/pkg_aliases_test.bzl

+41-25
Original file line numberDiff line numberDiff line change
@@ -56,12 +56,8 @@ def _test_config_setting_aliases(env):
5656
actual_no_match_error = []
5757

5858
def mock_select(value, no_match_error = None):
59-
actual_no_match_error.append(no_match_error)
60-
env.expect.that_str(no_match_error).contains("""\
61-
configuration settings:
62-
//:my_config_setting
63-
64-
""")
59+
if no_match_error and no_match_error not in actual_no_match_error:
60+
actual_no_match_error.append(no_match_error)
6561
return value
6662

6763
pkg_aliases(
@@ -71,7 +67,7 @@ configuration settings:
7167
},
7268
extra_aliases = ["my_special"],
7369
native = struct(
74-
alias = lambda name, actual: got.update({name: actual}),
70+
alias = lambda *, name, actual, visibility = None, tags = None: got.update({name: actual}),
7571
),
7672
select = mock_select,
7773
)
@@ -80,9 +76,22 @@ configuration settings:
8076
want = {
8177
"pkg": {
8278
"//:my_config_setting": "@bar_baz_repo//:pkg",
79+
"//conditions:default": "_no_matching_repository",
8380
},
81+
# This will be printing the current config values and will make sure we
82+
# have an error.
83+
"_no_matching_repository": {Label("//python/config_settings:is_not_matching_current_config"): Label("//python:none")},
8484
}
8585
env.expect.that_dict(got).contains_at_least(want)
86+
env.expect.that_collection(actual_no_match_error).has_size(1)
87+
env.expect.that_str(actual_no_match_error[0]).contains("""\
88+
configuration settings:
89+
//:my_config_setting
90+
91+
""")
92+
env.expect.that_str(actual_no_match_error[0]).contains(
93+
"//python/config_settings:current_config=fail",
94+
)
8695

8796
_tests.append(_test_config_setting_aliases)
8897

@@ -92,13 +101,8 @@ def _test_config_setting_aliases_many(env):
92101
actual_no_match_error = []
93102

94103
def mock_select(value, no_match_error = None):
95-
actual_no_match_error.append(no_match_error)
96-
env.expect.that_str(no_match_error).contains("""\
97-
configuration settings:
98-
//:another_config_setting
99-
//:my_config_setting
100-
//:third_config_setting
101-
""")
104+
if no_match_error and no_match_error not in actual_no_match_error:
105+
actual_no_match_error.append(no_match_error)
102106
return value
103107

104108
pkg_aliases(
@@ -112,7 +116,8 @@ configuration settings:
112116
},
113117
extra_aliases = ["my_special"],
114118
native = struct(
115-
alias = lambda name, actual: got.update({name: actual}),
119+
alias = lambda *, name, actual, visibility = None, tags = None: got.update({name: actual}),
120+
config_setting = lambda **_: None,
116121
),
117122
select = mock_select,
118123
)
@@ -125,9 +130,17 @@ configuration settings:
125130
"//:another_config_setting",
126131
): "@bar_baz_repo//:my_special",
127132
"//:third_config_setting": "@foo_repo//:my_special",
133+
"//conditions:default": "_no_matching_repository",
128134
},
129135
}
130136
env.expect.that_dict(got).contains_at_least(want)
137+
env.expect.that_collection(actual_no_match_error).has_size(1)
138+
env.expect.that_str(actual_no_match_error[0]).contains("""\
139+
configuration settings:
140+
//:another_config_setting
141+
//:my_config_setting
142+
//:third_config_setting
143+
""")
131144

132145
_tests.append(_test_config_setting_aliases_many)
133146

@@ -137,15 +150,8 @@ def _test_multiplatform_whl_aliases(env):
137150
actual_no_match_error = []
138151

139152
def mock_select(value, no_match_error = None):
140-
actual_no_match_error.append(no_match_error)
141-
env.expect.that_str(no_match_error).contains("""\
142-
configuration settings:
143-
//:my_config_setting
144-
//_config:is_cp3.9_linux_x86_64
145-
//_config:is_cp3.9_py3_none_any
146-
//_config:is_cp3.9_py3_none_any_linux_x86_64
147-
148-
""")
153+
if no_match_error and no_match_error not in actual_no_match_error:
154+
actual_no_match_error.append(no_match_error)
149155
return value
150156

151157
pkg_aliases(
@@ -168,7 +174,7 @@ configuration settings:
168174
},
169175
extra_aliases = [],
170176
native = struct(
171-
alias = lambda name, actual: got.update({name: actual}),
177+
alias = lambda *, name, actual, visibility = None, tags = None: got.update({name: actual}),
172178
),
173179
select = mock_select,
174180
glibc_versions = [],
@@ -183,9 +189,19 @@ configuration settings:
183189
"//_config:is_cp3.9_linux_x86_64": "@bzlmod_repo_for_a_particular_platform//:pkg",
184190
"//_config:is_cp3.9_py3_none_any": "@filename_repo//:pkg",
185191
"//_config:is_cp3.9_py3_none_any_linux_x86_64": "@filename_repo_for_platform//:pkg",
192+
"//conditions:default": "_no_matching_repository",
186193
},
187194
}
188195
env.expect.that_dict(got).contains_at_least(want)
196+
env.expect.that_collection(actual_no_match_error).has_size(1)
197+
env.expect.that_str(actual_no_match_error[0]).contains("""\
198+
configuration settings:
199+
//:my_config_setting
200+
//_config:is_cp3.9_linux_x86_64
201+
//_config:is_cp3.9_py3_none_any
202+
//_config:is_cp3.9_py3_none_any_linux_x86_64
203+
204+
""")
189205

190206
_tests.append(_test_multiplatform_whl_aliases)
191207

0 commit comments

Comments
 (0)