Skip to content

feat: freethreaded support for the builder API #3063

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 6 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 30 additions & 6 deletions MODULE.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -70,11 +70,15 @@ pip = use_extension("//python/extensions:pip.bzl", "pip")
config_settings = [
"@platforms//cpu:{}".format(cpu),
"@platforms//os:linux",
"//python/config_settings:_is_py_freethreaded_{}".format(
"yes" if freethreaded else "no",
),
],
env = {"platform_version": "0"},
marker = "python_version >= '3.13'" if freethreaded else "",
os_name = "linux",
platform = "linux_{}".format(cpu),
whl_abi_tags = [
platform = "linux_{}{}".format(cpu, freethreaded),
whl_abi_tags = ["cp{major}{minor}t"] if freethreaded else [
"abi3",
"cp{major}{minor}",
],
Expand All @@ -87,6 +91,10 @@ pip = use_extension("//python/extensions:pip.bzl", "pip")
"x86_64",
"aarch64",
]
for freethreaded in [
"",
"_freethreaded",
]
]

[
Expand All @@ -95,13 +103,17 @@ pip = use_extension("//python/extensions:pip.bzl", "pip")
config_settings = [
"@platforms//cpu:{}".format(cpu),
"@platforms//os:osx",
"//python/config_settings:_is_py_freethreaded_{}".format(
"yes" if freethreaded else "no",
),
],
# We choose the oldest non-EOL version at the time when we release `rules_python`.
# See https://endoflife.date/macos
env = {"platform_version": "14.0"},
marker = "python_version >= '3.13'" if freethreaded else "",
os_name = "osx",
platform = "osx_{}".format(cpu),
whl_abi_tags = [
platform = "osx_{}{}".format(cpu, freethreaded),
whl_abi_tags = ["cp{major}{minor}t"] if freethreaded else [
"abi3",
"cp{major}{minor}",
],
Expand All @@ -120,6 +132,10 @@ pip = use_extension("//python/extensions:pip.bzl", "pip")
"x86_64",
],
}.items()
for freethreaded in [
"",
"_freethreaded",
]
]

[
Expand All @@ -128,11 +144,15 @@ pip = use_extension("//python/extensions:pip.bzl", "pip")
config_settings = [
"@platforms//cpu:{}".format(cpu),
"@platforms//os:windows",
"//python/config_settings:_is_py_freethreaded_{}".format(
"yes" if freethreaded else "no",
),
],
env = {"platform_version": "0"},
marker = "python_version >= '3.13'" if freethreaded else "",
os_name = "windows",
platform = "windows_{}".format(cpu),
whl_abi_tags = [
platform = "windows_{}{}".format(cpu, freethreaded),
whl_abi_tags = ["cp{major}{minor}t"] if freethreaded else [
"abi3",
"cp{major}{minor}",
],
Expand All @@ -141,6 +161,10 @@ pip = use_extension("//python/extensions:pip.bzl", "pip")
for cpu, whl_platform_tags in {
"x86_64": ["win_amd64"],
}.items()
for freethreaded in [
"",
"_freethreaded",
]
]

pip.parse(
Expand Down
84 changes: 61 additions & 23 deletions python/private/pypi/extension.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ load(":hub_repository.bzl", "hub_repository", "whl_config_settings_to_json")
load(":parse_requirements.bzl", "parse_requirements")
load(":parse_whl_name.bzl", "parse_whl_name")
load(":pep508_env.bzl", "env")
load(":pep508_evaluate.bzl", "evaluate")
load(":pip_repository_attrs.bzl", "ATTRS")
load(":python_tag.bzl", "python_tag")
load(":requirements_files_by_platform.bzl", "requirements_files_by_platform")
Expand Down Expand Up @@ -80,21 +81,27 @@ def _platforms(*, python_version, minor_mapping, config):
for platform, values in config.platforms.items():
# TODO @aignas 2025-07-07: this is probably doing the parsing of the version too
# many times.
key = "{}{}{}.{}_{}".format(
abi = "{}{}{}.{}".format(
python_tag(values.env["implementation_name"]),
python_version.release[0],
python_version.release[1],
python_version.release[2],
platform,
)
key = "{}_{}".format(abi, platform)

env_ = env(
env = values.env,
os = values.os_name,
arch = values.arch_name,
python_version = python_version.string,
)

if values.marker and not evaluate(values.marker, env = env_):
continue

platforms[key] = struct(
env = env(
env = values.env,
os = values.os_name,
arch = values.arch_name,
python_version = python_version.string,
),
env = env_,
triple = "{}_{}_{}".format(abi, values.os_name, values.arch_name),
whl_abi_tags = [
v.format(
major = python_version.release[0],
Expand Down Expand Up @@ -203,17 +210,19 @@ def _create_whl_repos(
whl_group_mapping = {}
requirement_cycles = {}

platforms = _platforms(
python_version = pip_attr.python_version,
minor_mapping = minor_mapping,
config = config,
)

if evaluate_markers:
# This is most likely unit tests
pass
elif config.enable_pipstar:
evaluate_markers = lambda _, requirements: evaluate_markers_star(
requirements = requirements,
platforms = _platforms(
python_version = pip_attr.python_version,
minor_mapping = minor_mapping,
config = config,
),
platforms = platforms,
)
else:
# NOTE @aignas 2024-08-02: , we will execute any interpreter that we find either
Expand All @@ -232,7 +241,13 @@ def _create_whl_repos(
# spin up a Python interpreter.
evaluate_markers = lambda module_ctx, requirements: evaluate_markers_py(
module_ctx,
requirements = requirements,
requirements = {
k: {
p: platforms[p].triple
for p in plats
}
for k, plats in requirements.items()
},
python_interpreter = pip_attr.python_interpreter,
python_interpreter_target = python_interpreter_target,
srcs = pip_attr._evaluate_markers_srcs,
Expand All @@ -248,18 +263,14 @@ def _create_whl_repos(
requirements_osx = pip_attr.requirements_darwin,
requirements_windows = pip_attr.requirements_windows,
extra_pip_args = pip_attr.extra_pip_args,
platforms = sorted(config.platforms), # here we only need keys
platforms = sorted(platforms), # here we only need keys
python_version = full_version(
version = pip_attr.python_version,
minor_mapping = minor_mapping,
),
logger = logger,
),
platforms = _platforms(
python_version = pip_attr.python_version,
minor_mapping = minor_mapping,
config = config,
),
platforms = platforms,
extra_pip_args = pip_attr.extra_pip_args,
get_index_urls = get_index_urls,
evaluate_markers = evaluate_markers,
Expand Down Expand Up @@ -344,8 +355,19 @@ def _create_whl_repos(
repo_name,
whl.name,
))

whl_libraries[repo_name] = repo.args

if not config.enable_pipstar and "experimental_target_platforms" in repo.args:
whl_libraries[repo_name] |= {
"experimental_target_platforms": sorted({
# TODO @aignas 2025-07-07: this should be solved in a better way
platforms[candidate].triple.partition("_")[-1]: None
for p in repo.args["experimental_target_platforms"]
for candidate in platforms
if candidate.endswith(p)
}),
}

mapping = whl_map.setdefault(whl.name, {})
if repo.config_setting in mapping and mapping[repo.config_setting] != repo_name:
fail(
Expand Down Expand Up @@ -436,7 +458,7 @@ def _whl_repo(
),
)

def _plat(*, name, arch_name, os_name, config_settings = [], env = {}, whl_abi_tags = [], whl_platform_tags = []):
def _plat(*, name, arch_name, os_name, config_settings = [], env = {}, marker = "", whl_abi_tags = [], whl_platform_tags = []):
# NOTE @aignas 2025-07-08: the least preferred is the first item in the list
if "any" not in whl_platform_tags:
# the lowest priority one needs to be the first one
Expand All @@ -456,6 +478,7 @@ def _plat(*, name, arch_name, os_name, config_settings = [], env = {}, whl_abi_t
# defaults for env
"implementation_name": "cpython",
} | env,
marker = marker,
whl_abi_tags = whl_abi_tags,
whl_platform_tags = whl_platform_tags,
)
Expand Down Expand Up @@ -503,13 +526,14 @@ def build_config(
config_settings = tag.config_settings,
env = tag.env,
os_name = tag.os_name,
marker = tag.marker,
name = platform.replace("-", "_").lower(),
whl_abi_tags = tag.whl_abi_tags,
whl_platform_tags = tag.whl_platform_tags,
override = mod.is_root,
)

if platform and not (tag.arch_name or tag.config_settings or tag.env or tag.os_name or tag.whl_abi_tags or tag.whl_platform_tags):
if platform and not (tag.arch_name or tag.config_settings or tag.env or tag.os_name or tag.whl_abi_tags or tag.whl_platform_tags or tag.marker):
defaults["platforms"].pop(platform)

_configure(
Expand Down Expand Up @@ -916,6 +940,20 @@ Supported keys:
::::{note}
This is only used if the {envvar}`RULES_PYTHON_ENABLE_PIPSTAR` is enabled.
::::
""",
),
"marker": attr.string(
doc = """\
An environment marker expression that is used to enable/disable platforms for specific python
versions, operating systems or CPU architectures.

If specified, the expression is evaluated during the `bzlmod` extension evaluation phase and if it
evaluates to `True`, then the platform will be used to construct the hub repositories, otherwise, it
will be skipped.

This is especially useful for setting up freethreaded platform variants only for particular Python
versions for which the interpreter builds are available. However, this could be also used for other
things, such as setting up platforms for different `libc` variants.
""",
),
# The values for PEP508 env marker evaluation during the lock file parsing
Expand Down
7 changes: 6 additions & 1 deletion python/private/pypi/pip_repository.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,12 @@ def _pip_repository_impl(rctx):
extra_pip_args = rctx.attr.extra_pip_args,
evaluate_markers = lambda rctx, requirements: evaluate_markers_py(
rctx,
requirements = requirements,
requirements = {
# NOTE @aignas 2025-07-07: because we don't distinguish between
# freethreaded and non-freethreaded, it is a 1:1 mapping.
req: {p: p for p in plats}
for req, plats in requirements.items()
},
python_interpreter = rctx.attr.python_interpreter,
python_interpreter_target = rctx.attr.python_interpreter_target,
srcs = rctx.attr._evaluate_markers_srcs,
Expand Down
8 changes: 5 additions & 3 deletions python/private/pypi/requirements_files_by_platform.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,9 @@ def _default_platforms(*, filter, platforms):
if not prefix:
return platforms

match = [p for p in platforms if p.startswith(prefix)]
match = [p for p in platforms if p.startswith(prefix) or (
p.startswith("cp") and p.partition("_")[-1].startswith(prefix)
)]
else:
match = [p for p in platforms if filter in p]

Expand Down Expand Up @@ -140,7 +142,7 @@ def requirements_files_by_platform(
if logger:
logger.debug(lambda: "Platforms from pip args: {}".format(platforms_from_args))

default_platforms = [_platform(p, python_version) for p in platforms]
default_platforms = platforms

if platforms_from_args:
lock_files = [
Expand Down Expand Up @@ -252,6 +254,6 @@ def requirements_files_by_platform(

ret = {}
for plat, file in requirements.items():
ret.setdefault(file, []).append(plat)
ret.setdefault(file, []).append(_platform(plat, python_version = python_version))

return ret
Original file line number Diff line number Diff line change
Expand Up @@ -50,8 +50,8 @@ def main():
hashes = prefix + hashes

req = Requirement(entry)
for p in target_platforms:
(platform,) = Platform.from_string(p)
for p, triple in target_platforms.items():
(platform,) = Platform.from_string(triple)
if not req.marker or req.marker.evaluate(platform.env_markers("")):
response.setdefault(requirement_line, []).append(p)

Expand Down
Loading