From ff60856973deec5a93d863788eb5c12093c77eae Mon Sep 17 00:00:00 2001 From: Connor McEntee Date: Wed, 19 Aug 2026 12:21:55 -0600 Subject: [PATCH 1/4] feat(uv): parameterize the platform parent in gazelle_python_manifest The per-venv synthetic platform gazelle_python_manifest generates hardcoded parents = ["@platforms//host"], and the dep_group transition replaces --platforms wholesale. Anything building under the transitioned configuration therefore resolved toolchains against a bare os+cpu platform, which breaks builds with a custom --host_platform (hermetic cc toolchains gating on extra constraints fail sdist builds in the hub at analysis) and cross-compilation (wheel selection snaps back to the host platform, issue #1416). Add a platform_parents parameter, defaulting to the current behavior so existing callers are unaffected, and thread it into the synthetic platform's parents. The list must hold exactly one label (platform() accepts at most one parent); the macro fails eagerly with a message naming the parameter otherwise. The default is built as Label("@platforms//host") in the defining module, so callers need no direct bazel_dep on platforms to use it. A new e2e snapshot case builds a manifest under a caller-supplied parent and pins a probe transitioned through the macro-generated platform, selecting on a constraint only the custom parent carries. Fixes #1416 Co-Authored-By: Claude Fable 5 --- docs/uv.md | 7 ++ e2e/cases/BUILD.bazel | 13 ++++ .../gazelle-platform-parents-1416/BUILD.bazel | 65 +++++++++++++++++++ ..._platform_parents_1416.gazelle_python.yaml | 18 +++++ ...azelle_platform_parents_1416.probe_out.txt | 1 + uv/private/gazelle_manifest/defs.bzl | 31 +++++++-- 6 files changed, 131 insertions(+), 4 deletions(-) create mode 100644 e2e/cases/gazelle-platform-parents-1416/BUILD.bazel create mode 100644 e2e/cases/snapshots/gazelle_platform_parents_1416.gazelle_python.yaml create mode 100644 e2e/cases/snapshots/gazelle_platform_parents_1416.probe_out.txt diff --git a/docs/uv.md b/docs/uv.md index ac8b14f27..6de057026 100644 --- a/docs/uv.md +++ b/docs/uv.md @@ -421,6 +421,13 @@ gazelle_python_manifest( - `include_stub_packages` — Whether to index conventional stub distributions such as `types-requests` and `asyncpg-stubs`. The Gazelle Python extension then adds matching stub dependencies automatically. Defaults to `False`. +- `platform_parents` — Single-element list holding the parent platform for the synthetic + platforms this rule uses to pin each venv's dependency group (Bazel's `platform()` rule + accepts at most one parent). Defaults to `["@platforms//host"]`, which carries only bare + OS and CPU constraints. Set it when that isn't enough: pass your custom `--host_platform` + when hermetic C++ toolchains gate on extra constraints, so sdist builds in the hub can + still resolve a cc toolchain; or pass the target platform when cross-compiling, so wheel + selection follows it instead of snapping back to the host. - `venvs` — List of dependency group names whose wheels should be indexed. Module mappings from all listed dependency groups are merged into a single manifest. diff --git a/e2e/cases/BUILD.bazel b/e2e/cases/BUILD.bazel index f5ebd0dd9..bac80e1a4 100644 --- a/e2e/cases/BUILD.bazel +++ b/e2e/cases/BUILD.bazel @@ -241,5 +241,18 @@ write_source_files( # modules_mapping so generator drift (module additions, stub # synthesis, merge ordering) surfaces as a diff. "snapshots/uv_gazelle_778.gazelle_python.yaml": "//uv-gazelle-778:gazelle_python_manifest", + + # Gazelle manifest built by //gazelle-platform-parents-1416 with a + # caller-supplied `platform_parents` (issue #1416). Pins that the + # manifest builds end-to-end when the macro's synthetic per-venv + # platforms parent onto a custom platform rather than + # `@platforms//host`. + "snapshots/gazelle_platform_parents_1416.gazelle_python.yaml": "//gazelle-platform-parents-1416:gazelle_python_manifest", + # Companion probe: a genrule transitioned onto the macro-generated + # platform, selecting on a constraint only the custom parent carries. + # Pins "custom-parent": if the macro ignores `platform_parents` the + # constraint is dropped and the diff test fails, rather than the + # regression silently building. + "snapshots/gazelle_platform_parents_1416.probe_out.txt": "//gazelle-platform-parents-1416:probe", }, ) diff --git a/e2e/cases/gazelle-platform-parents-1416/BUILD.bazel b/e2e/cases/gazelle-platform-parents-1416/BUILD.bazel new file mode 100644 index 000000000..dd01c2665 --- /dev/null +++ b/e2e/cases/gazelle-platform-parents-1416/BUILD.bazel @@ -0,0 +1,65 @@ +load("@aspect_rules_py//uv:defs.bzl", "gazelle_python_manifest") +load("@bazel_lib//lib:transitions.bzl", "platform_transition_filegroup") + +# Regression test for `gazelle_python_manifest(platform_parents = ...)` +# (issue #1416: the macro's synthetic per-venv platforms were pinned to +# `@platforms//host`, breaking cross-compilation and custom host platforms). +# Reads from the //uv-deps-650 hub, which already declares an `extras` venv. +# +# Documented exception to per-case isolation: this test applies Gazelle to a +# venv from another case (//uv-deps-650), so the cross-case reference is by +# design. +package(default_visibility = ["//:__pkg__"]) + +# A case-local constraint that only `:parent_platform` carries: observing it +# downstream proves the macro-generated platform inherited from +# `:parent_platform`. +constraint_setting(name = "parentage") + +constraint_value( + name = "custom_parent", + constraint_setting = ":parentage", +) + +# Stands in for a user's custom `--host_platform` — e.g. one carrying the extra +# constraints a hermetic C++ toolchain gates on. Parenting it on +# `@platforms//host` keeps the case host-OS-agnostic. +platform( + name = "parent_platform", + constraint_values = [":custom_parent"], + parents = ["@platforms//host"], +) + +gazelle_python_manifest( + name = "gazelle_python_manifest", + hub = "pypi_uv_deps_650", + platform_parents = [":parent_platform"], + venvs = ["extras"], +) + +# The macro generates a synthetic platform named `_{name}_{hub}_{venv}` in +# this package; `:probe` depends on that naming scheme, so a rename inside the +# macro breaks this target with a missing-target analysis error. +# +# The snapshot pins "custom-parent": if the macro ignores `platform_parents`, +# the generated platform loses `:custom_parent`, the select falls through to +# "default-parent", and the snapshot diff test fails. +config_setting( + name = "under_custom_parent", + constraint_values = [":custom_parent"], +) + +genrule( + name = "probe_src", + outs = ["probe_out.txt"], + cmd = select({ + ":under_custom_parent": "echo custom-parent > $@", + "//conditions:default": "echo default-parent > $@", + }), +) + +platform_transition_filegroup( + name = "probe", + srcs = [":probe_src"], + target_platform = ":_gazelle_python_manifest_pypi_uv_deps_650_extras", +) diff --git a/e2e/cases/snapshots/gazelle_platform_parents_1416.gazelle_python.yaml b/e2e/cases/snapshots/gazelle_platform_parents_1416.gazelle_python.yaml new file mode 100644 index 000000000..e4d43618d --- /dev/null +++ b/e2e/cases/snapshots/gazelle_platform_parents_1416.gazelle_python.yaml @@ -0,0 +1,18 @@ +manifest: + modules_mapping: + brotli: brotli + brotlicffi: brotlicffi + build: build + certifi: certifi + cffi: cffi + charset_normalizer: charset_normalizer + colorama: colorama + idna: idna + packaging: packaging + pycparser: pycparser + pyproject_hooks: pyproject_hooks + requests: requests + setuptools: setuptools + urllib3: urllib3 + pip_repository: + name: pypi_uv_deps_650 diff --git a/e2e/cases/snapshots/gazelle_platform_parents_1416.probe_out.txt b/e2e/cases/snapshots/gazelle_platform_parents_1416.probe_out.txt new file mode 100644 index 000000000..a3838b087 --- /dev/null +++ b/e2e/cases/snapshots/gazelle_platform_parents_1416.probe_out.txt @@ -0,0 +1 @@ +custom-parent diff --git a/uv/private/gazelle_manifest/defs.bzl b/uv/private/gazelle_manifest/defs.bzl index 25d0727b9..3186a2f3b 100644 --- a/uv/private/gazelle_manifest/defs.bzl +++ b/uv/private/gazelle_manifest/defs.bzl @@ -70,7 +70,12 @@ _modules_mapping = rule( update = Label(":update.sh") -def gazelle_python_manifest(name, hub, venvs = [], include_stub_packages = False): +def gazelle_python_manifest( + name, + hub, + venvs = [], + include_stub_packages = False, + platform_parents = None): """Generates a Gazelle Python manifest from uv-managed wheels. Args: @@ -79,7 +84,27 @@ def gazelle_python_manifest(name, hub, venvs = [], include_stub_packages = False venvs: Dependency groups whose wheels should be indexed. include_stub_packages: Whether conventional stub distributions should be indexed for Gazelle's automatic stub dependency resolution. + platform_parents: Single-element list holding the parent platform for the + synthetic platforms this macro uses to select each venv's wheels. + (Bazel's `platform()` rule accepts at most one parent; the list shape + mirrors its `parents` attribute.) Defaults to + `[Label("@platforms//host")]`, resolved in rules_py's own repository — + you do not need a `bazel_dep` on `platforms` to use the default. The + host platform carries only OS and CPU constraints; point this at the + platform the wheels should be resolved for when that is not enough: + + - If the build sets a custom `--host_platform` (for example to carry + the constraints hermetic C++ toolchains require), pass that + platform here so sdist builds inside the hub can still resolve a + cc toolchain. + - When cross-compiling, pass the target platform so wheel selection + follows it instead of snapping back to the host. """ + if platform_parents == None: + platform_parents = [Label("@platforms//host")] + if len(platform_parents) != 1: + fail("gazelle_python_manifest: platform_parents must contain exactly one platform label (Bazel's platform() rule accepts at most one parent); got {}".format(platform_parents)) + file = "gazelle_python.yaml" hub = hub.lstrip("@") @@ -88,9 +113,7 @@ def gazelle_python_manifest(name, hub, venvs = [], include_stub_packages = False platform_name = "_{}_{}_{}".format(name, hub, venv) native.platform( name = platform_name, - parents = [ - "@platforms//host", - ], + parents = platform_parents, flags = [ "--@{}//dep_group={}".format(hub, venv), ], From cb106fd7f17e7d9f2323969675a67dee4096420d Mon Sep 17 00:00:00 2001 From: Cesar Abel Date: Wed, 19 Aug 2026 20:40:22 -0600 Subject: [PATCH 2/4] refactor(uv): move gazelle manifest tooling into a tools/ subpackage Moves generate.py and update.sh out of the package that holds the public defs.bzl into //uv/private/gazelle_manifest/tools with its own BUILD, and switches the _generator implicit attribute and the update label to absolute labels. Implicit-attribute visibility is checked against the .bzl's package, so the generator stays private to //uv/private/gazelle_manifest. --- .github/workflows/ci-workflows.yaml | 2 +- uv/private/gazelle_manifest/BUILD.bazel | 16 ---------------- uv/private/gazelle_manifest/defs.bzl | 4 ++-- uv/private/gazelle_manifest/tests/BUILD.bazel | 4 ++-- uv/private/gazelle_manifest/tools/BUILD.bazel | 17 +++++++++++++++++ .../gazelle_manifest/{ => tools}/generate.py | 0 .../gazelle_manifest/{ => tools}/update.sh | 0 7 files changed, 22 insertions(+), 21 deletions(-) create mode 100644 uv/private/gazelle_manifest/tools/BUILD.bazel rename uv/private/gazelle_manifest/{ => tools}/generate.py (100%) rename uv/private/gazelle_manifest/{ => tools}/update.sh (100%) diff --git a/.github/workflows/ci-workflows.yaml b/.github/workflows/ci-workflows.yaml index 24b5d4538..d2e003edb 100644 --- a/.github/workflows/ci-workflows.yaml +++ b/.github/workflows/ci-workflows.yaml @@ -54,7 +54,7 @@ jobs: py/tools/pex/main.py py/tools/site_merge/site_merge.py py/tools/unpack/unpack.py - uv/private/gazelle_manifest/generate.py + uv/private/gazelle_manifest/tools/generate.py uv/private/pep517_whl/tools/build_helper.py uv/private/pep517_whl/tools/memory_monitor.py uv/private/py_entrypoint_binary/search.py diff --git a/uv/private/gazelle_manifest/BUILD.bazel b/uv/private/gazelle_manifest/BUILD.bazel index 60909d932..2a1b37a0c 100644 --- a/uv/private/gazelle_manifest/BUILD.bazel +++ b/uv/private/gazelle_manifest/BUILD.bazel @@ -1,20 +1,4 @@ load("@bazel_lib//:bzl_library.bzl", "bzl_library") -load("//py:defs.bzl", "py_binary") - -exports_files(["update.sh"]) - -exports_files( - ["generate.py"], - visibility = ["//uv/private/gazelle_manifest/tests:__pkg__"], -) - -py_binary( - name = "generator", - srcs = [ - "generate.py", - ], - main = "generate.py", -) bzl_library( name = "defs", diff --git a/uv/private/gazelle_manifest/defs.bzl b/uv/private/gazelle_manifest/defs.bzl index 3186a2f3b..3bc1502b6 100644 --- a/uv/private/gazelle_manifest/defs.bzl +++ b/uv/private/gazelle_manifest/defs.bzl @@ -61,14 +61,14 @@ _modules_mapping = rule( "hub": attr.string(), "include_stub_packages": attr.bool(), "_generator": attr.label( - default = Label(":generator"), + default = Label("//uv/private/gazelle_manifest/tools:generator"), executable = True, cfg = "exec", ), }, ) -update = Label(":update.sh") +update = Label("//uv/private/gazelle_manifest/tools:update.sh") def gazelle_python_manifest( name, diff --git a/uv/private/gazelle_manifest/tests/BUILD.bazel b/uv/private/gazelle_manifest/tests/BUILD.bazel index 8ad960c48..446a28a60 100644 --- a/uv/private/gazelle_manifest/tests/BUILD.bazel +++ b/uv/private/gazelle_manifest/tests/BUILD.bazel @@ -7,11 +7,11 @@ py_test( srcs = [ "__test__.py", "test.py", - "//uv/private/gazelle_manifest:generate.py", + "//uv/private/gazelle_manifest/tools:generate.py", ], args = ["$(location :test.py)"], data = ["test.py"], - imports = [".."], + imports = ["../tools"], main = "__test__.py", deps = [ "@pypi//pytest", diff --git a/uv/private/gazelle_manifest/tools/BUILD.bazel b/uv/private/gazelle_manifest/tools/BUILD.bazel new file mode 100644 index 000000000..968e38eca --- /dev/null +++ b/uv/private/gazelle_manifest/tools/BUILD.bazel @@ -0,0 +1,17 @@ +load("//py:defs.bzl", "py_binary") + +exports_files(["update.sh"]) + +exports_files( + ["generate.py"], + visibility = ["//uv/private/gazelle_manifest/tests:__pkg__"], +) + +py_binary( + name = "generator", + srcs = [ + "generate.py", + ], + main = "generate.py", + visibility = ["//uv/private/gazelle_manifest:__pkg__"], +) diff --git a/uv/private/gazelle_manifest/generate.py b/uv/private/gazelle_manifest/tools/generate.py similarity index 100% rename from uv/private/gazelle_manifest/generate.py rename to uv/private/gazelle_manifest/tools/generate.py diff --git a/uv/private/gazelle_manifest/update.sh b/uv/private/gazelle_manifest/tools/update.sh similarity index 100% rename from uv/private/gazelle_manifest/update.sh rename to uv/private/gazelle_manifest/tools/update.sh From 7414b58d81468fec194f086fcea7b5f84fb33271 Mon Sep 17 00:00:00 2001 From: Cesar Abel Date: Wed, 19 Aug 2026 20:43:41 -0600 Subject: [PATCH 3/4] refactor(uv): make gazelle_python_manifest platform_parent a single label Replaces the platform_parents single-element-list parameter (not yet released) with a singular platform_parent label before the API ships. A list that fails on any length other than one promises plurality the underlying platform() rule does not support. The load-time check now rejects any non-string, non-Label value (lists, selects) with a message naming the parameter, instead of dying inside len() or native.platform. Also documents the cross-compilation trade-off: with platform_parent set to a target platform, .update builds sdist fallbacks for that platform, which needs a capable execution platform (e.g. RBE) unless everything resolves to wheels. --- docs/uv.md | 10 ++++--- .../gazelle-platform-parents-1416/BUILD.bazel | 6 ++--- uv/private/gazelle_manifest/defs.bzl | 26 ++++++++++--------- 3 files changed, 23 insertions(+), 19 deletions(-) diff --git a/docs/uv.md b/docs/uv.md index 6de057026..c93981783 100644 --- a/docs/uv.md +++ b/docs/uv.md @@ -421,13 +421,15 @@ gazelle_python_manifest( - `include_stub_packages` — Whether to index conventional stub distributions such as `types-requests` and `asyncpg-stubs`. The Gazelle Python extension then adds matching stub dependencies automatically. Defaults to `False`. -- `platform_parents` — Single-element list holding the parent platform for the synthetic - platforms this rule uses to pin each venv's dependency group (Bazel's `platform()` rule - accepts at most one parent). Defaults to `["@platforms//host"]`, which carries only bare +- `platform_parent` — Parent platform for the synthetic platforms this rule uses to pin + each venv's dependency group. Defaults to `@platforms//host`, which carries only bare OS and CPU constraints. Set it when that isn't enough: pass your custom `--host_platform` when hermetic C++ toolchains gate on extra constraints, so sdist builds in the hub can still resolve a cc toolchain; or pass the target platform when cross-compiling, so wheel - selection follows it instead of snapping back to the host. + selection follows it instead of snapping back to the host. In the cross-compilation case, + `bazel run .update` builds any sdist fallbacks *for that platform* — that needs an + execution platform able to run those builds (e.g. remote execution) unless every indexed + package resolves to a wheel. - `venvs` — List of dependency group names whose wheels should be indexed. Module mappings from all listed dependency groups are merged into a single manifest. diff --git a/e2e/cases/gazelle-platform-parents-1416/BUILD.bazel b/e2e/cases/gazelle-platform-parents-1416/BUILD.bazel index dd01c2665..b2f07e6d9 100644 --- a/e2e/cases/gazelle-platform-parents-1416/BUILD.bazel +++ b/e2e/cases/gazelle-platform-parents-1416/BUILD.bazel @@ -1,7 +1,7 @@ load("@aspect_rules_py//uv:defs.bzl", "gazelle_python_manifest") load("@bazel_lib//lib:transitions.bzl", "platform_transition_filegroup") -# Regression test for `gazelle_python_manifest(platform_parents = ...)` +# Regression test for `gazelle_python_manifest(platform_parent = ...)` # (issue #1416: the macro's synthetic per-venv platforms were pinned to # `@platforms//host`, breaking cross-compilation and custom host platforms). # Reads from the //uv-deps-650 hub, which already declares an `extras` venv. @@ -33,7 +33,7 @@ platform( gazelle_python_manifest( name = "gazelle_python_manifest", hub = "pypi_uv_deps_650", - platform_parents = [":parent_platform"], + platform_parent = ":parent_platform", venvs = ["extras"], ) @@ -41,7 +41,7 @@ gazelle_python_manifest( # this package; `:probe` depends on that naming scheme, so a rename inside the # macro breaks this target with a missing-target analysis error. # -# The snapshot pins "custom-parent": if the macro ignores `platform_parents`, +# The snapshot pins "custom-parent": if the macro ignores `platform_parent`, # the generated platform loses `:custom_parent`, the select falls through to # "default-parent", and the snapshot diff test fails. config_setting( diff --git a/uv/private/gazelle_manifest/defs.bzl b/uv/private/gazelle_manifest/defs.bzl index 3bc1502b6..3a2c7224d 100644 --- a/uv/private/gazelle_manifest/defs.bzl +++ b/uv/private/gazelle_manifest/defs.bzl @@ -75,7 +75,7 @@ def gazelle_python_manifest( hub, venvs = [], include_stub_packages = False, - platform_parents = None): + platform_parent = None): """Generates a Gazelle Python manifest from uv-managed wheels. Args: @@ -84,11 +84,9 @@ def gazelle_python_manifest( venvs: Dependency groups whose wheels should be indexed. include_stub_packages: Whether conventional stub distributions should be indexed for Gazelle's automatic stub dependency resolution. - platform_parents: Single-element list holding the parent platform for the - synthetic platforms this macro uses to select each venv's wheels. - (Bazel's `platform()` rule accepts at most one parent; the list shape - mirrors its `parents` attribute.) Defaults to - `[Label("@platforms//host")]`, resolved in rules_py's own repository — + platform_parent: Parent platform for the synthetic platforms this macro + uses to select each venv's wheels. Defaults to + `Label("@platforms//host")`, resolved in rules_py's own repository — you do not need a `bazel_dep` on `platforms` to use the default. The host platform carries only OS and CPU constraints; point this at the platform the wheels should be resolved for when that is not enough: @@ -98,12 +96,16 @@ def gazelle_python_manifest( platform here so sdist builds inside the hub can still resolve a cc toolchain. - When cross-compiling, pass the target platform so wheel selection - follows it instead of snapping back to the host. + follows it instead of snapping back to the host. Note this makes + `bazel run .update` build any sdist fallbacks *for that + platform*, which requires an execution platform able to run the + build (e.g. remote execution) unless every indexed package + resolves to a wheel. """ - if platform_parents == None: - platform_parents = [Label("@platforms//host")] - if len(platform_parents) != 1: - fail("gazelle_python_manifest: platform_parents must contain exactly one platform label (Bazel's platform() rule accepts at most one parent); got {}".format(platform_parents)) + if platform_parent == None: + platform_parent = Label("@platforms//host") + if type(platform_parent) not in ("string", "Label"): + fail("gazelle_python_manifest: platform_parent takes a single platform label (Bazel's platform() rule accepts at most one parent); got {} of type {}".format(platform_parent, type(platform_parent))) file = "gazelle_python.yaml" hub = hub.lstrip("@") @@ -113,7 +115,7 @@ def gazelle_python_manifest( platform_name = "_{}_{}_{}".format(name, hub, venv) native.platform( name = platform_name, - parents = platform_parents, + parents = [platform_parent], flags = [ "--@{}//dep_group={}".format(hub, venv), ], From 201d27afaaa3c64ea63d2194e17cba8e6ef5248e Mon Sep 17 00:00:00 2001 From: Cesar Abel Date: Wed, 19 Aug 2026 20:52:44 -0600 Subject: [PATCH 4/4] test(uv): pin wheel selection under the cross-compiled platform_parent MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Strengthens the gazelle-platform-parents-1416 case to cover the behavior issue #1416 actually reported — wheel selection following the caller's platform — not just constraint inheritance: - parent_platform is now a fully-specified linux_x86_64 platform carrying the platform_libc/platform_version flags, making the case the real cross-compilation scenario and host-invariant. The flags ride on the platform because their defaults are host-detected: without them manylinux config_settings never match off-linux and native packages silently degrade to sdist fallbacks. - A new analysis-time probe snapshots which wheel variant each hub dependency resolves to under the macro-generated platform (distribution: platform-class lines, no versions or tag spellings, so lock bumps don't churn the golden). Native packages pin to linux_x86_64, pure ones to any, and any sdist fallback shows up explicitly. Verified by simulating the regression: the golden flips to macosx wheels on a darwin host. - The manifest YAML golden is replaced by a build_test: the YAML content is platform-independent, so the golden only re-pinned another case's hub state; building the manifest end-to-end from the linux-selected wheels is the actual signal. --- e2e/cases/BUILD.bazel | 16 +++--- .../gazelle-platform-parents-1416/BUILD.bazel | 54 +++++++++++++++++-- .../wheel_selection_probe.bzl | 48 +++++++++++++++++ ..._platform_parents_1416.gazelle_python.yaml | 18 ------- ..._platform_parents_1416.wheel_selection.txt | 14 +++++ 5 files changed, 119 insertions(+), 31 deletions(-) create mode 100644 e2e/cases/gazelle-platform-parents-1416/wheel_selection_probe.bzl delete mode 100644 e2e/cases/snapshots/gazelle_platform_parents_1416.gazelle_python.yaml create mode 100644 e2e/cases/snapshots/gazelle_platform_parents_1416.wheel_selection.txt diff --git a/e2e/cases/BUILD.bazel b/e2e/cases/BUILD.bazel index bac80e1a4..cac9b199d 100644 --- a/e2e/cases/BUILD.bazel +++ b/e2e/cases/BUILD.bazel @@ -242,17 +242,17 @@ write_source_files( # synthesis, merge ordering) surfaces as a diff. "snapshots/uv_gazelle_778.gazelle_python.yaml": "//uv-gazelle-778:gazelle_python_manifest", - # Gazelle manifest built by //gazelle-platform-parents-1416 with a - # caller-supplied `platform_parents` (issue #1416). Pins that the - # manifest builds end-to-end when the macro's synthetic per-venv - # platforms parent onto a custom platform rather than - # `@platforms//host`. - "snapshots/gazelle_platform_parents_1416.gazelle_python.yaml": "//gazelle-platform-parents-1416:gazelle_python_manifest", - # Companion probe: a genrule transitioned onto the macro-generated + # Probes for `gazelle_python_manifest(platform_parent = ...)` + # (issue #1416); the manifest build itself is covered by the case's + # build_test. First, a genrule transitioned onto the macro-generated # platform, selecting on a constraint only the custom parent carries. - # Pins "custom-parent": if the macro ignores `platform_parents` the + # Pins "custom-parent": if the macro ignores `platform_parent` the # constraint is dropped and the diff test fails, rather than the # regression silently building. "snapshots/gazelle_platform_parents_1416.probe_out.txt": "//gazelle-platform-parents-1416:probe", + # Second, the wheel-selection probe: pins that each dependency in the + # transitioned index resolves to a linux_x86_64 (or pure) wheel, + # matching the case's cross-compilation parent regardless of host. + "snapshots/gazelle_platform_parents_1416.wheel_selection.txt": "//gazelle-platform-parents-1416:wheel_selection", }, ) diff --git a/e2e/cases/gazelle-platform-parents-1416/BUILD.bazel b/e2e/cases/gazelle-platform-parents-1416/BUILD.bazel index b2f07e6d9..30fc21b40 100644 --- a/e2e/cases/gazelle-platform-parents-1416/BUILD.bazel +++ b/e2e/cases/gazelle-platform-parents-1416/BUILD.bazel @@ -1,5 +1,7 @@ load("@aspect_rules_py//uv:defs.bzl", "gazelle_python_manifest") load("@bazel_lib//lib:transitions.bzl", "platform_transition_filegroup") +load("@bazel_skylib//rules:build_test.bzl", "build_test") +load(":wheel_selection_probe.bzl", "wheel_selection_probe") # Regression test for `gazelle_python_manifest(platform_parent = ...)` # (issue #1416: the macro's synthetic per-venv platforms were pinned to @@ -21,13 +23,26 @@ constraint_value( constraint_setting = ":parentage", ) -# Stands in for a user's custom `--host_platform` — e.g. one carrying the extra -# constraints a hermetic C++ toolchain gates on. Parenting it on -# `@platforms//host` keeps the case host-OS-agnostic. +# Stands in for a cross-compilation target platform (issue #1416's scenario): +# fully specified rather than parented on `@platforms//host`, so wheel +# selection under the macro's synthetic platform resolves identically on +# every host and the goldens below are host-invariant. The libc flags ride on +# the platform because their defaults are detected from the host: without +# them, manylinux config_settings never match on a non-linux host and every +# native package silently degrades to its sdist fallback — the same config +# chimera the issue describes. This doubles as the documented recipe for +# real cross-compilation parents. platform( name = "parent_platform", - constraint_values = [":custom_parent"], - parents = ["@platforms//host"], + constraint_values = [ + ":custom_parent", + "@platforms//cpu:x86_64", + "@platforms//os:linux", + ], + flags = [ + "--@aspect_rules_py//uv/private/constraints/platform:platform_libc=glibc", + "--@aspect_rules_py//uv/private/constraints/platform:platform_version=2.28", + ], ) gazelle_python_manifest( @@ -37,6 +52,16 @@ gazelle_python_manifest( venvs = ["extras"], ) +# The manifest YAML content itself is platform-independent (same module +# mapping regardless of which wheel variant was indexed), so a golden on it +# would only re-pin the hub's dependency set. Building it end-to-end is the +# signal: the generator action consumes wheels selected for the linux parent +# on whatever host runs the test. +build_test( + name = "manifest_build_test", + targets = [":gazelle_python_manifest"], +) + # The macro generates a synthetic platform named `_{name}_{hub}_{venv}` in # this package; `:probe` depends on that naming scheme, so a rename inside the # macro breaks this target with a missing-target analysis error. @@ -63,3 +88,22 @@ platform_transition_filegroup( srcs = [":probe_src"], target_platform = ":_gazelle_python_manifest_pypi_uv_deps_650_extras", ) + +# Second probe, at the level the bug actually manifested (#1416): pins that +# wheel *selection* follows the parent. Transitions the hub's index filegroup +# through the same macro-generated platform and snapshots which wheel variant +# each dependency resolved to — native packages must come out `linux_x86_64`, +# never the host's, and never an sdist fallback. The constraint probe above +# discriminates on any host; this one additionally fails if selection snaps +# back to the host on non-linux-x86_64 hosts, or if a lock change quietly +# drops a dependency to an sdist fallback for the cross-compiled platform. +platform_transition_filegroup( + name = "selected_whls", + srcs = ["@pypi_uv_deps_650//:gazelle_index_whls"], + target_platform = ":_gazelle_python_manifest_pypi_uv_deps_650_extras", +) + +wheel_selection_probe( + name = "wheel_selection", + src = ":selected_whls", +) diff --git a/e2e/cases/gazelle-platform-parents-1416/wheel_selection_probe.bzl b/e2e/cases/gazelle-platform-parents-1416/wheel_selection_probe.bzl new file mode 100644 index 000000000..708db937a --- /dev/null +++ b/e2e/cases/gazelle-platform-parents-1416/wheel_selection_probe.bzl @@ -0,0 +1,48 @@ +"""Analysis-time probe over a hub's `gazelle_index_whls` filegroup. + +Writes one `distribution: platform-class` line per resolved wheel, so a +snapshot can pin *which kind* of wheel the configuration selected (e.g. +`cffi: linux_x86_64`) without materializing the wheels and without coupling +the golden to exact versions or manylinux tag spellings, both of which churn +on lock updates. Classification happens at analysis time from basenames, so +the probe never triggers sdist builds for the transitioned platform. +""" + +def _classify_platform_tag(basename): + tag = basename[:-len(".whl")].split("-")[-1] + if tag == "any": + return "any" + os = "unknown-os" + if "linux" in tag: + os = "linux" + elif "macosx" in tag: + os = "macosx" + elif "win" in tag: + os = "windows" + cpu = "unknown-cpu" + if "universal2" in tag: + cpu = "universal2" + elif "x86_64" in tag or "amd64" in tag: + cpu = "x86_64" + elif "aarch64" in tag or "arm64" in tag: + cpu = "aarch64" + return "{}_{}".format(os, cpu) + +def _wheel_selection_probe_impl(ctx): + out = ctx.actions.declare_file(ctx.label.name + ".txt") + lines = [] + for f in ctx.attr.src[DefaultInfo].files.to_list(): + if f.basename.endswith(".whl"): + dist = f.basename.split("-")[0].lower() + lines.append("{}: {}".format(dist, _classify_platform_tag(f.basename))) + elif f.basename == "whl": + lines.append("{}: sdist-fallback".format(f.owner.repo_name)) + ctx.actions.write(out, "\n".join(sorted(lines)) + "\n") + return [DefaultInfo(files = depset([out]))] + +wheel_selection_probe = rule( + implementation = _wheel_selection_probe_impl, + attrs = { + "src": attr.label(providers = [DefaultInfo]), + }, +) diff --git a/e2e/cases/snapshots/gazelle_platform_parents_1416.gazelle_python.yaml b/e2e/cases/snapshots/gazelle_platform_parents_1416.gazelle_python.yaml deleted file mode 100644 index e4d43618d..000000000 --- a/e2e/cases/snapshots/gazelle_platform_parents_1416.gazelle_python.yaml +++ /dev/null @@ -1,18 +0,0 @@ -manifest: - modules_mapping: - brotli: brotli - brotlicffi: brotlicffi - build: build - certifi: certifi - cffi: cffi - charset_normalizer: charset_normalizer - colorama: colorama - idna: idna - packaging: packaging - pycparser: pycparser - pyproject_hooks: pyproject_hooks - requests: requests - setuptools: setuptools - urllib3: urllib3 - pip_repository: - name: pypi_uv_deps_650 diff --git a/e2e/cases/snapshots/gazelle_platform_parents_1416.wheel_selection.txt b/e2e/cases/snapshots/gazelle_platform_parents_1416.wheel_selection.txt new file mode 100644 index 000000000..af6ecaaff --- /dev/null +++ b/e2e/cases/snapshots/gazelle_platform_parents_1416.wheel_selection.txt @@ -0,0 +1,14 @@ +brotli: linux_x86_64 +brotlicffi: linux_x86_64 +build: any +certifi: any +cffi: linux_x86_64 +charset_normalizer: linux_x86_64 +colorama: any +idna: any +packaging: any +pycparser: any +pyproject_hooks: any +requests: any +setuptools: any +urllib3: any