Skip to content
40 changes: 32 additions & 8 deletions python/private/config_settings.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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.
"""
Expand All @@ -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"],
)

Expand All @@ -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"],
Expand All @@ -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 = "",
Expand Down
5 changes: 4 additions & 1 deletion python/private/full_version.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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(
Expand Down
31 changes: 24 additions & 7 deletions python/private/pypi/hub_builder.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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,
)

Expand Down
5 changes: 5 additions & 0 deletions python/private/python.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
12 changes: 0 additions & 12 deletions python/versions.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -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": {
Expand Down Expand Up @@ -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",
Expand Down
71 changes: 71 additions & 0 deletions tests/python/python_tests.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down