diff --git a/python/private/config_settings.bzl b/python/private/config_settings.bzl index 3089b9c6cf..91fbbba8cb 100644 --- a/python/private/config_settings.bzl +++ b/python/private/config_settings.bzl @@ -35,7 +35,14 @@ If the value is missing, then the default value is being used, see documentation # access it, but it's not intended for general public usage. _NOT_ACTUALLY_PUBLIC = ["//visibility:public"] -def construct_config_settings(*, name, default_version, versions, minor_mapping, documented_flags): # buildifier: disable=function-docstring +def construct_config_settings( + *, + name, + default_version, + versions, + minor_mapping, + compat_lowest_version = "3.8", + 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 @@ -46,6 +53,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. + compat_lowest_version: {type}`str` The version that we should use as the lowest available + version for `is_python_3.X` flags. documented_flags: {type}`list[str]` The labels of the documented settings that affect build configuration. """ @@ -69,21 +78,21 @@ def construct_config_settings(*, name, default_version, versions, minor_mapping, ) _reverse_minor_mapping = {full: minor for minor, full in minor_mapping.items()} - for version in versions: - minor_version = _reverse_minor_mapping.get(version) + for ver in versions: + minor_version = _reverse_minor_mapping.get(ver) if not minor_version: native.config_setting( - name = "is_python_{}".format(version), - flag_values = {":python_version": version}, + name = "is_python_{}".format(ver), + flag_values = {":python_version": ver}, visibility = ["//visibility:public"], ) continue # Also need to match the minor version when using - name = "is_python_{}".format(version) + name = "is_python_{}".format(ver) native.config_setting( name = "_" + name, - flag_values = {":python_version": version}, + flag_values = {":python_version": ver}, visibility = ["//visibility:public"], ) @@ -94,7 +103,7 @@ def construct_config_settings(*, name, default_version, versions, minor_mapping, selects.config_setting_group( name = "_{}_group".format(name), match_any = [ - ":_is_python_{}".format(version), + ":_is_python_{}".format(ver), ":is_python_{}".format(minor_version), ], visibility = ["//visibility:private"], @@ -109,13 +118,28 @@ def construct_config_settings(*, name, default_version, versions, minor_mapping, # 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. + first_minor = None for minor in minor_mapping.keys(): + ver = version.parse(minor) + if first_minor == None or version.is_lt(ver, first_minor): + first_minor = ver + native.config_setting( name = "is_python_{}".format(minor), flag_values = {_PYTHON_VERSION_MAJOR_MINOR_FLAG: minor}, visibility = ["//visibility:public"], ) + # This is a compatibility layer to ensure that `select` statements don't break out right + # when the toolchains for EOL minor versions are no longer registered. + compat_lowest_version = version.parse(compat_lowest_version) + for minor in range(compat_lowest_version.release[-1], first_minor.release[-1]): + native.alias( + name = "is_python_3.{}".format(minor), + actual = "@platforms//:incompatible", + visibility = ["//visibility:public"], + ) + _current_config( name = "current_config", build_setting_default = "", diff --git a/python/private/full_version.bzl b/python/private/full_version.bzl index 0292d6c77d..82ac12e11e 100644 --- a/python/private/full_version.bzl +++ b/python/private/full_version.bzl @@ -14,12 +14,13 @@ """A small helper to ensure that we are working with full versions.""" -def full_version(*, version, minor_mapping): +def full_version(*, version, minor_mapping, err = True): """Return a full version. Args: version: {type}`str` the version in `X.Y` or `X.Y.Z` format. minor_mapping: {type}`dict[str, str]` mapping between `X.Y` to `X.Y.Z` format. + err: {type}`bool` whether to fail on error or return `None` instead. Returns: a full version given the version string. If the string is already a @@ -31,6 +32,8 @@ def full_version(*, version, minor_mapping): parts = version.split(".") if len(parts) == 3: return version + elif not err: + return None elif len(parts) == 2: fail( "Unknown Python version '{}', available values are: {}".format( diff --git a/python/private/pypi/hub_builder.bzl b/python/private/pypi/hub_builder.bzl index 7cf60ff85f..4abd141f27 100644 --- a/python/private/pypi/hub_builder.bzl +++ b/python/private/pypi/hub_builder.bzl @@ -114,9 +114,29 @@ def _pip_parse(self, module_ctx, pip_attr): version = python_version, )) - self._platforms[python_version] = _platforms( - python_version = python_version, + full_python_version = full_version( + version = python_version, minor_mapping = self._minor_mapping, + err = False, + ) + if not full_python_version: + # NOTE @aignas 2025-11-18: If the python version is not present in our + # minor_mapping, then we will not register any packages and then the + # select in the hub repository will fail, which will prompt the user to + # configure the toolchain correctly and move forward. + self._logger.info(lambda: ( + "Ignoring pip python version '{version}' for hub " + + "'{hub}' in module '{module}' because there is no registered " + + "toolchain for it." + ).format( + hub = self.name, + module = self.module_name, + version = python_version, + )) + return + + self._platforms[python_version] = _platforms( + python_version = full_python_version, config = self._config, ) _set_get_index_urls(self, pip_attr) @@ -280,13 +300,10 @@ def _detect_interpreter(self, pip_attr): path = pip_attr.python_interpreter, ) -def _platforms(*, python_version, minor_mapping, config): +def _platforms(*, python_version, config): platforms = {} python_version = version.parse( - full_version( - version = python_version, - minor_mapping = minor_mapping, - ), + python_version, strict = True, ) diff --git a/python/private/python.bzl b/python/private/python.bzl index a1fe80e0ce..124aea7d63 100644 --- a/python/private/python.bzl +++ b/python/private/python.bzl @@ -268,7 +268,12 @@ def _python_impl(module_ctx): full_python_version = full_version( version = toolchain_info.python_version, minor_mapping = py.config.minor_mapping, + err = False, ) + if not full_python_version: + logger.info(lambda: "The actual toolchain for python_version '{}' has not been registered, but was requested, please configure a toolchain to be actually downloaded and setup".format(toolchain_info.python_version)) + continue + kwargs = { "python_version": full_python_version, "register_coverage_tool": toolchain_info.register_coverage_tool, diff --git a/python/versions.bzl b/python/versions.bzl index 7e1b36b207..842fb39658 100644 --- a/python/versions.bzl +++ b/python/versions.bzl @@ -54,17 +54,6 @@ DEFAULT_RELEASE_BASE_URL = "https://github.com/astral-sh/python-build-standalone # # buildifier: disable=unsorted-dict-items TOOL_VERSIONS = { - "3.8.20": { - "url": "20241002/cpython-{python_version}+20241002-{platform}-{build}.tar.gz", - "sha256": { - "aarch64-apple-darwin": "2ddfc04bdb3e240f30fb782fa1deec6323799d0e857e0b63fa299218658fd3d4", - "aarch64-unknown-linux-gnu": "9d8798f9e79e0fc0f36fcb95bfa28a1023407d51a8ea5944b4da711f1f75f1ed", - "x86_64-apple-darwin": "68d060cd373255d2ca5b8b3441363d5aa7cc45b0c11bbccf52b1717c2b5aa8bb", - "x86_64-pc-windows-msvc": "41b6709fec9c56419b7de1940d1f87fa62045aff81734480672dcb807eedc47e", - "x86_64-unknown-linux-gnu": "285e141c36f88b2e9357654c5f77d1f8fb29cc25132698fe35bb30d787f38e87", - }, - "strip_prefix": "python", - }, "3.9.25": { "url": "20251031/cpython-{python_version}+20251031-{platform}-{build}.tar.gz", "sha256": { @@ -872,7 +861,6 @@ TOOL_VERSIONS = { # buildifier: disable=unsorted-dict-items MINOR_MAPPING = { - "3.8": "3.8.20", "3.9": "3.9.25", "3.10": "3.10.19", "3.11": "3.11.14", diff --git a/tests/python/python_tests.bzl b/tests/python/python_tests.bzl index f2e87274f8..ff02cc859e 100644 --- a/tests/python/python_tests.bzl +++ b/tests/python/python_tests.bzl @@ -707,6 +707,77 @@ def _test_register_all_versions(env): _tests.append(_test_register_all_versions) +def _test_ignore_unsupported_versions(env): + py = parse_modules( + module_ctx = _mock_mctx( + _mod( + name = "my_module", + is_root = True, + toolchain = [ + _toolchain("3.11"), + _toolchain("3.12"), + _toolchain("3.13", is_default = True), + ], + single_version_override = [ + _single_version_override( + python_version = "3.13.0", + sha256 = { + "aarch64-unknown-linux-gnu": "deadbeef", + }, + urls = ["example.org"], + ), + ], + single_version_platform_override = [ + _single_version_platform_override( + sha256 = "deadb00f", + urls = ["something.org"], + platform = "aarch64-unknown-linux-gnu", + python_version = "3.13.99", + ), + ], + override = [ + _override( + base_url = "", + available_python_versions = ["3.12.4", "3.13.0", "3.13.1"], + minor_mapping = { + "3.12": "3.12.4", + "3.13": "3.13.1", + }, + ), + ], + ), + ), + logger = repo_utils.logger(verbosity_level = 0, name = "python"), + ) + + env.expect.that_str(py.default_python_version).equals("3.13") + env.expect.that_collection(py.config.default["tool_versions"].keys()).contains_exactly([ + "3.12.4", + "3.13.0", + "3.13.1", + ]) + env.expect.that_dict(py.config.minor_mapping).contains_exactly({ + # The mapping is calculated automatically + "3.12": "3.12.4", + "3.13": "3.13.1", + }) + env.expect.that_collection(py.toolchains).contains_exactly([ + struct( + name = name, + python_version = version, + register_coverage_tool = False, + ) + for name, version in { + # NOTE: that '3.11' wont be actually registered and present in the + # `tool_versions` above. + "python_3_11": "3.11", + "python_3_12": "3.12", + "python_3_13": "3.13", + }.items() + ]) + +_tests.append(_test_ignore_unsupported_versions) + def _test_add_patches(env): py = parse_modules( module_ctx = _mock_mctx(