Skip to content

Commit 5e66b5b

Browse files
committed
feat: freethreaded support for the builder API
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 TODO: - [ ] Fix the remaining expectations in the unit tests. Maybe make the tests less brittle and define platforms for unit testing.
1 parent 855a673 commit 5e66b5b

File tree

6 files changed

+141
-52
lines changed

6 files changed

+141
-52
lines changed

MODULE.bazel

Lines changed: 52 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -70,14 +70,22 @@ 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),
80+
platform = "linux_{}{}".format(cpu, freethreaded),
7781
platform_tags = [
7882
"linux_*_{}".format(cpu),
7983
"manylinux_*_{}".format(cpu),
8084
],
85+
want_abis = [
86+
"cp{0}{1}t",
87+
"none",
88+
] if freethreaded else [],
8189
)
8290
for cpu in [
8391
"x86_64",
@@ -89,6 +97,10 @@ pip = use_extension("//python/extensions:pip.bzl", "pip")
8997
"ppc",
9098
"s390x",
9199
]
100+
for freethreaded in [
101+
"",
102+
"_freethreaded",
103+
]
92104
]
93105

94106
[
@@ -97,16 +109,24 @@ pip = use_extension("//python/extensions:pip.bzl", "pip")
97109
config_settings = [
98110
"@platforms//cpu:{}".format(cpu),
99111
"@platforms//os:osx",
112+
"//python/config_settings:_is_py_freethreaded_{}".format(
113+
"yes" if freethreaded else "no",
114+
),
100115
],
101116
# We choose the oldest non-EOL version at the time when we release `rules_python`.
102117
# See https://endoflife.date/macos
103118
env = {"platform_version": "14.0"},
119+
marker = "python_version ~= \"3.13\"" if freethreaded else "",
104120
os_name = "osx",
105-
platform = "osx_{}".format(cpu),
121+
platform = "osx_{}{}".format(cpu, freethreaded),
106122
platform_tags = [
107123
"macosx_*_{}".format(suffix)
108124
for suffix in platform_tag_cpus
109125
],
126+
want_abis = [
127+
"cp{0}{1}t",
128+
"none",
129+
] if freethreaded else [],
110130
)
111131
for cpu, platform_tag_cpus in {
112132
"aarch64": [
@@ -118,19 +138,38 @@ pip = use_extension("//python/extensions:pip.bzl", "pip")
118138
"x86_64",
119139
],
120140
}.items()
141+
for freethreaded in [
142+
"",
143+
"_freethreaded",
144+
]
145+
]
146+
147+
[
148+
pip.default(
149+
arch_name = "x86_64",
150+
config_settings = [
151+
"@platforms//cpu:x86_64",
152+
"@platforms//os:windows",
153+
"//python/config_settings:_is_py_freethreaded_{}".format(
154+
"yes" if freethreaded else "no",
155+
),
156+
],
157+
env = {"platform_version": "0"},
158+
marker = "python_version ~= \"3.13\"" if freethreaded else "",
159+
os_name = "windows",
160+
platform = "windows_x86_64{}".format(freethreaded),
161+
platform_tags = ["win_amd64"],
162+
want_abis = [
163+
"cp{0}{1}t",
164+
"none",
165+
] if freethreaded else [],
166+
)
167+
for freethreaded in [
168+
"",
169+
"_freethreaded",
170+
]
121171
]
122172

123-
pip.default(
124-
arch_name = "x86_64",
125-
config_settings = [
126-
"@platforms//cpu:x86_64",
127-
"@platforms//os:windows",
128-
],
129-
env = {"platform_version": "0"},
130-
os_name = "windows",
131-
platform = "windows_x86_64",
132-
platform_tags = ["win_amd64"],
133-
)
134173
pip.parse(
135174
# NOTE @aignas 2024-10-26: We have an integration test that depends on us
136175
# being able to build sdists for this hub, so explicitly set this to False.

python/private/pypi/extension.bzl

Lines changed: 55 additions & 23 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(":requirements_files_by_platform.bzl", "requirements_files_by_platform")
3536
load(":simpleapi_download.bzl", "simpleapi_download")
@@ -83,8 +84,13 @@ def _platforms(*, python_version, minor_mapping, config):
8384
os = values.os_name,
8485
arch = values.arch_name,
8586
)) | values.env
87+
88+
if values.marker and not evaluate(values.marker, env = env_):
89+
continue
90+
8691
platforms[key] = struct(
8792
env = env_,
93+
tripple = "{}_{}_{}".format(abi, values.os_name, values.arch_name),
8894
want_abis = [
8995
v.format(*python_version.split("."))
9096
for v in values.want_abis
@@ -190,17 +196,19 @@ def _create_whl_repos(
190196
whl_group_mapping = {}
191197
requirement_cycles = {}
192198

199+
platforms = _platforms(
200+
python_version = pip_attr.python_version,
201+
minor_mapping = minor_mapping,
202+
config = config,
203+
)
204+
193205
if evaluate_markers:
194206
# This is most likely unit tests
195207
pass
196208
elif config.enable_pipstar:
197209
evaluate_markers = lambda _, requirements: evaluate_markers_star(
198210
requirements = requirements,
199-
platforms = _platforms(
200-
python_version = pip_attr.python_version,
201-
minor_mapping = minor_mapping,
202-
config = config,
203-
),
211+
platforms = platforms,
204212
)
205213
else:
206214
# NOTE @aignas 2024-08-02: , we will execute any interpreter that we find either
@@ -219,7 +227,14 @@ def _create_whl_repos(
219227
# spin up a Python interpreter.
220228
evaluate_markers = lambda module_ctx, requirements: evaluate_markers_py(
221229
module_ctx,
222-
requirements = requirements,
230+
requirements = {
231+
k: {
232+
# TODO @aignas 2025-07-06: should we leave this as is?
233+
p: platforms[p].tripple
234+
for p in plats
235+
}
236+
for k, plats in requirements.items()
237+
},
223238
python_interpreter = pip_attr.python_interpreter,
224239
python_interpreter_target = python_interpreter_target,
225240
srcs = pip_attr._evaluate_markers_srcs,
@@ -235,18 +250,14 @@ def _create_whl_repos(
235250
requirements_osx = pip_attr.requirements_darwin,
236251
requirements_windows = pip_attr.requirements_windows,
237252
extra_pip_args = pip_attr.extra_pip_args,
238-
platforms = sorted(config.platforms), # here we only need keys
253+
platforms = sorted(platforms), # here we only need keys
239254
python_version = full_version(
240255
version = pip_attr.python_version,
241256
minor_mapping = minor_mapping,
242257
),
243258
logger = logger,
244259
),
245-
platforms = _platforms(
246-
python_version = pip_attr.python_version,
247-
minor_mapping = minor_mapping,
248-
config = config,
249-
),
260+
platforms = platforms,
250261
extra_pip_args = pip_attr.extra_pip_args,
251262
get_index_urls = get_index_urls,
252263
evaluate_markers = evaluate_markers,
@@ -320,6 +331,16 @@ def _create_whl_repos(
320331
))
321332

322333
whl_libraries[repo_name] = repo.args
334+
if "experimental_target_platforms" in repo.args:
335+
whl_libraries[repo_name] |= {
336+
"experimental_target_platforms": sorted({
337+
# TODO @aignas 2025-07-07: this should be solved in a better way
338+
platforms[candidate].tripple.partition("_")[-1]: None
339+
for p in repo.args["experimental_target_platforms"]
340+
for candidate in platforms
341+
if candidate.endswith(p)
342+
}),
343+
}
323344
whl_map.setdefault(whl.name, {})[repo.config_setting] = repo_name
324345

325346
return struct(
@@ -385,7 +406,7 @@ def _whl_repo(*, src, whl_library_args, is_multiple_versions, download_only, net
385406
),
386407
)
387408

388-
def _configure(config, *, platform, os_name, arch_name, config_settings, env = {}, want_abis, platform_tags, override = False):
409+
def _configure(config, *, platform, os_name, arch_name, config_settings, env = {}, want_abis, platform_tags, marker, override = False):
389410
"""Set the value in the config if the value is provided"""
390411
config.setdefault("platforms", {})
391412
if platform and (os_name or arch_name or config_settings or platform_tags or env):
@@ -406,21 +427,25 @@ def _configure(config, *, platform, os_name, arch_name, config_settings, env = {
406427
# the lowest priority one needs to be the first one
407428
platform_tags = ["any"] + platform_tags
408429

430+
want_abis = want_abis or [
431+
"cp{0}{1}",
432+
"abi3",
433+
"none",
434+
]
435+
env = {
436+
# default to this
437+
"implementation_name": "cpython",
438+
} | env
439+
409440
config["platforms"][platform] = struct(
410441
name = platform.replace("-", "_").lower(),
411-
os_name = os_name,
412442
arch_name = arch_name,
413443
config_settings = config_settings,
414-
want_abis = want_abis or [
415-
"cp{0}{1}",
416-
"abi3",
417-
"none",
418-
],
444+
env = env,
445+
marker = marker,
446+
os_name = os_name,
419447
platform_tags = platform_tags,
420-
env = {
421-
# default to this
422-
"implementation_name": "cpython",
423-
} | env,
448+
want_abis = want_abis,
424449
)
425450
else:
426451
config["platforms"].pop(platform)
@@ -491,6 +516,7 @@ You cannot use both the additive_build_content and additive_build_content_file a
491516
env = tag.env,
492517
os_name = tag.os_name,
493518
platform = tag.platform,
519+
marker = tag.marker,
494520
platform_tags = tag.platform_tags,
495521
want_abis = tag.want_abis,
496522
override = mod.is_root,
@@ -823,6 +849,12 @@ Supported keys:
823849
::::{note}
824850
This is only used if the {envvar}`RULES_PYTHON_ENABLE_PIPSTAR` is enabled.
825851
::::
852+
""",
853+
),
854+
"marker": attr.string(
855+
doc = """\
856+
A marker which will be evaluated to disable the target platform for certain python versions. This
857+
is especially useful when defining freethreaded platform variants.
826858
""",
827859
),
828860
# 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)