Skip to content

Commit 78d4007

Browse files
committed
feat(pypi): freethreaded support for the builder API
DO NOT MERGE: stacked on bazel-contrib#3058 This is a continuation of bazel-contrib#3058 where we define freethreaded platforms. They need to be used only for particular python versions so I included an extra marker configuration attribute where we are using pipstar marker evaluation before using the platform. I think this in general will be a useful tool to configure only particular platforms for particular python versions Work towards bazel-contrib#2548, since this shows how we can define custom platforms Work towards bazel-contrib#2747
1 parent 89e58de commit 78d4007

File tree

6 files changed

+118
-46
lines changed

6 files changed

+118
-46
lines changed

MODULE.bazel

Lines changed: 30 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -70,11 +70,15 @@ pip = use_extension("//python/extensions:pip.bzl", "pip")
7070
config_settings = [
7171
"@platforms//cpu:{}".format(cpu),
7272
"@platforms//os:linux",
73+
"//python/config_settings:_is_py_freethreaded_{}".format(
74+
"yes" if freethreaded else "no",
75+
),
7376
],
7477
env = {"platform_version": "0"},
78+
marker = "python_version ~= \"3.13\"" if freethreaded else "",
7579
os_name = "linux",
76-
platform = "linux_{}".format(cpu),
77-
whl_abi_tags = [
80+
platform = "linux_{}{}".format(cpu, freethreaded),
81+
whl_abi_tags = ["cp{major}{minor}t"] if freethreaded else [
7882
"abi3",
7983
"cp{major}{minor}",
8084
],
@@ -87,6 +91,10 @@ pip = use_extension("//python/extensions:pip.bzl", "pip")
8791
"x86_64",
8892
"aarch64",
8993
]
94+
for freethreaded in [
95+
"",
96+
"_freethreaded",
97+
]
9098
]
9199

92100
[
@@ -95,13 +103,17 @@ pip = use_extension("//python/extensions:pip.bzl", "pip")
95103
config_settings = [
96104
"@platforms//cpu:{}".format(cpu),
97105
"@platforms//os:osx",
106+
"//python/config_settings:_is_py_freethreaded_{}".format(
107+
"yes" if freethreaded else "no",
108+
),
98109
],
99110
# We choose the oldest non-EOL version at the time when we release `rules_python`.
100111
# See https://endoflife.date/macos
101112
env = {"platform_version": "14.0"},
113+
marker = "python_version ~= \"3.13\"" if freethreaded else "",
102114
os_name = "osx",
103-
platform = "osx_{}".format(cpu),
104-
whl_abi_tags = [
115+
platform = "osx_{}{}".format(cpu, freethreaded),
116+
whl_abi_tags = ["cp{major}{minor}t"] if freethreaded else [
105117
"abi3",
106118
"cp{major}{minor}",
107119
],
@@ -120,6 +132,10 @@ pip = use_extension("//python/extensions:pip.bzl", "pip")
120132
"x86_64",
121133
],
122134
}.items()
135+
for freethreaded in [
136+
"",
137+
"_freethreaded",
138+
]
123139
]
124140

125141
[
@@ -128,11 +144,15 @@ pip = use_extension("//python/extensions:pip.bzl", "pip")
128144
config_settings = [
129145
"@platforms//cpu:{}".format(cpu),
130146
"@platforms//os:windows",
147+
"//python/config_settings:_is_py_freethreaded_{}".format(
148+
"yes" if freethreaded else "no",
149+
),
131150
],
132151
env = {"platform_version": "0"},
152+
marker = "python_version ~= \"3.13\"" if freethreaded else "",
133153
os_name = "windows",
134-
platform = "windows_{}".format(cpu),
135-
whl_abi_tags = [
154+
platform = "windows_{}{}".format(cpu, freethreaded),
155+
whl_abi_tags = ["cp{major}{minor}t"] if freethreaded else [
136156
"abi3",
137157
"cp{major}{minor}",
138158
],
@@ -141,6 +161,10 @@ pip = use_extension("//python/extensions:pip.bzl", "pip")
141161
for cpu, whl_platform_tags in {
142162
"x86_64": ["win_amd64"],
143163
}.items()
164+
for freethreaded in [
165+
"",
166+
"_freethreaded",
167+
]
144168
]
145169

146170
pip.parse(

python/private/pypi/extension.bzl

Lines changed: 54 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ load(":hub_repository.bzl", "hub_repository", "whl_config_settings_to_json")
3030
load(":parse_requirements.bzl", "parse_requirements")
3131
load(":parse_whl_name.bzl", "parse_whl_name")
3232
load(":pep508_env.bzl", "env")
33+
load(":pep508_evaluate.bzl", "evaluate")
3334
load(":pip_repository_attrs.bzl", "ATTRS")
3435
load(":python_tag.bzl", "python_tag")
3536
load(":requirements_files_by_platform.bzl", "requirements_files_by_platform")
@@ -80,21 +81,27 @@ def _platforms(*, python_version, minor_mapping, config):
8081
for platform, values in config.platforms.items():
8182
# TODO @aignas 2025-07-07: this is probably doing the parsing of the version too
8283
# many times.
83-
key = "{}{}{}.{}_{}".format(
84+
abi = "{}{}{}.{}".format(
8485
python_tag(values.env["implementation_name"]),
8586
python_version.release[0],
8687
python_version.release[1],
8788
python_version.release[2],
88-
platform,
8989
)
90+
key = "{}_{}".format(abi, platform)
91+
92+
env_ = env(
93+
env = values.env,
94+
os = values.os_name,
95+
arch = values.arch_name,
96+
python_version = python_version.string,
97+
)
98+
99+
if values.marker and not evaluate(values.marker, env = env_):
100+
continue
90101

91102
platforms[key] = struct(
92-
env = env(
93-
env = values.env,
94-
os = values.os_name,
95-
arch = values.arch_name,
96-
python_version = python_version.string,
97-
),
103+
env = env_,
104+
triple = "{}_{}_{}".format(abi, values.os_name, values.arch_name),
98105
whl_abi_tags = [
99106
v.format(
100107
major = python_version.release[0],
@@ -203,17 +210,19 @@ def _create_whl_repos(
203210
whl_group_mapping = {}
204211
requirement_cycles = {}
205212

213+
platforms = _platforms(
214+
python_version = pip_attr.python_version,
215+
minor_mapping = minor_mapping,
216+
config = config,
217+
)
218+
206219
if evaluate_markers:
207220
# This is most likely unit tests
208221
pass
209222
elif config.enable_pipstar:
210223
evaluate_markers = lambda _, requirements: evaluate_markers_star(
211224
requirements = requirements,
212-
platforms = _platforms(
213-
python_version = pip_attr.python_version,
214-
minor_mapping = minor_mapping,
215-
config = config,
216-
),
225+
platforms = platforms,
217226
)
218227
else:
219228
# NOTE @aignas 2024-08-02: , we will execute any interpreter that we find either
@@ -232,7 +241,14 @@ def _create_whl_repos(
232241
# spin up a Python interpreter.
233242
evaluate_markers = lambda module_ctx, requirements: evaluate_markers_py(
234243
module_ctx,
235-
requirements = requirements,
244+
requirements = {
245+
k: {
246+
# TODO @aignas 2025-07-06: should we leave this as is?
247+
p: platforms[p].triple
248+
for p in plats
249+
}
250+
for k, plats in requirements.items()
251+
},
236252
python_interpreter = pip_attr.python_interpreter,
237253
python_interpreter_target = python_interpreter_target,
238254
srcs = pip_attr._evaluate_markers_srcs,
@@ -248,18 +264,14 @@ def _create_whl_repos(
248264
requirements_osx = pip_attr.requirements_darwin,
249265
requirements_windows = pip_attr.requirements_windows,
250266
extra_pip_args = pip_attr.extra_pip_args,
251-
platforms = sorted(config.platforms), # here we only need keys
267+
platforms = sorted(platforms), # here we only need keys
252268
python_version = full_version(
253269
version = pip_attr.python_version,
254270
minor_mapping = minor_mapping,
255271
),
256272
logger = logger,
257273
),
258-
platforms = _platforms(
259-
python_version = pip_attr.python_version,
260-
minor_mapping = minor_mapping,
261-
config = config,
262-
),
274+
platforms = platforms,
263275
extra_pip_args = pip_attr.extra_pip_args,
264276
get_index_urls = get_index_urls,
265277
evaluate_markers = evaluate_markers,
@@ -346,6 +358,16 @@ def _create_whl_repos(
346358
))
347359

348360
whl_libraries[repo_name] = repo.args
361+
if "experimental_target_platforms" in repo.args:
362+
whl_libraries[repo_name] |= {
363+
"experimental_target_platforms": sorted({
364+
# TODO @aignas 2025-07-07: this should be solved in a better way
365+
platforms[candidate].triple.partition("_")[-1]: None
366+
for p in repo.args["experimental_target_platforms"]
367+
for candidate in platforms
368+
if candidate.endswith(p)
369+
}),
370+
}
349371
whl_map.setdefault(whl.name, {})[repo.config_setting] = repo_name
350372

351373
return struct(
@@ -434,14 +456,15 @@ def _configure(
434456
arch_name,
435457
config_settings,
436458
env = {},
459+
marker,
437460
whl_abi_tags,
438461
whl_platform_tags,
439462
override = False):
440463
"""Set the value in the config if the value is provided"""
441464
config.setdefault("platforms", {})
442465

443466
if platform and (
444-
os_name or arch_name or config_settings or whl_abi_tags or whl_platform_tags or env
467+
os_name or arch_name or config_settings or whl_abi_tags or whl_platform_tags or env or marker
445468
):
446469
if not override and config["platforms"].get(platform):
447470
return
@@ -455,6 +478,7 @@ def _configure(
455478
"arch_name": arch_name,
456479
"config_settings": config_settings,
457480
"env": env,
481+
"marker": marker,
458482
"name": platform.replace("-", "_").lower(),
459483
"os_name": os_name,
460484
"whl_abi_tags": whl_abi_tags,
@@ -470,7 +494,7 @@ def _configure(
470494
else:
471495
config["platforms"].pop(platform)
472496

473-
def _plat(*, name, arch_name, os_name, config_settings = [], env = {}, whl_abi_tags = [], whl_platform_tags = []):
497+
def _plat(*, name, arch_name, os_name, config_settings = [], env = {}, marker = "", whl_abi_tags = [], whl_platform_tags = []):
474498
# NOTE @aignas 2025-07-08: the least preferred is the first item in the list
475499
if "any" not in whl_platform_tags:
476500
# the lowest priority one needs to be the first one
@@ -490,6 +514,7 @@ def _plat(*, name, arch_name, os_name, config_settings = [], env = {}, whl_abi_t
490514
# defaults for env
491515
"implementation_name": "cpython",
492516
} | env,
517+
marker = marker,
493518
whl_abi_tags = whl_abi_tags,
494519
whl_platform_tags = whl_platform_tags,
495520
)
@@ -524,6 +549,7 @@ def build_config(
524549
config_settings = tag.config_settings,
525550
env = tag.env,
526551
os_name = tag.os_name,
552+
marker = tag.marker,
527553
platform = platform,
528554
override = mod.is_root,
529555
whl_abi_tags = tag.whl_abi_tags,
@@ -533,8 +559,6 @@ def build_config(
533559
# attribute.
534560
# * for index/downloader config. This includes all of those attributes for
535561
# overrides, etc. Index overrides per platform could be also used here.
536-
# * for whl selection - selecting preferences of which `platform_tag`s we should use
537-
# for what. We could also model the `cp313t` freethreaded as separate platforms.
538562
)
539563

540564
return struct(
@@ -918,6 +942,12 @@ Supported keys:
918942
::::{note}
919943
This is only used if the {envvar}`RULES_PYTHON_ENABLE_PIPSTAR` is enabled.
920944
::::
945+
""",
946+
),
947+
"marker": attr.string(
948+
doc = """\
949+
A marker which will be evaluated to disable the target platform for certain python versions. This
950+
is especially useful when defining freethreaded platform variants.
921951
""",
922952
),
923953
# The values for PEP508 env marker evaluation during the lock file parsing

python/private/pypi/pip_repository.bzl

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,12 @@ def _pip_repository_impl(rctx):
9494
extra_pip_args = rctx.attr.extra_pip_args,
9595
evaluate_markers = lambda rctx, requirements: evaluate_markers_py(
9696
rctx,
97-
requirements = requirements,
97+
requirements = {
98+
# NOTE @aignas 2025-07-07: because we don't distinguish between
99+
# freethreaded and non-freethreaded, it is a 1:1 mapping.
100+
req: {p: p for p in plats}
101+
for req, plats in requirements.items()
102+
},
98103
python_interpreter = rctx.attr.python_interpreter,
99104
python_interpreter_target = rctx.attr.python_interpreter_target,
100105
srcs = rctx.attr._evaluate_markers_srcs,

python/private/pypi/requirements_files_by_platform.bzl

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,9 @@ def _default_platforms(*, filter, platforms):
3737
if not prefix:
3838
return platforms
3939

40-
match = [p for p in platforms if p.startswith(prefix)]
40+
match = [p for p in platforms if p.startswith(prefix) or (
41+
p.startswith("cp") and p.partition("_")[-1].startswith(prefix)
42+
)]
4143
else:
4244
match = [p for p in platforms if filter in p]
4345

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

143-
default_platforms = [_platform(p, python_version) for p in platforms]
145+
default_platforms = platforms
144146

145147
if platforms_from_args:
146148
lock_files = [
@@ -252,6 +254,6 @@ def requirements_files_by_platform(
252254

253255
ret = {}
254256
for plat, file in requirements.items():
255-
ret.setdefault(file, []).append(plat)
257+
ret.setdefault(file, []).append(_platform(plat, python_version = python_version))
256258

257259
return ret

python/private/pypi/requirements_parser/resolve_target_platforms.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -50,8 +50,8 @@ def main():
5050
hashes = prefix + hashes
5151

5252
req = Requirement(entry)
53-
for p in target_platforms:
54-
(platform,) = Platform.from_string(p)
53+
for p, tripple in target_platforms.items():
54+
(platform,) = Platform.from_string(tripple)
5555
if not req.marker or req.marker.evaluate(platform.env_markers("")):
5656
response.setdefault(requirement_line, []).append(p)
5757

0 commit comments

Comments
 (0)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy