Skip to content

Commit cea20c0

Browse files
committed
feat: allow toolchain without host compatible variant
1 parent 2036571 commit cea20c0

File tree

8 files changed

+344
-47
lines changed

8 files changed

+344
-47
lines changed

.python-version

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
3.13.3
1+
3.12

MODULE.bazel

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,23 @@ dev_python = use_extension(
125125
dev_python.override(
126126
register_all_versions = True,
127127
)
128+
dev_python.toolchain(python_version = "3.13")
129+
130+
# For testing an arbitrary runtime triggered by a custom flag.
131+
# See //tests/toolchains:custom_platform_toolchain_test
132+
dev_python.single_version_platform_override(
133+
platform = "linux-x86-install-only-stripped",
134+
python_version = "3.13.3",
135+
sha256 = "01d08b9bc8a96698b9d64c2fc26da4ecc4fa9e708ce0a34fb88f11ab7e552cbd",
136+
#target_compatible_with = [
137+
# "@platforms//os:linux",
138+
# "@platforms//cpu:x86_64",
139+
#],
140+
#target_settings = [
141+
# "@@//tests/support:is_custom_runtime_linux-x86-install-only-stripped",
142+
#],
143+
urls = ["https://github.com/astral-sh/python-build-standalone/releases/download/20250409/cpython-3.13.3+20250409-x86_64-unknown-linux-gnu-install_only_stripped.tar.gz"],
144+
)
128145

129146
dev_pip = use_extension(
130147
"//python/extensions:pip.bzl",

python/private/python.bzl

Lines changed: 191 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -15,13 +15,19 @@
1515
"Python toolchain module extensions for use with bzlmod."
1616

1717
load("@bazel_features//:features.bzl", "bazel_features")
18-
load("//python:versions.bzl", "DEFAULT_RELEASE_BASE_URL", "PLATFORMS", "TOOL_VERSIONS")
18+
load("//python:versions.bzl", "DEFAULT_RELEASE_BASE_URL", "PLATFORMS", "TOOL_VERSIONS", "platform_info")
1919
load(":auth.bzl", "AUTH_ATTRS")
2020
load(":full_version.bzl", "full_version")
2121
load(":python_register_toolchains.bzl", "python_register_toolchains")
2222
load(":pythons_hub.bzl", "hub_repo")
2323
load(":repo_utils.bzl", "repo_utils")
24-
load(":toolchains_repo.bzl", "host_compatible_python_repo", "multi_toolchain_aliases", "sorted_host_platforms")
24+
load(
25+
":toolchains_repo.bzl",
26+
"host_compatible_python_repo",
27+
"multi_toolchain_aliases",
28+
"sorted_host_platform_names",
29+
"sorted_host_platforms",
30+
)
2531
load(":util.bzl", "IS_BAZEL_6_4_OR_HIGHER")
2632
load(":version.bzl", "version")
2733

@@ -267,6 +273,24 @@ def parse_modules(*, module_ctx, _fail = fail):
267273
def _python_impl(module_ctx):
268274
py = parse_modules(module_ctx = module_ctx)
269275

276+
# Host compatible runtime repos
277+
# dict[str version, struct] where struct has:
278+
# * full_python_version: str
279+
# * platform: platform_info struct
280+
# * platform_name: str platform name
281+
# * impl_repo_name: str repo name of the runtime's python_repository() repo
282+
all_host_compatible_impls = {}
283+
284+
# Host compatible repos that still need to be created because, when
285+
# creating the actual runtime repo, there wasn't a host-compatible
286+
# variant defined for it.
287+
# dict[str reponame, struct] where struct has:
288+
# * compatible_version: str, e.g. 3.10 or 3.10.1. The version the host
289+
# repo should be compatible with
290+
# * full_python_version: str, e.g. 3.10.1, the full python version of
291+
# the toolchain that still needs a host repo created.
292+
needed_host_repos = {}
293+
270294
# list of structs; see inline struct call within the loop below.
271295
toolchain_impls = []
272296

@@ -293,11 +317,24 @@ def _python_impl(module_ctx):
293317
kwargs.update(py.config.kwargs.get(toolchain_info.python_version, {}))
294318
kwargs.update(py.config.kwargs.get(full_python_version, {}))
295319
kwargs.update(py.config.default)
320+
if "3.13" in full_python_version:
321+
print("bzlmod register:", toolchain_info.name, full_python_version)
322+
print("kwargs.platforms:", kwargs["platforms"].keys())
323+
##print("kwargs.tool_versions:", kwargs["tool_versions"])
324+
325+
# todo: this part is failing. python_register_toolchains doesn't have
326+
# the new platform in its platform map, so never tries
296327
register_result = python_register_toolchains(
297328
name = toolchain_info.name,
298329
_internal_bzlmod_toolchain_call = True,
299330
**kwargs
300331
)
332+
if not register_result.impl_repos:
333+
# If nothing was registered, something has gone wrong. This probably
334+
# means the `platforms` map and `tool_versions[version]["shas"]`
335+
# aren't in sync.
336+
# todo: ignore instead of fail?
337+
fail("No impls registered for", toolchain_info)
301338

302339
host_platforms = {}
303340
for repo_name, (platform_name, platform_info) in register_result.impl_repos.items():
@@ -318,23 +355,111 @@ def _python_impl(module_ctx):
318355
set_python_version_constraint = is_last,
319356
))
320357
if _is_compatible_with_host(module_ctx, platform_info):
321-
host_platforms[platform_name] = platform_info
322-
323-
host_platforms = sorted_host_platforms(host_platforms)
324-
host_compatible_python_repo(
325-
name = toolchain_info.name + "_host",
326-
# NOTE: Order matters. The first found to be compatible is (usually) used.
327-
platforms = host_platforms.keys(),
328-
os_names = {
329-
str(i): platform_info.os_name
330-
for i, platform_info in enumerate(host_platforms.values())
331-
},
332-
arch_names = {
333-
str(i): platform_info.arch
334-
for i, platform_info in enumerate(host_platforms.values())
335-
},
336-
python_version = full_python_version,
337-
)
358+
host_compat_entry = struct(
359+
full_python_version = full_python_version,
360+
platform = platform_info,
361+
platform_name = platform_name,
362+
impl_repo_name = repo_name,
363+
)
364+
host_platforms[platform_name] = host_compat_entry
365+
all_host_compatible_impls.setdefault(full_python_version, []).append(
366+
host_compat_entry,
367+
)
368+
all_host_compatible_impls.setdefault(
369+
full_python_version.rpartition(".")[0],
370+
[],
371+
).append(host_compat_entry)
372+
373+
host_repo_name = toolchain_info.name + "_host"
374+
if not host_platforms:
375+
print("need:", host_repo_name)
376+
needed_host_repos[host_repo_name] = struct(
377+
compatible_version = toolchain_info.python_version,
378+
full_python_version = full_python_version,
379+
)
380+
else:
381+
print("create:", host_repo_name)
382+
host_platforms = sorted_host_platforms(host_platforms)
383+
entries = host_platforms.values()
384+
host_compatible_python_repo(
385+
name = host_repo_name,
386+
base_name = host_repo_name,
387+
# NOTE: Order matters. The first found to be compatible is (usually) used.
388+
platforms = host_platforms.keys(),
389+
os_names = {
390+
str(i): entry.platform.os_name
391+
for i, entry in enumerate(entries)
392+
},
393+
arch_names = {
394+
str(i): entry.platform.arch
395+
for i, entry in enumerate(entries)
396+
},
397+
python_versions = {
398+
str(i): entry.full_python_version
399+
for i, entry in enumerate(entries)
400+
},
401+
impl_repo_names = {
402+
str(i): entry.impl_repo_name
403+
for i, entry in enumerate(entries)
404+
},
405+
)
406+
407+
def vt(s):
408+
return tuple([int(x) for x in s.split(".")])
409+
410+
if needed_host_repos:
411+
print("host repos still needed:", needed_host_repos)
412+
for key, entries in all_host_compatible_impls.items():
413+
all_host_compatible_impls[key] = sorted(
414+
entries,
415+
reverse = True,
416+
key = lambda e: vt(e.full_python_version),
417+
)
418+
419+
for host_repo_name, info in needed_host_repos.items():
420+
choices = []
421+
if info.compatible_version not in all_host_compatible_impls:
422+
print(
423+
"No host compatible for:",
424+
info.compatible_version,
425+
"available:",
426+
all_host_compatible_impls.keys(),
427+
)
428+
continue
429+
##fail(" version missing", info.compatible_version)
430+
431+
for entry in all_host_compatible_impls[info.compatible_version]:
432+
# todo: numeric version comparison
433+
# todo: should we restrict at all? Maybe just take the highest?
434+
if vt(entry.full_python_version) <= vt(info.full_python_version):
435+
choices.append(entry)
436+
if choices:
437+
platform_keys = [
438+
# We have to prepend the offset because the same platform
439+
# name might occur across different versions
440+
"{}_{}".format(i, entry.platform_name)
441+
for i, entry in enumerate(choices)
442+
]
443+
platform_keys = sorted_host_platform_names(platform_keys)
444+
445+
print("create alt: {} for {}".format(host_repo_name, info.compatible_version))
446+
print("platforms=", platform_keys)
447+
448+
host_compatible_python_repo(
449+
name = host_repo_name,
450+
base_name = host_repo_name,
451+
platforms = platform_keys,
452+
impl_repo_names = {
453+
str(i): entry.impl_repo_name
454+
for i, entry in enumerate(choices)
455+
},
456+
os_names = {str(i): entry.platform.os_name for i, entry in enumerate(choices)},
457+
arch_names = {str(i): entry.platform.arch for i, entry in enumerate(choices)},
458+
python_versions = {str(i): entry.full_python_version for i, entry in enumerate(choices)},
459+
)
460+
else:
461+
# todo: figure out what to do. Define nothing, if we can.
462+
fail("No host-compatible found")
338463

339464
# List of the base names ("python_3_10") for the toolchain repos
340465
base_toolchain_repo_names = []
@@ -586,6 +711,49 @@ def _process_single_version_platform_overrides(*, tag, _fail = fail, default):
586711
if tag.urls:
587712
available_versions[tag.python_version].setdefault("url", {})[tag.platform] = tag.urls
588713

714+
if tag.platform not in default["platforms"]:
715+
os_name = tag.os_name
716+
arch = tag.arch
717+
if not os_name or not arch:
718+
for v in tag.target_compatible_with:
719+
if os_name and arch:
720+
break
721+
if not os_name:
722+
if v.startswith("@platforms//os:"):
723+
if v.endswith(":linux"):
724+
os_name = "linux"
725+
726+
if not arch:
727+
if v.startswith("@platforms//cpu:"):
728+
if v.endswith(":x86_64"):
729+
arch = "x86_64"
730+
731+
if not os_name:
732+
os_name = "UNKNOWN_CUSTOM"
733+
if not arch:
734+
arch = "UNKNOWN_CUSTOM"
735+
736+
# todo: figure out why these can't be none
737+
os_name = None
738+
arch = None
739+
740+
default["platforms"][tag.platform] = platform_info(
741+
compatible_with = tag.target_compatible_with,
742+
target_settings = tag.target_settings,
743+
os_name = os_name,
744+
arch = arch,
745+
)
746+
elif (
747+
tag.target_compatible_with or tag.target_settings or
748+
tag.os_name or tag.arch
749+
):
750+
# todo: fail, or ignore?
751+
fail((
752+
"Cannot override platform {} with custom platform settings"
753+
).format(
754+
tag.platform,
755+
))
756+
589757
def _process_global_overrides(*, tag, default, _fail = fail):
590758
if tag.available_python_versions:
591759
available_versions = default["tool_versions"]
@@ -1084,12 +1252,14 @@ configuration, please use {obj}`single_version_override`.
10841252
:::
10851253
""",
10861254
attrs = {
1255+
"arch": attr.string(),
10871256
"coverage_tool": attr.label(
10881257
doc = """\
10891258
The coverage tool to be used for a particular Python interpreter. This can override
10901259
`rules_python` defaults.
10911260
""",
10921261
),
1262+
"os_name": attr.string(),
10931263
"patch_strip": attr.int(
10941264
mandatory = False,
10951265
doc = "Same as the --strip argument of Unix patch.",
@@ -1101,7 +1271,6 @@ The coverage tool to be used for a particular Python interpreter. This can overr
11011271
),
11021272
"platform": attr.string(
11031273
mandatory = True,
1104-
values = PLATFORMS.keys(),
11051274
doc = "The platform to override the values for, must be one of:\n{}.".format("\n".join(sorted(["* `{}`".format(p) for p in PLATFORMS]))),
11061275
),
11071276
"python_version": attr.string(
@@ -1117,6 +1286,8 @@ The coverage tool to be used for a particular Python interpreter. This can overr
11171286
doc = "The 'strip_prefix' for the archive, defaults to 'python'.",
11181287
default = "python",
11191288
),
1289+
"target_compatible_with": attr.string_list(),
1290+
"target_settings": attr.string_list(),
11201291
"urls": attr.string_list(
11211292
mandatory = False,
11221293
doc = "The URL template to fetch releases for this Python version. If the URL template results in a relative fragment, default base URL is going to be used. Occurrences of `{python_version}`, `{platform}` and `{build}` will be interpolated based on the contents in the override and the known {attr}`platform` values.",

python/private/python_register_toolchains.bzl

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,8 @@ def python_register_toolchains(
117117

118118
# dict[str repo name, tuple[str, platform_info]]
119119
impl_repos = {}
120+
if "3.13" in python_version:
121+
print("plat map:", platforms.keys())
120122
for platform, platform_info in platforms.items():
121123
sha256 = tool_versions[python_version]["sha256"].get(platform, None)
122124
if not sha256:
@@ -144,6 +146,12 @@ def python_register_toolchains(
144146

145147
impl_repo_name = "{}_{}".format(name, platform)
146148
impl_repos[impl_repo_name] = (platform, platform_info)
149+
if "3.13" in python_version:
150+
print("py repo:", impl_repo_name, python_version)
151+
print(sha256, patches, patch_strip, platform)
152+
print(release_filename, urls, strip_prefix)
153+
print(coverage_tool)
154+
print(kwargs)
147155
python_repository(
148156
name = impl_repo_name,
149157
sha256 = sha256,

python/private/python_repository.bzl

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
"""This file contains repository rules and macros to support toolchain registration.
1616
"""
1717

18-
load("//python:versions.bzl", "FREETHREADED", "INSTALL_ONLY", "PLATFORMS")
18+
load("//python:versions.bzl", "FREETHREADED", "INSTALL_ONLY")
1919
load(":auth.bzl", "get_auth")
2020
load(":repo_utils.bzl", "REPO_DEBUG_ENV_VAR", "repo_utils")
2121
load(":text_util.bzl", "render")
@@ -327,7 +327,7 @@ function defaults (e.g. `single_version_override` for `MODULE.bazel` files.
327327
"platform": attr.string(
328328
doc = "The platform name for the Python interpreter tarball.",
329329
mandatory = True,
330-
values = PLATFORMS.keys(),
330+
##values = PLATFORMS.keys(),
331331
),
332332
"python_version": attr.string(
333333
doc = "The Python version.",

0 commit comments

Comments
 (0)