Skip to content

Commit 4e64781

Browse files
gregmagolanjbedard
andauthored
fix(toolchains): register native_build_toolchain per exec/target platform for sdist builds (#928)
## Problem `native_build_toolchain` (used by `pep517_whl` for sdist builds) was not registered with `@rules_py_tools//:all` and it was a single static toolchain with `use_target_platform_constraints = True`. This only matches when exec == target, but Bazel needs one registered entry per exec platform so it can resolve `native_build_toolchain_type` for each build host. ## Fix Generate one `native_build_toolchain` entry per exec platform in `toolchains_repo` (`repo.bzl`), so they land in `@rules_py_tools//:all` which is already registered by all users. `native_build_toolchain_type` now resolves automatically on all supported exec platforms via the existing `register_toolchains("@rules_py_tools//:all")`. --------- Co-authored-by: Jason Bedard <jason+github@jbedard.ca>
1 parent 2ab00fe commit 4e64781

8 files changed

Lines changed: 195 additions & 16 deletions

File tree

e2e/MODULE.bazel

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,13 @@ use_repo(interpreters, "python_interpreters")
4747

4848
register_toolchains("@python_interpreters//:all")
4949

50+
# rules_py tools — provides the native_build_toolchain entries for sdist builds.
51+
tools = use_extension("@aspect_rules_py//py:extensions.bzl", "py_tools")
52+
tools.rules_py_tools()
53+
use_repo(tools, "rules_py_tools")
54+
55+
register_toolchains("@rules_py_tools//:all")
56+
5057
# For the OCI tests
5158
# {{{
5259
oci = use_extension("@rules_oci//oci:extensions.bzl", "oci")
@@ -345,4 +352,21 @@ uv.project(
345352
lock = "//cases/pytest-subdir-imports:uv.lock",
346353
pyproject = "//cases/pytest-subdir-imports:pyproject.toml",
347354
)
355+
356+
# For cases/uv-sdist-native-build
357+
# Verify that native_build_toolchain_type is registered via @rules_py_tools//:all
358+
# so that sdist packages with C extensions can be built with pep517_native_whl.
359+
# python-geohash is sdist-only and contains a C++ extension (_geohash).
360+
# {{{
361+
uv.project(
362+
default_build_dependencies = [
363+
"build",
364+
"setuptools",
365+
],
366+
hub_name = "pypi",
367+
lock = "//cases/uv-sdist-native-build:uv.lock",
368+
pyproject = "//cases/uv-sdist-native-build:pyproject.toml",
369+
)
370+
# }}}
371+
348372
use_repo(uv, "pypi")
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
# Regression test: python-geohash (sdist-only, C++ extension) must be buildable
2+
# when native_build_toolchain_type is registered via @rules_py_tools//:all.
3+
#
4+
# The pep517_native_whl rule requires the native_build_toolchain_type to resolve,
5+
# asserting that the exec and target platforms match. This test verifies that the
6+
# per-platform toolchain entries generated into @rules_py_tools are reachable.
7+
8+
load("@aspect_rules_py//py/unstable:defs.bzl", "py_venv_test")
9+
10+
py_venv_test(
11+
name = "test",
12+
srcs = ["test_geohash.py"],
13+
main = "test_geohash.py",
14+
target_compatible_with = ["@platforms//os:linux"],
15+
deps = ["@pypi//python_geohash"],
16+
)
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
[project]
2+
name = "uv-sdist-native-build"
3+
version = "0.0.0"
4+
requires-python = ">=3.11"
5+
dependencies = [
6+
"python-geohash",
7+
]
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
"""Verify that python-geohash (built from sdist via pep517_native_whl) is importable."""
2+
3+
import geohash
4+
5+
6+
def test_encode_decode():
7+
lat, lon = 37.7749, -122.4194
8+
encoded = geohash.encode(lat, lon)
9+
assert encoded, "geohash.encode should return a non-empty string"
10+
decoded = geohash.decode(encoded)
11+
assert len(decoded) == 2, "geohash.decode should return (lat, lon)"
12+
13+
14+
if __name__ == "__main__":
15+
test_encode_decode()
16+
print("OK")

e2e/cases/uv-sdist-native-build/uv.lock

Lines changed: 70 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

py/private/toolchain/BUILD.bazel

Lines changed: 5 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -28,31 +28,24 @@ toolchain_type(
2828
visibility = ["//visibility:public"],
2929
)
3030

31-
# Implementation detail of the target exec toolchain
31+
# Dummy toolchain backing target — no binary, used as a sentinel.
32+
# Public so the generated @rules_py_tools repo can reference it for
33+
# native_build toolchain entries.
3234
dummy_toolchain(
3335
name = "empty",
34-
visibility = ["//visibility:private"],
36+
visibility = ["//visibility:public"],
3537
)
3638

3739
toolchain_type(
3840
name = "native_build_toolchain_type",
41+
visibility = ["//visibility:public"],
3942
)
4043

4144
toolchain_type(
4245
name = "exec_tools_toolchain_type",
4346
visibility = ["//visibility:public"],
4447
)
4548

46-
# This creates a toolchain instance with exec_compatible_with placement
47-
# constraints matching the target, backed by the Python toolchain already
48-
# resolved for the target.
49-
toolchain(
50-
name = "native_build_toolchain",
51-
toolchain = ":empty",
52-
toolchain_type = "native_build_toolchain_type",
53-
use_target_platform_constraints = True,
54-
)
55-
5649
bzl_library(
5750
name = "autodetecting",
5851
srcs = ["autodetecting.bzl"],

py/private/toolchain/repo.bzl

Lines changed: 47 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,28 @@ toolchain(
6969
compatible_with = meta.compatible_with,
7070
)
7171

72+
# Generate one native_build toolchain entry per platform.
73+
# Used by pep517_whl (sdist builds) as a sentinel asserting the build is
74+
# running natively (exec == target). Both exec_compatible_with and
75+
# target_compatible_with are set to the same constraints so that this
76+
# toolchain is only selected when the exec and target platforms match —
77+
# cross-compilation sdist builds are unsupported and correctly fail.
78+
# Registered via @rules_py_tools//:all.
79+
for [platform, meta] in TOOLCHAIN_PLATFORMS.items():
80+
build_content += """
81+
toolchain(
82+
name = "native_build_{platform}_toolchain",
83+
exec_compatible_with = {compatible_with},
84+
target_compatible_with = {compatible_with},
85+
toolchain = "@aspect_rules_py//py/private/toolchain:empty",
86+
toolchain_type = "@aspect_rules_py//py/private/toolchain:native_build_toolchain_type",
87+
)
88+
89+
""".format(
90+
platform = platform,
91+
compatible_with = meta.compatible_with,
92+
)
93+
7294
# Base BUILD file for this repository
7395
repository_ctx.file("BUILD.bazel", build_content)
7496

@@ -87,14 +109,35 @@ toolchains_repo = repository_rule(
87109
)
88110

89111
def _prerelease_toolchains_repo_impl(repository_ctx):
90-
repository_ctx.file("BUILD.bazel", "# No toolchains created for pre-releases")
112+
# No tool toolchains in prerelease (no pre-built binaries), but we still
113+
# generate the native_build toolchain entries. These use the sentinel
114+
# @aspect_rules_py//py/private/toolchain:empty target which requires no
115+
# downloaded binary, so they work correctly in development/prerelease mode.
116+
build_content = "# No tool toolchains created for pre-releases\n"
117+
for [platform, meta] in TOOLCHAIN_PLATFORMS.items():
118+
build_content += """
119+
toolchain(
120+
name = "native_build_{platform}_toolchain",
121+
exec_compatible_with = {compatible_with},
122+
target_compatible_with = {compatible_with},
123+
toolchain = "@aspect_rules_py//py/private/toolchain:empty",
124+
toolchain_type = "@aspect_rules_py//py/private/toolchain:native_build_toolchain_type",
125+
)
126+
127+
""".format(
128+
platform = platform,
129+
compatible_with = meta.compatible_with,
130+
)
131+
repository_ctx.file("BUILD.bazel", build_content)
91132

92133
prerelease_toolchains_repo = repository_rule(
93134
_prerelease_toolchains_repo_impl,
94-
doc = """Create a repo with an empty BUILD file, which registers no toolchains.
95-
This is used for pre-releases, which have no pre-built binaries, but still want to call
135+
doc = """Create a repo with native_build toolchain entries but no tool toolchains.
136+
This is used for pre-releases, which have no pre-built tool binaries, but still want to call
96137
register_toolchains("@this_repo//:all")
97-
By doing this, we can avoid those register_toolchains callsites needing to be conditional on IS_PRERELEASE
138+
By doing this, we can avoid those register_toolchains callsites needing to be conditional on IS_PRERELEASE.
139+
The native_build toolchain entries are included because they reference only the sentinel :empty
140+
target (no downloaded binary required), so they work in dev mode too.
98141
""",
99142
)
100143

uv/private/pep517_whl/rule.bzl

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -159,6 +159,16 @@ constraints of the target platform.
159159
# Create an exec group which depends on a toolchain which can only be
160160
# resolved to exec_compatible_with constraints equal to the target. This
161161
# allows us to discover what those constraints need to be.
162+
#
163+
# NATIVE_BUILD_TOOLCHAIN has matching exec_compatible_with and
164+
# target_compatible_with, so this exec group only resolves when the exec
165+
# and target platforms match. Cross-compilation of sdists is intentionally
166+
# unsupported: PEP 517 build backends (setuptools, meson-python, etc.)
167+
# have no standard mechanism for cross-compilation, Python headers for
168+
# the target platform are not readily available, and output wheel tags
169+
# would need to encode the target platform with no upstream tooling
170+
# support. Packages that need cross-compiled native extensions should
171+
# publish pre-built wheels for their target platforms instead.
162172
"target": exec_group(
163173
toolchains = [
164174
PY_TOOLCHAIN,

0 commit comments

Comments
 (0)