Skip to content

Commit 5a1d250

Browse files
committed
feat: freethreaded support for the builder API
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 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 5a1d250

File tree

5 files changed

+112
-41
lines changed

5 files changed

+112
-41
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: 51 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,15 @@ 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+
abi = abi,
94+
os_name = values.os_name,
95+
arch_name = values.arch_name,
8896
want_abis = [
8997
v.format(*python_version.split("."))
9098
for v in values.want_abis
@@ -190,17 +198,19 @@ def _create_whl_repos(
190198
whl_group_mapping = {}
191199
requirement_cycles = {}
192200

201+
platforms = _platforms(
202+
python_version = pip_attr.python_version,
203+
minor_mapping = minor_mapping,
204+
config = config,
205+
)
206+
193207
if evaluate_markers:
194208
# This is most likely unit tests
195209
pass
196210
elif config.enable_pipstar:
197211
evaluate_markers = lambda _, requirements: evaluate_markers_star(
198212
requirements = requirements,
199-
platforms = _platforms(
200-
python_version = pip_attr.python_version,
201-
minor_mapping = minor_mapping,
202-
config = config,
203-
),
213+
platforms = platforms,
204214
)
205215
else:
206216
# NOTE @aignas 2024-08-02: , we will execute any interpreter that we find either
@@ -219,7 +229,18 @@ def _create_whl_repos(
219229
# spin up a Python interpreter.
220230
evaluate_markers = lambda module_ctx, requirements: evaluate_markers_py(
221231
module_ctx,
222-
requirements = requirements,
232+
requirements = {
233+
k: {
234+
# TODO @aignas 2025-07-06: should we leave this as is?
235+
p: "{abi}_{os}_{cpu}".format(
236+
abi = platforms[p].abi,
237+
os = platforms[p].os_name,
238+
cpu = platforms[p].arch_name,
239+
)
240+
for p in plats
241+
}
242+
for k, plats in requirements.items()
243+
},
223244
python_interpreter = pip_attr.python_interpreter,
224245
python_interpreter_target = python_interpreter_target,
225246
srcs = pip_attr._evaluate_markers_srcs,
@@ -235,18 +256,14 @@ def _create_whl_repos(
235256
requirements_osx = pip_attr.requirements_darwin,
236257
requirements_windows = pip_attr.requirements_windows,
237258
extra_pip_args = pip_attr.extra_pip_args,
238-
platforms = sorted(config.platforms), # here we only need keys
259+
platforms = sorted(platforms), # here we only need keys
239260
python_version = full_version(
240261
version = pip_attr.python_version,
241262
minor_mapping = minor_mapping,
242263
),
243264
logger = logger,
244265
),
245-
platforms = _platforms(
246-
python_version = pip_attr.python_version,
247-
minor_mapping = minor_mapping,
248-
config = config,
249-
),
266+
platforms = platforms,
250267
extra_pip_args = pip_attr.extra_pip_args,
251268
get_index_urls = get_index_urls,
252269
evaluate_markers = evaluate_markers,
@@ -385,7 +402,7 @@ def _whl_repo(*, src, whl_library_args, is_multiple_versions, download_only, net
385402
),
386403
)
387404

388-
def _configure(config, *, platform, os_name, arch_name, config_settings, env = {}, want_abis, platform_tags, override = False):
405+
def _configure(config, *, platform, os_name, arch_name, config_settings, env = {}, want_abis, platform_tags, marker, override = False):
389406
"""Set the value in the config if the value is provided"""
390407
config.setdefault("platforms", {})
391408
if platform and (os_name or arch_name or config_settings or platform_tags or env):
@@ -406,21 +423,25 @@ def _configure(config, *, platform, os_name, arch_name, config_settings, env = {
406423
# the lowest priority one needs to be the first one
407424
platform_tags = ["any"] + platform_tags
408425

426+
want_abis = want_abis or [
427+
"cp{0}{1}",
428+
"abi3",
429+
"none",
430+
]
431+
env = {
432+
# default to this
433+
"implementation_name": "cpython",
434+
} | env
435+
409436
config["platforms"][platform] = struct(
410437
name = platform.replace("-", "_").lower(),
411-
os_name = os_name,
412438
arch_name = arch_name,
413439
config_settings = config_settings,
414-
want_abis = want_abis or [
415-
"cp{0}{1}",
416-
"abi3",
417-
"none",
418-
],
440+
env = env,
441+
marker = marker,
442+
os_name = os_name,
419443
platform_tags = platform_tags,
420-
env = {
421-
# default to this
422-
"implementation_name": "cpython",
423-
} | env,
444+
want_abis = want_abis,
424445
)
425446
else:
426447
config["platforms"].pop(platform)
@@ -491,6 +512,7 @@ You cannot use both the additive_build_content and additive_build_content_file a
491512
env = tag.env,
492513
os_name = tag.os_name,
493514
platform = tag.platform,
515+
marker = tag.marker,
494516
platform_tags = tag.platform_tags,
495517
want_abis = tag.want_abis,
496518
override = mod.is_root,
@@ -823,6 +845,12 @@ Supported keys:
823845
::::{note}
824846
This is only used if the {envvar}`RULES_PYTHON_ENABLE_PIPSTAR` is enabled.
825847
::::
848+
""",
849+
),
850+
"marker": attr.string(
851+
doc = """\
852+
A marker which will be evaluated to disable the target platform for certain python versions. This
853+
is especially useful when defining freethreaded platform variants.
826854
""",
827855
),
828856
# The values for PEP508 env marker evaluation during the lock file parsing

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

tests/pypi/extension/extension_tests.bzl

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,7 @@ def _default(
101101
platform = None,
102102
platform_tags = None,
103103
env = None,
104+
marker = None,
104105
want_abis = None):
105106
return struct(
106107
arch_name = arch_name,
@@ -109,6 +110,7 @@ def _default(
109110
platform_tags = platform_tags or [],
110111
config_settings = config_settings,
111112
env = env or {},
113+
marker = marker or "",
112114
want_abis = want_abis or [],
113115
)
114116

0 commit comments

Comments
 (0)