diff --git a/CHANGELOG.md b/CHANGELOG.md index 9976d2027c..9a3436487e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -70,6 +70,9 @@ Unreleased changes template. * (pypi) Using {bzl:obj}`pip_parse.experimental_requirement_cycles` and {bzl:obj}`pip_parse.use_hub_alias_dependencies` together now works when using WORKSPACE files. +* (pypi) The error messages when the wheel distributions do not match anything + are now printing more details and include the currently active flag + values. Fixes [#2466](https://github.com/bazelbuild/rules_python/issues/2466). * (py_proto_library) Fix import paths in Bazel 8. [pep-695]: https://peps.python.org/pep-0695/ diff --git a/docs/api/rules_python/python/config_settings/index.md b/docs/api/rules_python/python/config_settings/index.md index ef829bab76..793f6e08fd 100644 --- a/docs/api/rules_python/python/config_settings/index.md +++ b/docs/api/rules_python/python/config_settings/index.md @@ -240,3 +240,21 @@ instead. ::: :::: + +::::{bzl:flag} current_config +Fail the build if the current build configuration does not match the +{obj}`pip.parse` defined wheels. + +Values: +* `fail`: Will fail in the build action ensuring that we get the error + message no matter the action cache. +* ``: (empty string) The default value, that will just print a warning. + +:::{seealso} +{obj}`pip.parse` +::: + +:::{versionadded} 1.1.0 +::: + +:::: diff --git a/python/config_settings/BUILD.bazel b/python/config_settings/BUILD.bazel index 5455f5aef7..fcebcd76dc 100644 --- a/python/config_settings/BUILD.bazel +++ b/python/config_settings/BUILD.bazel @@ -29,6 +29,15 @@ filegroup( construct_config_settings( name = "construct_config_settings", default_version = DEFAULT_PYTHON_VERSION, + documented_flags = [ + ":pip_whl", + ":pip_whl_glibc_version", + ":pip_whl_muslc_version", + ":pip_whl_osx_arch", + ":pip_whl_osx_version", + ":py_freethreaded", + ":py_linux_libc", + ], minor_mapping = MINOR_MAPPING, versions = PYTHON_VERSIONS, ) diff --git a/python/private/config_settings.bzl b/python/private/config_settings.bzl index 10b4d686a7..e5f9d865d1 100644 --- a/python/private/config_settings.bzl +++ b/python/private/config_settings.bzl @@ -17,12 +17,21 @@ load("@bazel_skylib//lib:selects.bzl", "selects") load("@bazel_skylib//rules:common_settings.bzl", "BuildSettingInfo") +load("//python/private:text_util.bzl", "render") load(":semver.bzl", "semver") _PYTHON_VERSION_FLAG = Label("//python/config_settings:python_version") _PYTHON_VERSION_MAJOR_MINOR_FLAG = Label("//python/config_settings:python_version_major_minor") -def construct_config_settings(*, name, default_version, versions, minor_mapping): # buildifier: disable=function-docstring +_DEBUG_ENV_MESSAGE_TEMPLATE = """\ +The current configuration rules_python config flags is: + {flags} + +If the value is missing, then the default value is being used, see documentation: +{docs_url}/python/config_settings +""" + +def construct_config_settings(*, name, default_version, versions, minor_mapping, documented_flags): # buildifier: disable=function-docstring """Create a 'python_version' config flag and construct all config settings used in rules_python. 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) default_version: {type}`str` the default value for the `python_version` flag. versions: {type}`list[str]` A list of versions to build constraint settings for. minor_mapping: {type}`dict[str, str]` A mapping from `X.Y` to `X.Y.Z` python versions. + documented_flags: {type}`list[str]` The labels of the documented settings + that affect build configuration. """ _ = name # @unused _python_version_flag( @@ -101,6 +112,25 @@ def construct_config_settings(*, name, default_version, versions, minor_mapping) visibility = ["//visibility:public"], ) + _current_config( + name = "current_config", + build_setting_default = "", + settings = documented_flags + [_PYTHON_VERSION_FLAG.name], + visibility = ["//visibility:private"], + ) + native.config_setting( + name = "is_not_matching_current_config", + # We use the rule above instead of @platforms//:incompatible so that the + # printing of the current env always happens when the _current_config rule + # is executed. + # + # NOTE: This should in practise only happen if there is a missing compatible + # `whl_library` in the hub repo created by `pip.parse`. + flag_values = {"current_config": "will-never-match"}, + # Only public so that PyPI hub repo can access it + visibility = ["//visibility:public"], + ) + def _python_version_flag_impl(ctx): value = ctx.build_setting_value return [ @@ -122,7 +152,7 @@ _python_version_flag = rule( ) def _python_version_major_minor_flag_impl(ctx): - input = ctx.attr._python_version_flag[config_common.FeatureFlagInfo].value + input = _flag_value(ctx.attr._python_version_flag) if input: version = semver(input) value = "{}.{}".format(version.major, version.minor) @@ -140,3 +170,42 @@ _python_version_major_minor_flag = rule( ), }, ) + +def _flag_value(s): + if config_common.FeatureFlagInfo in s: + return s[config_common.FeatureFlagInfo].value + else: + return s[BuildSettingInfo].value + +def _print_current_config_impl(ctx): + flags = "\n".join([ + "{}: \"{}\"".format(k, v) + for k, v in sorted({ + str(setting.label): _flag_value(setting) + for setting in ctx.attr.settings + }.items()) + ]) + + msg = ctx.attr._template.format( + docs_url = "https://rules-python.readthedocs.io/en/latest/api/rules_python", + flags = render.indent(flags).lstrip(), + ) + if ctx.build_setting_value and ctx.build_setting_value != "fail": + fail("Only 'fail' and empty build setting values are allowed for {}".format( + str(ctx.label), + )) + elif ctx.build_setting_value: + fail(msg) + else: + print(msg) # buildifier: disable=print + + return [config_common.FeatureFlagInfo(value = "")] + +_current_config = rule( + implementation = _print_current_config_impl, + build_setting = config.string(flag = True), + attrs = { + "settings": attr.label_list(mandatory = True), + "_template": attr.string(default = _DEBUG_ENV_MESSAGE_TEMPLATE), + }, +) diff --git a/python/private/pypi/pkg_aliases.bzl b/python/private/pypi/pkg_aliases.bzl index a6872fdce9..980921b474 100644 --- a/python/private/pypi/pkg_aliases.bzl +++ b/python/private/pypi/pkg_aliases.bzl @@ -36,8 +36,6 @@ load(":whl_target_platforms.bzl", "whl_target_platforms") # it. It is more of an internal consistency check. _VERSION_NONE = (0, 0) -_CONFIG_SETTINGS_PKG = str(Label("//python/config_settings:BUILD.bazel")).partition(":")[0] - _NO_MATCH_ERROR_TEMPLATE = """\ No matching wheel for current configuration's Python version. @@ -49,37 +47,18 @@ configuration settings: To determine the current configuration's Python version, run: `bazel config ` (shown further below) -and look for one of: - {settings_pkg}:python_version - {settings_pkg}:pip_whl - {settings_pkg}:pip_whl_glibc_version - {settings_pkg}:pip_whl_muslc_version - {settings_pkg}:pip_whl_osx_arch - {settings_pkg}:pip_whl_osx_version - {settings_pkg}:py_freethreaded - {settings_pkg}:py_linux_libc - -If the value is missing, then the default value is being used, see documentation: -{docs_url}/python/config_settings""" - -def _no_match_error(actual): - if type(actual) != type({}): - return None - - if "//conditions:default" in actual: - return None - - return _NO_MATCH_ERROR_TEMPLATE.format( - config_settings = render.indent( - "\n".join(sorted([ - value - for key in actual - for value in (key if type(key) == "tuple" else [key]) - ])), - ).lstrip(), - settings_pkg = _CONFIG_SETTINGS_PKG, - docs_url = "https://rules-python.readthedocs.io/en/latest/api/rules_python", - ) +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: + --{current_flags}=fail + +However, the command above will hide the `bazel config ` message. +""" + +_LABEL_NONE = Label("//python:none") +_LABEL_CURRENT_CONFIG = Label("//python/config_settings:current_config") +_LABEL_CURRENT_CONFIG_NO_MATCH = Label("//python/config_settings:is_not_matching_current_config") +_INCOMPATIBLE = "_no_matching_repository" def pkg_aliases( *, @@ -120,7 +99,26 @@ def pkg_aliases( } actual = multiplatform_whl_aliases(aliases = actual, **kwargs) - no_match_error = _no_match_error(actual) + if type(actual) == type({}) and "//conditions:default" not in actual: + native.alias( + name = _INCOMPATIBLE, + actual = select( + {_LABEL_CURRENT_CONFIG_NO_MATCH: _LABEL_NONE}, + no_match_error = _NO_MATCH_ERROR_TEMPLATE.format( + config_settings = render.indent( + "\n".join(sorted([ + value + for key in actual + for value in (key if type(key) == "tuple" else [key]) + ])), + ).lstrip(), + current_flags = str(_LABEL_CURRENT_CONFIG), + ), + ), + visibility = ["//visibility:private"], + tags = ["manual"], + ) + actual["//conditions:default"] = _INCOMPATIBLE for name, target_name in target_names.items(): if type(actual) == type(""): @@ -134,10 +132,9 @@ def pkg_aliases( v: "@{repo}//:{target_name}".format( repo = repo, target_name = name, - ) + ) if repo != _INCOMPATIBLE else repo for v, repo in actual.items() }, - no_match_error = no_match_error, ) else: fail("The `actual` arg must be a dictionary or a string") diff --git a/tests/pypi/pkg_aliases/pkg_aliases_test.bzl b/tests/pypi/pkg_aliases/pkg_aliases_test.bzl index 23a0f01db9..f13b62f13d 100644 --- a/tests/pypi/pkg_aliases/pkg_aliases_test.bzl +++ b/tests/pypi/pkg_aliases/pkg_aliases_test.bzl @@ -56,12 +56,8 @@ def _test_config_setting_aliases(env): actual_no_match_error = [] def mock_select(value, no_match_error = None): - actual_no_match_error.append(no_match_error) - env.expect.that_str(no_match_error).contains("""\ -configuration settings: - //:my_config_setting - -""") + if no_match_error and no_match_error not in actual_no_match_error: + actual_no_match_error.append(no_match_error) return value pkg_aliases( @@ -71,7 +67,7 @@ configuration settings: }, extra_aliases = ["my_special"], native = struct( - alias = lambda name, actual: got.update({name: actual}), + alias = lambda *, name, actual, visibility = None, tags = None: got.update({name: actual}), ), select = mock_select, ) @@ -80,9 +76,22 @@ configuration settings: want = { "pkg": { "//:my_config_setting": "@bar_baz_repo//:pkg", + "//conditions:default": "_no_matching_repository", }, + # This will be printing the current config values and will make sure we + # have an error. + "_no_matching_repository": {Label("//python/config_settings:is_not_matching_current_config"): Label("//python:none")}, } env.expect.that_dict(got).contains_at_least(want) + env.expect.that_collection(actual_no_match_error).has_size(1) + env.expect.that_str(actual_no_match_error[0]).contains("""\ +configuration settings: + //:my_config_setting + +""") + env.expect.that_str(actual_no_match_error[0]).contains( + "//python/config_settings:current_config=fail", + ) _tests.append(_test_config_setting_aliases) @@ -92,13 +101,8 @@ def _test_config_setting_aliases_many(env): actual_no_match_error = [] def mock_select(value, no_match_error = None): - actual_no_match_error.append(no_match_error) - env.expect.that_str(no_match_error).contains("""\ -configuration settings: - //:another_config_setting - //:my_config_setting - //:third_config_setting -""") + if no_match_error and no_match_error not in actual_no_match_error: + actual_no_match_error.append(no_match_error) return value pkg_aliases( @@ -112,7 +116,8 @@ configuration settings: }, extra_aliases = ["my_special"], native = struct( - alias = lambda name, actual: got.update({name: actual}), + alias = lambda *, name, actual, visibility = None, tags = None: got.update({name: actual}), + config_setting = lambda **_: None, ), select = mock_select, ) @@ -125,9 +130,17 @@ configuration settings: "//:another_config_setting", ): "@bar_baz_repo//:my_special", "//:third_config_setting": "@foo_repo//:my_special", + "//conditions:default": "_no_matching_repository", }, } env.expect.that_dict(got).contains_at_least(want) + env.expect.that_collection(actual_no_match_error).has_size(1) + env.expect.that_str(actual_no_match_error[0]).contains("""\ +configuration settings: + //:another_config_setting + //:my_config_setting + //:third_config_setting +""") _tests.append(_test_config_setting_aliases_many) @@ -137,15 +150,8 @@ def _test_multiplatform_whl_aliases(env): actual_no_match_error = [] def mock_select(value, no_match_error = None): - actual_no_match_error.append(no_match_error) - env.expect.that_str(no_match_error).contains("""\ -configuration settings: - //:my_config_setting - //_config:is_cp3.9_linux_x86_64 - //_config:is_cp3.9_py3_none_any - //_config:is_cp3.9_py3_none_any_linux_x86_64 - -""") + if no_match_error and no_match_error not in actual_no_match_error: + actual_no_match_error.append(no_match_error) return value pkg_aliases( @@ -168,7 +174,7 @@ configuration settings: }, extra_aliases = [], native = struct( - alias = lambda name, actual: got.update({name: actual}), + alias = lambda *, name, actual, visibility = None, tags = None: got.update({name: actual}), ), select = mock_select, glibc_versions = [], @@ -183,9 +189,19 @@ configuration settings: "//_config:is_cp3.9_linux_x86_64": "@bzlmod_repo_for_a_particular_platform//:pkg", "//_config:is_cp3.9_py3_none_any": "@filename_repo//:pkg", "//_config:is_cp3.9_py3_none_any_linux_x86_64": "@filename_repo_for_platform//:pkg", + "//conditions:default": "_no_matching_repository", }, } env.expect.that_dict(got).contains_at_least(want) + env.expect.that_collection(actual_no_match_error).has_size(1) + env.expect.that_str(actual_no_match_error[0]).contains("""\ +configuration settings: + //:my_config_setting + //_config:is_cp3.9_linux_x86_64 + //_config:is_cp3.9_py3_none_any + //_config:is_cp3.9_py3_none_any_linux_x86_64 + +""") _tests.append(_test_multiplatform_whl_aliases)