Skip to content

Commit ce4dd59

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 415e893 commit ce4dd59

File tree

3 files changed

+140
-94
lines changed

3 files changed

+140
-94
lines changed

python/private/pypi/extension.bzl

Lines changed: 132 additions & 91 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,6 +84,10 @@ 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_,
8893
want_abis = [
@@ -190,17 +195,19 @@ def _create_whl_repos(
190195
whl_group_mapping = {}
191196
requirement_cycles = {}
192197

198+
platforms = _platforms(
199+
python_version = pip_attr.python_version,
200+
minor_mapping = minor_mapping,
201+
config = config,
202+
)
203+
193204
if evaluate_markers:
194205
# This is most likely unit tests
195206
pass
196207
elif config.enable_pipstar:
197208
evaluate_markers = lambda _, requirements: evaluate_markers_star(
198209
requirements = requirements,
199-
platforms = _platforms(
200-
python_version = pip_attr.python_version,
201-
minor_mapping = minor_mapping,
202-
config = config,
203-
),
210+
platforms = platforms,
204211
)
205212
else:
206213
# NOTE @aignas 2024-08-02: , we will execute any interpreter that we find either
@@ -235,18 +242,14 @@ def _create_whl_repos(
235242
requirements_osx = pip_attr.requirements_darwin,
236243
requirements_windows = pip_attr.requirements_windows,
237244
extra_pip_args = pip_attr.extra_pip_args,
238-
platforms = sorted(config.platforms), # here we only need keys
245+
platforms = sorted(platforms), # here we only need keys
239246
python_version = full_version(
240247
version = pip_attr.python_version,
241248
minor_mapping = minor_mapping,
242249
),
243250
logger = logger,
244251
),
245-
platforms = _platforms(
246-
python_version = pip_attr.python_version,
247-
minor_mapping = minor_mapping,
248-
config = config,
249-
),
252+
platforms = platforms,
250253
extra_pip_args = pip_attr.extra_pip_args,
251254
get_index_urls = get_index_urls,
252255
evaluate_markers = evaluate_markers,
@@ -385,7 +388,7 @@ def _whl_repo(*, src, whl_library_args, is_multiple_versions, download_only, net
385388
),
386389
)
387390

388-
def _configure(config, *, platform, os_name, arch_name, config_settings, env = {}, want_abis, platform_tags, override = False):
391+
def _configure(config, *, platform, os_name, arch_name, config_settings, env = {}, want_abis, platform_tags, marker, override = False):
389392
"""Set the value in the config if the value is provided"""
390393
config.setdefault("platforms", {})
391394
if platform and (os_name or arch_name or config_settings or platform_tags or env):
@@ -406,21 +409,25 @@ def _configure(config, *, platform, os_name, arch_name, config_settings, env = {
406409
# the lowest priority one needs to be the first one
407410
platform_tags = ["any"] + platform_tags
408411

412+
want_abis = want_abis or [
413+
"cp{0}{1}",
414+
"abi3",
415+
"none",
416+
]
417+
env = {
418+
# default to this
419+
"implementation_name": "cpython",
420+
} | env
421+
409422
config["platforms"][platform] = struct(
410423
name = platform.replace("-", "_").lower(),
411-
os_name = os_name,
412424
arch_name = arch_name,
413425
config_settings = config_settings,
414-
want_abis = want_abis or [
415-
"cp{0}{1}",
416-
"abi3",
417-
"none",
418-
],
426+
env = env,
427+
marker = marker,
428+
os_name = os_name,
419429
platform_tags = platform_tags,
420-
env = {
421-
# default to this
422-
"implementation_name": "cpython",
423-
} | env,
430+
want_abis = want_abis,
424431
)
425432
else:
426433
config["platforms"].pop(platform)
@@ -432,79 +439,106 @@ def _set_defaults(defaults):
432439
a little problematic.
433440
"""
434441

435-
# NOTE: We have this so that it is easier to maintain unit tests assuming certain
436-
# defaults
437-
for cpu in [
438-
"x86_64",
439-
"aarch64",
440-
# TODO @aignas 2025-05-19: only leave tier 0-1 cpus when stabilizing the
441-
# `pip.default` extension. i.e. drop the below values - users will have to
442-
# define themselves if they need them.
443-
"arm",
444-
"ppc",
445-
"s390x",
446-
]:
447-
_configure(
448-
defaults,
449-
arch_name = cpu,
450-
os_name = "linux",
451-
platform = "linux_{}".format(cpu),
452-
want_abis = [],
453-
config_settings = [
454-
"@platforms//os:linux",
455-
"@platforms//cpu:{}".format(cpu),
442+
for suffix, config in {
443+
"": struct(
444+
want_abis = [
445+
"cp{0}{1}",
446+
"abi3",
447+
"none",
456448
],
457-
platform_tags = [
458-
"linux_*_{}".format(cpu),
459-
"manylinux_*_{}".format(cpu),
449+
freethreaded = "no",
450+
marker = "",
451+
),
452+
"_freethreaded": struct(
453+
want_abis = [
454+
"cp{0}{1}t",
455+
"none",
460456
],
461-
env = {
462-
"platform_version": "0",
463-
},
464-
)
465-
for cpu, platform_tag_cpus in {
466-
"aarch64": ["universal2", "arm64"],
467-
"x86_64": ["universal2", "x86_64"],
457+
freethreaded = "yes",
458+
marker = "python_version ~= \"3.13\"",
459+
),
468460
}.items():
469-
_configure(
470-
defaults,
471-
arch_name = cpu,
472-
os_name = "osx",
473-
platform = "osx_{}".format(cpu),
474-
config_settings = [
475-
"@platforms//os:osx",
476-
"@platforms//cpu:{}".format(cpu),
477-
],
478-
want_abis = [],
479-
platform_tags = [
480-
"macosx_*_{}".format(suffix)
481-
for suffix in platform_tag_cpus
482-
],
483-
# We choose the oldest non-EOL version at the time when we release `rules_python`.
484-
# See https://endoflife.date/macos
485-
env = {
486-
"platform_version": "14.0",
487-
},
488-
)
461+
freethreaded_flag = Label("//python/config_settings:_is_py_freethreaded_{}".format(config.freethreaded))
462+
463+
# NOTE: We have this so that it is easier to maintain unit tests assuming certain
464+
# defaults
465+
for cpu in [
466+
"x86_64",
467+
"aarch64",
468+
# TODO @aignas 2025-05-19: only leave tier 0-1 cpus when stabilizing the
469+
# `pip.default` extension. i.e. drop the below values - users will have to
470+
# define themselves if they need them.
471+
"arm",
472+
"ppc",
473+
"s390x",
474+
]:
475+
_configure(
476+
defaults,
477+
arch_name = cpu,
478+
os_name = "linux",
479+
platform = "linux_{}{}".format(cpu, suffix),
480+
want_abis = config.want_abis,
481+
marker = config.marker,
482+
config_settings = [
483+
"@platforms//os:linux",
484+
"@platforms//cpu:{}".format(cpu),
485+
freethreaded_flag,
486+
],
487+
platform_tags = [
488+
"linux_*_{}".format(cpu),
489+
"manylinux_*_{}".format(cpu),
490+
],
491+
env = {
492+
"platform_version": "0",
493+
},
494+
)
495+
for cpu, platform_tag_cpus in {
496+
"aarch64": ["universal2", "arm64"],
497+
"x86_64": ["universal2", "x86_64"],
498+
}.items():
499+
_configure(
500+
defaults,
501+
arch_name = cpu,
502+
os_name = "osx",
503+
platform = "osx_{}{}".format(cpu, suffix),
504+
config_settings = [
505+
"@platforms//os:osx",
506+
"@platforms//cpu:{}".format(cpu),
507+
freethreaded_flag,
508+
],
509+
want_abis = config.want_abis,
510+
marker = config.marker,
511+
platform_tags = [
512+
"macosx_*_{}".format(suffix)
513+
for suffix in platform_tag_cpus
514+
],
515+
# We choose the oldest non-EOL version at the time when we release `rules_python`.
516+
# See https://endoflife.date/macos
517+
env = {
518+
"platform_version": "14.0",
519+
},
520+
)
489521

490-
for cpu, platform_tags in {
491-
"x86_64": ["win_amd64"],
492-
}.items():
493-
_configure(
494-
defaults,
495-
arch_name = cpu,
496-
os_name = "windows",
497-
platform = "windows_{}".format(cpu),
498-
config_settings = [
499-
"@platforms//os:windows",
500-
"@platforms//cpu:{}".format(cpu),
501-
],
502-
want_abis = [],
503-
platform_tags = platform_tags,
504-
env = {
505-
"platform_version": "0",
506-
},
507-
)
522+
for cpu, platform_tags in {
523+
"x86_64": ["win_amd64"],
524+
}.items():
525+
_configure(
526+
defaults,
527+
arch_name = cpu,
528+
os_name = "windows",
529+
platform = "windows_{}{}".format(cpu, suffix),
530+
config_settings = [
531+
"@platforms//os:windows",
532+
"@platforms//cpu:{}".format(cpu),
533+
freethreaded_flag,
534+
],
535+
want_abis = config.want_abis,
536+
marker = config.marker,
537+
platform_tags = platform_tags,
538+
env = {
539+
"platform_version": "0",
540+
},
541+
)
508542

509543
def parse_modules(
510544
module_ctx,
@@ -573,6 +607,7 @@ You cannot use both the additive_build_content and additive_build_content_file a
573607
env = tag.env,
574608
os_name = tag.os_name,
575609
platform = tag.platform,
610+
marker = tag.marker,
576611
platform_tags = tag.platform_tags,
577612
want_abis = tag.want_abis,
578613
override = mod.is_root,
@@ -905,6 +940,12 @@ Supported keys:
905940
::::{note}
906941
This is only used if the {envvar}`RULES_PYTHON_ENABLE_PIPSTAR` is enabled.
907942
::::
943+
""",
944+
),
945+
"marker": attr.string(
946+
doc = """\
947+
A marker which will be evaluated to disable the target platform for certain python versions. This
948+
is especially useful when defining freethreaded platform variants.
908949
""",
909950
),
910951
# 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 = [

tests/pypi/extension/extension_tests.bzl

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,7 @@ def _default(
8484
platform = None,
8585
platform_tags = None,
8686
env = None,
87+
marker = None,
8788
want_abis = None):
8889
return struct(
8990
arch_name = arch_name,
@@ -92,6 +93,7 @@ def _default(
9293
platform_tags = platform_tags or [],
9394
config_settings = config_settings,
9495
env = env or {},
96+
marker = marker or "",
9597
want_abis = want_abis or [],
9698
)
9799

@@ -1068,7 +1070,7 @@ def _test_pipstar_platforms(env):
10681070
("osx", "aarch64"),
10691071
]
10701072
] + [
1071-
_default(platform = name)
1073+
_default(platform = name + suffix)
10721074
for name in [
10731075
"linux_x86_64",
10741076
"linux_aarch64",
@@ -1079,6 +1081,7 @@ def _test_pipstar_platforms(env):
10791081
"osx_aarch64",
10801082
"windows_x86_64",
10811083
]
1084+
for suffix in ["", "_freethreaded"]
10821085
],
10831086
parse = [
10841087
_parse(

0 commit comments

Comments
 (0)