diff --git a/python/config_settings/BUILD.bazel b/python/config_settings/BUILD.bazel index 20bc50660a..08e02d4790 100644 --- a/python/config_settings/BUILD.bazel +++ b/python/config_settings/BUILD.bazel @@ -1,4 +1,6 @@ load("@bazel_skylib//rules:common_settings.bzl", "string_flag") +load("//python:versions.bzl", "MINOR_MAPPING", "TOOL_VERSIONS") +load("//python/private:config_settings.bzl", "construct_config_settings") load( "//python/private:flags.bzl", "BootstrapImplFlag", @@ -8,6 +10,7 @@ load( "PrecompileSourceRetentionFlag", "PycCollectionFlag", ) +load("//python/private:python_version_flag.bzl", "flag_values") load( "//python/private/pypi:flags.bzl", "UniversalWhlFlag", @@ -15,7 +18,11 @@ load( "WhlLibcFlag", "define_pypi_internal_flags", ) -load(":config_settings.bzl", "construct_config_settings") + +_VERSION_FLAG_VALUES = flag_values( + TOOL_VERSIONS.keys(), + MINOR_MAPPING, +) filegroup( name = "distribution", @@ -27,6 +34,7 @@ filegroup( construct_config_settings( name = "construct_config_settings", + version_flag_values = _VERSION_FLAG_VALUES, ) string_flag( diff --git a/python/private/BUILD.bazel b/python/private/BUILD.bazel index 7b913df2b3..30848faa3d 100644 --- a/python/private/BUILD.bazel +++ b/python/private/BUILD.bazel @@ -79,10 +79,21 @@ bzl_library( srcs = ["config_settings.bzl"], deps = [ "//python:versions_bzl", + ":bzlmod_enabled_bzl", + ":python_version_flag_bzl", "@bazel_skylib//lib:selects", ], ) +bzl_library( + name = "python_version_flag_bzl", + srcs = ["python_version_flag.bzl"], + deps = [ + "//python:versions_bzl", + "@bazel_skylib//rules:common_settings", + ], +) + bzl_library( name = "coverage_deps_bzl", srcs = ["coverage_deps.bzl"], diff --git a/python/private/config_settings.bzl b/python/private/config_settings.bzl index 99b8b94adf..80f1f22136 100644 --- a/python/private/config_settings.bzl +++ b/python/private/config_settings.bzl @@ -16,60 +16,11 @@ """ load("@bazel_skylib//lib:selects.bzl", "selects") -load("@bazel_skylib//rules:common_settings.bzl", "BuildSettingInfo") -load("//python:versions.bzl", "MINOR_MAPPING", "TOOL_VERSIONS") +load(":python_version_flag.bzl", "python_version_flag") _PYTHON_VERSION_FLAG = str(Label("//python/config_settings:python_version")) -def _ver_key(s): - major, _, s = s.partition(".") - minor, _, s = s.partition(".") - micro, _, s = s.partition(".") - return (int(major), int(minor), int(micro)) - -def _flag_values(python_versions): - """Construct a map of python_version to a list of toolchain values. - - This mapping maps the concept of a config setting to a list of compatible toolchain versions. - For using this in the code, the VERSION_FLAG_VALUES should be used instead. - - Args: - python_versions: list of strings; all X.Y.Z python versions - - Returns: - A `map[str, list[str]]`. Each key is a python_version flag value. Each value - is a list of the python_version flag values that should match when for the - `key`. For example: - ``` - "3.8" -> ["3.8", "3.8.1", "3.8.2", ..., "3.8.19"] # All 3.8 versions - "3.8.2" -> ["3.8.2"] # Only 3.8.2 - "3.8.19" -> ["3.8.19", "3.8"] # The latest version should also match 3.8 so - as when the `3.8` toolchain is used we just use the latest `3.8` toolchain. - this makes the `select("is_python_3.8.19")` work no matter how the user - specifies the latest python version to use. - ``` - """ - ret = {} - - for micro_version in sorted(python_versions, key = _ver_key): - minor_version, _, _ = micro_version.rpartition(".") - - # This matches the raw flag value, e.g. --//python/config_settings:python_version=3.8 - # It's private because matching the concept of e.g. "3.8" value is done - # using the `is_python_X.Y` config setting group, which is aware of the - # minor versions that could match instead. - ret.setdefault(minor_version, [minor_version]).append(micro_version) - - # Ensure that is_python_3.9.8 is matched if python_version is set - # to 3.9 if MINOR_MAPPING points to 3.9.8 - default_micro_version = MINOR_MAPPING[minor_version] - ret[micro_version] = [micro_version, minor_version] if default_micro_version == micro_version else [micro_version] - - return ret - -VERSION_FLAG_VALUES = _flag_values(TOOL_VERSIONS.keys()) - -def is_python_config_setting(name, *, python_version, reuse_conditions = None, **kwargs): +def is_python_config_setting(name, *, python_version, reuse_conditions = None, version_flag_values = None, **kwargs): """Create a config setting for matching 'python_version' configuration flag. This function is mainly intended for internal use within the `whl_library` and `pip_parse` @@ -94,15 +45,19 @@ def is_python_config_setting(name, *, python_version, reuse_conditions = None, * reuse config_setting targets instead of creating them from scratch. This is useful when using is_python_config_setting multiple times in the same package with the same `major.minor` python versions. + version_flag_values: A dict for using the version flag values. **kwargs: extra kwargs passed to the `config_setting`. """ if python_version not in name: fail("The name '{}' must have the python version '{}' in it".format(name, python_version)) - if python_version not in VERSION_FLAG_VALUES: - fail("The 'python_version' must be known to 'rules_python', choose from the values: {}".format(VERSION_FLAG_VALUES.keys())) + if python_version not in version_flag_values: + fail("The 'python_version' must be known to 'rules_python', got '{}', please choose from the values: {}".format( + python_version, + version_flag_values.keys(), + )) - python_versions = VERSION_FLAG_VALUES[python_version] + python_versions = version_flag_values[python_version] extra_flag_values = kwargs.pop("flag_values", {}) if _PYTHON_VERSION_FLAG in extra_flag_values: fail("Cannot set '{}' in the flag values".format(_PYTHON_VERSION_FLAG)) @@ -125,7 +80,7 @@ def is_python_config_setting(name, *, python_version, reuse_conditions = None, * } match_any = list(create_config_settings.keys()) for version, condition in reuse_conditions.items(): - if len(VERSION_FLAG_VALUES[version]) == 1: + if len(version_flag_values[version]) == 1: match_any.append(condition) continue @@ -161,35 +116,32 @@ def is_python_config_setting(name, *, python_version, reuse_conditions = None, * visibility = kwargs.get("visibility", []), ) -def construct_config_settings(name = None): # buildifier: disable=function-docstring +def construct_config_settings(name = None, version_flag_values = None): # 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 repositories that only match on the 'python_version' flag values. Args: - name(str): A dummy name value that is no-op for now. + name: {type}`str` A dummy name value that is no-op for now. + version_flag_values: {type}`dict[str, str]` the version flag values """ - _python_version_flag( + if not version_flag_values: + native.alias( + name = "python_version", + actual = "@pythons_hub//:python_version", + visibility = ["//visibility:public"], + ) + return + + python_version_flag( name = "python_version", - # TODO: The default here should somehow match the MODULE config. Until - # then, use the empty string to indicate an unknown version. This - # also prevents version-unaware targets from inadvertently matching - # a select condition when they shouldn't. build_setting_default = "", - values = [""] + VERSION_FLAG_VALUES.keys(), + values = [""] + version_flag_values.keys(), visibility = ["//visibility:public"], ) - native.config_setting( - name = "is_python_version_unset", - flag_values = { - Label("//python/config_settings:python_version"): "", - }, - visibility = ["//visibility:public"], - ) - - for version, matching_versions in VERSION_FLAG_VALUES.items(): + for version, matching_versions in version_flag_values.items(): is_python_config_setting( name = "is_python_{}".format(version), python_version = version, @@ -198,37 +150,14 @@ def construct_config_settings(name = None): # buildifier: disable=function-docs for v in matching_versions if v != version }, + version_flag_values = version_flag_values, visibility = ["//visibility:public"], ) -def _python_version_flag_impl(ctx): - value = ctx.build_setting_value - if value not in ctx.attr.values: - fail(( - "Invalid --python_version value: {actual}\nAllowed values {allowed}" - ).format( - actual = value, - allowed = ", ".join(sorted(ctx.attr.values)), - )) - - return [ - # BuildSettingInfo is the original provider returned, so continue to - # return it for compatibility - BuildSettingInfo(value = value), - # FeatureFlagInfo is returned so that config_setting respects the value - # as returned by this rule instead of as originally seen on the command - # line. - # It is also for Google compatibility, which expects the FeatureFlagInfo - # provider. - config_common.FeatureFlagInfo(value = value), - ] - -_python_version_flag = rule( - implementation = _python_version_flag_impl, - build_setting = config.string(flag = True), - attrs = { - "values": attr.string_list( - doc = "Allowed values.", - ), - }, -) + native.config_setting( + name = "is_python_version_unset", + flag_values = { + Label("//python/config_settings:python_version"): "", + }, + visibility = ["//visibility:public"], + ) diff --git a/python/private/pypi/config_settings.bzl b/python/private/pypi/config_settings.bzl index 974121782f..1719c0a084 100644 --- a/python/private/pypi/config_settings.bzl +++ b/python/private/pypi/config_settings.bzl @@ -111,7 +111,7 @@ def config_settings( is_python = "is_python_{}".format(python_version or "version_unset") native.alias( name = is_python, - actual = Label("//python/config_settings:" + is_python), + actual = Label("@pythons_hub//:" + is_python), visibility = visibility, ) diff --git a/python/private/python_version_flag.bzl b/python/private/python_version_flag.bzl new file mode 100644 index 0000000000..aa68553889 --- /dev/null +++ b/python/private/python_version_flag.bzl @@ -0,0 +1,97 @@ +# Copyright 2024 The Bazel Authors. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""python_version related code. +""" + +load("@bazel_skylib//rules:common_settings.bzl", "BuildSettingInfo") + +def _ver_key(s): + major, _, s = s.partition(".") + minor, _, s = s.partition(".") + micro, _, s = s.partition(".") + return (int(major), int(minor), int(micro)) + +def flag_values(python_versions, minor_mapping): + """Construct a map of python_version to a list of toolchain values. + + This mapping maps the concept of a config setting to a list of compatible toolchain versions. + For using this in the code, the VERSION_FLAG_VALUES should be used instead. + + Args: + python_versions: list of strings; all X.Y.Z python versions. + minor_mapping: minor version to full version mapping. + + Returns: + A `map[str, list[str]]`. Each key is a python_version flag value. Each value + is a list of the python_version flag values that should match when for the + `key`. For example: + ``` + "3.8" -> ["3.8", "3.8.1", "3.8.2", ..., "3.8.19"] # All 3.8 versions + "3.8.2" -> ["3.8.2"] # Only 3.8.2 + "3.8.19" -> ["3.8.19", "3.8"] # The latest version should also match 3.8 so + as when the `3.8` toolchain is used we just use the latest `3.8` toolchain. + this makes the `select("is_python_3.8.19")` work no matter how the user + specifies the latest python version to use. + ``` + """ + ret = {} + + for micro_version in sorted(python_versions, key = _ver_key): + minor_version, _, _ = micro_version.rpartition(".") + + # This matches the raw flag value, e.g. --//python/config_settings:python_version=3.8 + # It's private because matching the concept of e.g. "3.8" value is done + # using the `is_python_X.Y` config setting group, which is aware of the + # minor versions that could match instead. + ret.setdefault(minor_version, [minor_version]).append(micro_version) + + # Ensure that is_python_3.9.8 is matched if python_version is set + # to 3.9 if MINOR_MAPPING points to 3.9.8 + default_micro_version = minor_mapping[minor_version] + ret[micro_version] = [micro_version, minor_version] if default_micro_version == micro_version else [micro_version] + + return ret + +def _python_version_flag_impl(ctx): + value = ctx.build_setting_value + if value not in ctx.attr.values: + fail(( + "Invalid --python_version value: {actual}\nAllowed values {allowed}" + ).format( + actual = value, + allowed = ", ".join(sorted(ctx.attr.values)), + )) + + return [ + # BuildSettingInfo is the original provider returned, so continue to + # return it for compatibility + BuildSettingInfo(value = value), + # FeatureFlagInfo is returned so that config_setting respects the value + # as returned by this rule instead of as originally seen on the command + # line. + # It is also for Google compatibility, which expects the FeatureFlagInfo + # provider. + config_common.FeatureFlagInfo(value = value), + ] + +python_version_flag = rule( + implementation = _python_version_flag_impl, + build_setting = config.string(flag = True), + attrs = { + "values": attr.string_list( + doc = "Allowed values.", + ), + }, +) diff --git a/python/private/pythons_hub.bzl b/python/private/pythons_hub.bzl index 7a8c874ed8..9f139aacaa 100644 --- a/python/private/pythons_hub.bzl +++ b/python/private/pythons_hub.bzl @@ -14,11 +14,11 @@ "Repo rule used by bzlmod extension to create a repo that has a map of Python interpreters and their labels" -load("//python/private:full_version.bzl", "full_version") -load( - "//python/private:toolchains_repo.bzl", - "python_toolchain_build_file_content", -) +load("//python:versions.bzl", "MINOR_MAPPING", "TOOL_VERSIONS") +load(":full_version.bzl", "full_version") +load(":python_version_flag.bzl", "flag_values") +load(":text_util.bzl", "render") +load(":toolchains_repo.bzl", "python_toolchain_build_file_content") def _have_same_length(*lists): if not lists: @@ -28,6 +28,13 @@ def _have_same_length(*lists): _HUB_BUILD_FILE_TEMPLATE = """\ load("@bazel_skylib//:bzl_library.bzl", "bzl_library") load("@@{rules_python}//python/private:py_toolchain_suite.bzl", "py_toolchain_suite") +load("@@{rules_python}//python/private:config_settings.bzl", "construct_config_settings") +load("//:version_flag_values.bzl", "VERSION_FLAG_VALUES") + +construct_config_settings( + name = "construct_config_settings", + version_flag_values = VERSION_FLAG_VALUES, +) bzl_library( name = "interpreters_bzl", @@ -35,6 +42,12 @@ bzl_library( visibility = ["@rules_python//:__subpackages__"], ) +bzl_library( + name = "version_flag_values_bzl", + srcs = ["version_flag_values.bzl"], + visibility = ["@rules_python//:__subpackages__"], +) + {toolchains} """ @@ -43,7 +56,8 @@ def _hub_build_file_content( python_versions, set_python_version_constraints, user_repository_names, - workspace_location): + workspace_location, + default_python_version): """This macro iterates over each of the lists and returns the toolchain content. python_toolchain_build_file_content is called to generate each of the toolchain @@ -70,6 +84,7 @@ def _hub_build_file_content( return _HUB_BUILD_FILE_TEMPLATE.format( toolchains = toolchains, rules_python = workspace_location.workspace_name, + default_python_version = default_python_version, ) _interpreters_bzl_template = """ @@ -94,6 +109,7 @@ def _hub_repo_impl(rctx): rctx.attr.toolchain_set_python_version_constraints, rctx.attr.toolchain_user_repository_names, rctx.attr._rules_python_workspace, + rctx.attr.default_python_version, ), executable = False, ) @@ -105,6 +121,12 @@ def _hub_repo_impl(rctx): for name in rctx.attr.toolchain_user_repository_names ]) + rctx.file( + "version_flag_values.bzl", + "VERSION_FLAG_VALUES = " + render.dict(flag_values(TOOL_VERSIONS.keys(), MINOR_MAPPING)), + executable = False, + ) + rctx.file( "interpreters.bzl", _interpreters_bzl_template.format(