Skip to content

Commit 68dfedb

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 68dfedb

File tree

5 files changed

+110
-40
lines changed

5 files changed

+110
-40
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: 50 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,17 @@ 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+
p: "{abi}_{os}_{cpu}".format(
235+
abi = platforms[p].abi,
236+
os = platforms[p].os_name,
237+
cpu = platforms[p].arch_name,
238+
)
239+
for p in plats
240+
}
241+
for k, plats in requirements.items()
242+
},
223243
python_interpreter = pip_attr.python_interpreter,
224244
python_interpreter_target = python_interpreter_target,
225245
srcs = pip_attr._evaluate_markers_srcs,
@@ -235,18 +255,14 @@ def _create_whl_repos(
235255
requirements_osx = pip_attr.requirements_darwin,
236256
requirements_windows = pip_attr.requirements_windows,
237257
extra_pip_args = pip_attr.extra_pip_args,
238-
platforms = sorted(config.platforms), # here we only need keys
258+
platforms = sorted(platforms), # here we only need keys
239259
python_version = full_version(
240260
version = pip_attr.python_version,
241261
minor_mapping = minor_mapping,
242262
),
243263
logger = logger,
244264
),
245-
platforms = _platforms(
246-
python_version = pip_attr.python_version,
247-
minor_mapping = minor_mapping,
248-
config = config,
249-
),
265+
platforms = platforms,
250266
extra_pip_args = pip_attr.extra_pip_args,
251267
get_index_urls = get_index_urls,
252268
evaluate_markers = evaluate_markers,
@@ -385,7 +401,7 @@ def _whl_repo(*, src, whl_library_args, is_multiple_versions, download_only, net
385401
),
386402
)
387403

388-
def _configure(config, *, platform, os_name, arch_name, config_settings, env = {}, want_abis, platform_tags, override = False):
404+
def _configure(config, *, platform, os_name, arch_name, config_settings, env = {}, want_abis, platform_tags, marker, override = False):
389405
"""Set the value in the config if the value is provided"""
390406
config.setdefault("platforms", {})
391407
if platform and (os_name or arch_name or config_settings or platform_tags or env):
@@ -406,21 +422,25 @@ def _configure(config, *, platform, os_name, arch_name, config_settings, env = {
406422
# the lowest priority one needs to be the first one
407423
platform_tags = ["any"] + platform_tags
408424

425+
want_abis = want_abis or [
426+
"cp{0}{1}",
427+
"abi3",
428+
"none",
429+
]
430+
env = {
431+
# default to this
432+
"implementation_name": "cpython",
433+
} | env
434+
409435
config["platforms"][platform] = struct(
410436
name = platform.replace("-", "_").lower(),
411-
os_name = os_name,
412437
arch_name = arch_name,
413438
config_settings = config_settings,
414-
want_abis = want_abis or [
415-
"cp{0}{1}",
416-
"abi3",
417-
"none",
418-
],
439+
env = env,
440+
marker = marker,
441+
os_name = os_name,
419442
platform_tags = platform_tags,
420-
env = {
421-
# default to this
422-
"implementation_name": "cpython",
423-
} | env,
443+
want_abis = want_abis,
424444
)
425445
else:
426446
config["platforms"].pop(platform)
@@ -491,6 +511,7 @@ You cannot use both the additive_build_content and additive_build_content_file a
491511
env = tag.env,
492512
os_name = tag.os_name,
493513
platform = tag.platform,
514+
marker = tag.marker,
494515
platform_tags = tag.platform_tags,
495516
want_abis = tag.want_abis,
496517
override = mod.is_root,
@@ -823,6 +844,12 @@ Supported keys:
823844
::::{note}
824845
This is only used if the {envvar}`RULES_PYTHON_ENABLE_PIPSTAR` is enabled.
825846
::::
847+
""",
848+
),
849+
"marker": attr.string(
850+
doc = """\
851+
A marker which will be evaluated to disable the target platform for certain python versions. This
852+
is especially useful when defining freethreaded platform variants.
826853
""",
827854
),
828855
# The values for PEP508 env marker evaluation during the lock file parsing

python/private/pypi/requirements_files_by_platform.bzl

Lines changed: 4 additions & 2 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+
# TODO @aignas 2025-07-06: this assumes that the platforms start with
41+
# cp313.3_<suffix>
42+
match = [p for p in platforms if p.partition("_")[-1].startswith(prefix)]
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 = [

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)