Skip to content

Commit b1720c1

Browse files
committed
feat(pypi): freethreaded support for the builder API
DO NOT MERGE: stacked on #3058 This is a continuation of #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 #2548, since this shows how we can define custom platforms Work towards #2747
1 parent 89e58de commit b1720c1

File tree

6 files changed

+119
-46
lines changed

6 files changed

+119
-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, triple in target_platforms.items():
54+
(platform,) = Platform.from_string(triple)
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)