From 48d841a14d609e6a88d7d39458c26b7a112fe0ea Mon Sep 17 00:00:00 2001 From: Connor McEntee Date: Sun, 5 Jul 2026 16:43:54 +0000 Subject: [PATCH 1/4] test(uv): failing regression for native sdist workspace-relative env paths MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Builds a native sdist whose C extension consumes an in-repo cc_library via `override_package(env=/toolchains=)`-shaped make-vars: CPPFLAGS = `-I$(DEP_INC)` and LDFLAGS = `$(DEP_LIB_A)`. Both expand to Bazel workspace-relative paths, which resolve from the action execroot but not from the PEP 517 backend's cwd inside the unpacked sdist worktree. This commit is intentionally RED — it documents the bug. The compile fails with: mod.c: fatal error: 'dep.h' file not found cc ... -Iuv/private/pep517_whl/tests/native_dep -c mod.c The fix follows. --- .../pep517_whl/tests/native_dep/BUILD.bazel | 66 +++++++++++++++++++ .../pep517_whl/tests/native_dep/defs.bzl | 37 +++++++++++ uv/private/pep517_whl/tests/native_dep/dep.c | 3 + uv/private/pep517_whl/tests/native_dep/dep.h | 6 ++ .../tests/native_dep/sdist/backend.py | 64 ++++++++++++++++++ .../pep517_whl/tests/native_dep/sdist/mod.c | 3 + .../tests/native_dep/sdist/pyproject.toml | 8 +++ 7 files changed, 187 insertions(+) create mode 100644 uv/private/pep517_whl/tests/native_dep/BUILD.bazel create mode 100644 uv/private/pep517_whl/tests/native_dep/defs.bzl create mode 100644 uv/private/pep517_whl/tests/native_dep/dep.c create mode 100644 uv/private/pep517_whl/tests/native_dep/dep.h create mode 100644 uv/private/pep517_whl/tests/native_dep/sdist/backend.py create mode 100644 uv/private/pep517_whl/tests/native_dep/sdist/mod.c create mode 100644 uv/private/pep517_whl/tests/native_dep/sdist/pyproject.toml diff --git a/uv/private/pep517_whl/tests/native_dep/BUILD.bazel b/uv/private/pep517_whl/tests/native_dep/BUILD.bazel new file mode 100644 index 000000000..36063fcdf --- /dev/null +++ b/uv/private/pep517_whl/tests/native_dep/BUILD.bazel @@ -0,0 +1,66 @@ +# Regression: a native sdist consuming an in-repo cc_library via +# override_package(env=/toolchains=) must build. The dep's include dir and +# static archive reach the build as workspace-relative paths, which resolve +# from the execroot but not the PEP 517 backend's worktree cwd. +load("@bazel_skylib//rules:build_test.bzl", "build_test") +load("@rules_cc//cc:defs.bzl", "cc_library") +load("@tar.bzl//tar:mtree.bzl", "mtree_mutate", "mtree_spec") +load("@tar.bzl//tar:tar.bzl", "tar_rule") +load("//uv/private/pep517_whl:rule.bzl", "pep517_native_whl") +load(":defs.bzl", "dep_makevars") + +cc_library( + name = "dep", + testonly = True, + srcs = ["dep.c"], + hdrs = ["dep.h"], +) + +dep_makevars( + name = "dep_makevars", + testonly = True, + hdr = "dep.h", + lib = ":dep", +) + +_SDIST_SRCS = glob(["sdist/**"]) + +mtree_spec( + name = "sdist_mtree_raw", + testonly = True, + srcs = _SDIST_SRCS, + include_runfiles = False, +) + +mtree_mutate( + name = "sdist_mtree", + testonly = True, + mtree = ":sdist_mtree_raw", + strip_prefix = package_name(), +) + +tar_rule( + name = "sdist", + testonly = True, + srcs = _SDIST_SRCS, + mtree = ":sdist_mtree", +) + +pep517_native_whl( + name = "whl", + testonly = True, + src = ":sdist", + env = { + "CPPFLAGS": "-I$(DEP_INC)", + "LDFLAGS": "$(DEP_LIB_A)", + }, + tags = ["manual"], + tool = "//uv/private/pep517_whl:__build_helper", + toolchains = [":dep_makevars"], + version = "0.0.1", +) + +build_test( + name = "native_dep_env_test", + targets = [":whl"], +) diff --git a/uv/private/pep517_whl/tests/native_dep/defs.bzl b/uv/private/pep517_whl/tests/native_dep/defs.bzl new file mode 100644 index 000000000..14d0fabce --- /dev/null +++ b/uv/private/pep517_whl/tests/native_dep/defs.bzl @@ -0,0 +1,37 @@ +"""Test shim: emulate `uv.override_package(toolchains=, env=)` by surfacing a +cc_library's include dir + static archive as make-vars.""" + +load("@rules_cc//cc/common:cc_info.bzl", "CcInfo") + +def _dep_makevars_impl(ctx): + cc_info = ctx.attr.lib[CcInfo] + archives = [] + for linker_input in cc_info.linking_context.linker_inputs.to_list(): + for lib in linker_input.libraries: + archive = lib.static_library or lib.pic_static_library + if archive: + archives.append(archive) + if not archives: + fail("cc_library {} produced no static archive".format(ctx.attr.lib.label)) + archive = archives[0] + header = ctx.file.hdr + + return [ + # header + archive must be staged as build-action inputs; + # pep517_native_whl stages each toolchain dep's DefaultInfo.files. + DefaultInfo(files = depset([header, archive])), + platform_common.TemplateVariableInfo({ + # Deliberately workspace-relative (execroot-valid, worktree-invalid): + # the input under test. + "DEP_INC": header.dirname, + "DEP_LIB_A": archive.path, + }), + ] + +dep_makevars = rule( + implementation = _dep_makevars_impl, + attrs = { + "lib": attr.label(providers = [CcInfo], mandatory = True), + "hdr": attr.label(allow_single_file = True, mandatory = True), + }, +) diff --git a/uv/private/pep517_whl/tests/native_dep/dep.c b/uv/private/pep517_whl/tests/native_dep/dep.c new file mode 100644 index 000000000..b2cd3c71c --- /dev/null +++ b/uv/private/pep517_whl/tests/native_dep/dep.c @@ -0,0 +1,3 @@ +#include "dep.h" + +int dep_answer(void) { return 42; } diff --git a/uv/private/pep517_whl/tests/native_dep/dep.h b/uv/private/pep517_whl/tests/native_dep/dep.h new file mode 100644 index 000000000..7e45bda56 --- /dev/null +++ b/uv/private/pep517_whl/tests/native_dep/dep.h @@ -0,0 +1,6 @@ +#ifndef ASPECT_RULES_PY_TEST_DEP_H +#define ASPECT_RULES_PY_TEST_DEP_H + +int dep_answer(void); + +#endif diff --git a/uv/private/pep517_whl/tests/native_dep/sdist/backend.py b/uv/private/pep517_whl/tests/native_dep/sdist/backend.py new file mode 100644 index 000000000..e63b306ef --- /dev/null +++ b/uv/private/pep517_whl/tests/native_dep/sdist/backend.py @@ -0,0 +1,64 @@ +"""PEP 517 backend for the regression test: compiles mod.c with the +env-supplied CC/CPPFLAGS/LDFLAGS, so the build succeeds only if those +workspace-relative flag paths resolve from this backend's worktree cwd.""" + +from __future__ import annotations + +import base64 +import csv +import hashlib +import io +import os +import shlex +import subprocess +from pathlib import Path +from zipfile import ZIP_DEFLATED, ZipFile + +_DIST_INFO = "native_dep_ext-0.0.1.dist-info" +_EXT = "native_dep_ext.so" + + +def _compile() -> None: + cc = shlex.split(os.environ.get("CC") or "cc") + cppflags = shlex.split(os.environ.get("CPPFLAGS", "")) + ldflags = shlex.split(os.environ.get("LDFLAGS", "")) + subprocess.run([*cc, "-fPIC", *cppflags, "-c", "mod.c", "-o", "mod.o"], check=True) + subprocess.run([*cc, "-shared", "mod.o", *ldflags, "-o", _EXT], check=True) + + +def get_requires_for_build_wheel(config_settings=None): + del config_settings + return [] + + +def build_wheel(wheel_directory, config_settings=None, metadata_directory=None): + del config_settings, metadata_directory + _compile() + + files = { + _EXT: Path(_EXT).read_bytes(), + f"{_DIST_INFO}/METADATA": ( + "Metadata-Version: 2.1\nName: native-dep-ext\nVersion: 0.0.1\n" + ).encode(), + f"{_DIST_INFO}/WHEEL": ( + "Wheel-Version: 1.0\n" + "Generator: rules_py native-dep test backend\n" + "Root-Is-Purelib: false\n" + "Tag: py3-none-linux_x86_64\n" + ).encode(), + } + + rows: list[list[str]] = [] + for name, content in files.items(): + digest = base64.urlsafe_b64encode(hashlib.sha256(content).digest()).rstrip(b"=") + rows.append([name, "sha256=" + digest.decode(), str(len(content))]) + rows.append([f"{_DIST_INFO}/RECORD", "", ""]) + record = io.StringIO() + csv.writer(record, lineterminator="\n").writerows(rows) + files[f"{_DIST_INFO}/RECORD"] = record.getvalue().encode() + + wheel_name = "native_dep_ext-0.0.1-py3-none-linux_x86_64.whl" + with ZipFile(Path(wheel_directory, wheel_name), "w", ZIP_DEFLATED) as wheel: + for name, content in files.items(): + wheel.writestr(name, content) + return wheel_name diff --git a/uv/private/pep517_whl/tests/native_dep/sdist/mod.c b/uv/private/pep517_whl/tests/native_dep/sdist/mod.c new file mode 100644 index 000000000..dba1ab6de --- /dev/null +++ b/uv/private/pep517_whl/tests/native_dep/sdist/mod.c @@ -0,0 +1,3 @@ +#include "dep.h" + +int mod_value(void) { return dep_answer(); } diff --git a/uv/private/pep517_whl/tests/native_dep/sdist/pyproject.toml b/uv/private/pep517_whl/tests/native_dep/sdist/pyproject.toml new file mode 100644 index 000000000..1ff07ac36 --- /dev/null +++ b/uv/private/pep517_whl/tests/native_dep/sdist/pyproject.toml @@ -0,0 +1,8 @@ +[build-system] +requires = [] +build-backend = "backend" +backend-path = ["."] + +[project] +name = "native_dep_ext" +version = "0.0.1" From f5161a9c59afa8f790c242fcf64d1ea19eb9f5c0 Mon Sep 17 00:00:00 2001 From: Connor McEntee Date: Sun, 5 Jul 2026 21:39:37 +0000 Subject: [PATCH 2/4] fix(uv): add $(EXECROOT) make-var to anchor env paths across the backend chdir MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `override_package(env = {...})` values expanded from make-vars are Bazel workspace-relative; they resolve from the action execroot but not from the PEP 517 backend's cwd, which `python -m build` changes into the unpacked sdist worktree. `$(EXECROOT)` expands to a marker that build_helper replaces with the absolute execroot at build time, so `-I$(EXECROOT)/$(DEP_INC)` is anchored deterministically — no path detection, no per-var-name list, and it applies uniformly to any env var (e.g. JAVA_HOME). Fixes the regression from the previous commit. --- uv/private/extension/defs.bzl | 2 +- uv/private/pep517_whl/build_helper.py | 10 +++++++-- uv/private/pep517_whl/rule.bzl | 21 ++++++++++++++++++- .../pep517_whl/tests/native_dep/BUILD.bazel | 4 ++-- 4 files changed, 31 insertions(+), 6 deletions(-) diff --git a/uv/private/extension/defs.bzl b/uv/private/extension/defs.bzl index 3bcfac550..fc515f103 100644 --- a/uv/private/extension/defs.bzl +++ b/uv/private/extension/defs.bzl @@ -888,7 +888,7 @@ _override_package_tag = tag_class( ), "env": attr.string_dict( default = {}, - doc = "Extra environment variables merged into the build action's `env` dict. Values may reference $(VAR) make-variables sourced from the default CC toolchain or any extra `toolchains` listed above.", + doc = "Extra environment variables merged into the build action's `env` dict. Values may reference $(VAR) make-variables sourced from the default CC toolchain or any extra `toolchains` listed above. Prefix a workspace-relative path with `$(EXECROOT)/` (e.g. `-I$(EXECROOT)/$(DEP_INC)`) to anchor it to an absolute path so it survives the build backend's chdir into the sdist worktree.", ), # Pre-build patches: applied to extracted sdist source before wheel build. diff --git a/uv/private/pep517_whl/build_helper.py b/uv/private/pep517_whl/build_helper.py index 17644aa73..c75888778 100644 --- a/uv/private/pep517_whl/build_helper.py +++ b/uv/private/pep517_whl/build_helper.py @@ -98,7 +98,7 @@ def _override_tool(env, key, wrapper): env[key] = shlex.join(parts) -def _compiler_env(tmpdir): +def _compiler_env(tmpdir, execroot_marker=None): env = dict(os.environ) # The helper's launcher exports RUNFILES_DIR, RUNFILES_MANIFEST_FILE, and # JAVA_RUNFILES: @@ -149,6 +149,11 @@ def _compiler_env(tmpdir): ("LDCXXSHARED", cxx), ]: _override_tool(env, key, wrapper) + + # Anchor `$(EXECROOT)` paths: our cwd is the execroot (the backend chdir happens later, in a subprocess). + if execroot_marker: + root = os.getcwd() + env = {key: value.replace(execroot_marker, root) for key, value in env.items()} return env @@ -205,6 +210,7 @@ def _legacy_metadata_conflicts_with_pyproject(worktree): PARSER.add_argument("--validate-anyarch", action="store_true") PARSER.add_argument("--patch-strip", type=int, default=0, help="Strip count for patch (-p)") PARSER.add_argument("--patch", action="append", default=[], dest="patches", help="Patch file to apply (repeatable)") +PARSER.add_argument("--execroot-marker", default=None, help="Token in env values to replace with the absolute execroot") opts, args = PARSER.parse_known_args() tmp_root = path.abspath(opts.outdir) + ".tmp" @@ -249,7 +255,7 @@ def _legacy_metadata_conflicts_with_pyproject(worktree): # and re-point CC/CXX/etc. through wrapper scripts in tmp_root so the # Bazel-supplied workspace-relative compiler paths survive the cwd # change into the worktree. -build_env = _compiler_env(tmp_root) +build_env = _compiler_env(tmp_root, opts.execroot_marker) if _legacy_metadata_conflicts_with_pyproject(t): print( diff --git a/uv/private/pep517_whl/rule.bzl b/uv/private/pep517_whl/rule.bzl index 1e4198b46..d3a5b040a 100644 --- a/uv/private/pep517_whl/rule.bzl +++ b/uv/private/pep517_whl/rule.bzl @@ -11,6 +11,11 @@ load("//py/private/toolchain:types.bzl", "NATIVE_BUILD_TOOLCHAIN", "PY_TOOLCHAIN _CC_TOOLCHAIN_TYPE = Label("@bazel_tools//tools/cpp:toolchain_type") _TARGET_EXEC_GROUP = "target" +# `$(EXECROOT)` expands to this marker, which build_helper replaces with the +# absolute execroot at build time — the execroot isn't known at analysis. Lets +# an `env` path opt into anchoring so it survives the backend's chdir. +_EXECROOT_MARKER = "__ASPECT_RULES_PY_EXECROOT__" + _INHERITED_PYTHON_ENV = ( "PYTHONHOME", "PYTHONPATH", @@ -151,6 +156,15 @@ def _pep517_native_whl(ctx): env = _common_env(ctx) extra_inputs, known_variables = _collect_toolchain_inputs_and_vars(ctx) + # `EXECROOT` is reserved for the anchor below. If a toolchain already + # exports it, `$(EXECROOT)` would expand to that value instead of our + # marker and anchoring would silently no-op, so fail loudly instead. + if "EXECROOT" in known_variables: + fail("A toolchain listed in `toolchains` exports a reserved `EXECROOT` " + + "make-variable, which collides with the `$(EXECROOT)` anchor used to " + + "absolutize workspace-relative env paths. Rename that toolchain's variable.") + known_variables["EXECROOT"] = _EXECROOT_MARKER + cc_files, cc_compiler = _cc_toolchain_inputs_and_compiler(ctx) if cc_files: extra_inputs.append(cc_files) @@ -168,6 +182,8 @@ def _pep517_native_whl(ctx): executable = ctx.executable.tool, toolchain = None, arguments = ctx.attr.args + patch_args + _memory_args(ctx) + [ + "--execroot-marker", + _EXECROOT_MARKER, archive.path, wheel_dir.path, ], @@ -253,7 +269,10 @@ constraints of the target platform. doc = "Environment variables to set on the build action. Values may " + "contain `$(VAR)` references to make-variables exposed by any " + "target in the rule's `toolchains` attribute (via " + - "`TemplateVariableInfo`).", + "`TemplateVariableInfo`). Prefix a workspace-relative path with " + + "`$(EXECROOT)/` (e.g. `-I$(EXECROOT)/$(DEP_INC)`) to anchor it to " + + "an absolute path so it survives the build backend's chdir into " + + "the sdist worktree.", ), }, exec_groups = { diff --git a/uv/private/pep517_whl/tests/native_dep/BUILD.bazel b/uv/private/pep517_whl/tests/native_dep/BUILD.bazel index 36063fcdf..114798017 100644 --- a/uv/private/pep517_whl/tests/native_dep/BUILD.bazel +++ b/uv/private/pep517_whl/tests/native_dep/BUILD.bazel @@ -51,8 +51,8 @@ pep517_native_whl( testonly = True, src = ":sdist", env = { - "CPPFLAGS": "-I$(DEP_INC)", - "LDFLAGS": "$(DEP_LIB_A)", + "CPPFLAGS": "-I$(EXECROOT)/$(DEP_INC)", + "LDFLAGS": "$(EXECROOT)/$(DEP_LIB_A)", }, tags = ["manual"], tool = "//uv/private/pep517_whl:__build_helper", From 9e07677436b387a9c7cfde575b728d9e6c7705b6 Mon Sep 17 00:00:00 2001 From: Connor McEntee Date: Mon, 6 Jul 2026 13:48:26 -0600 Subject: [PATCH 3/4] chore(uv): gazelle bzl_library for native_dep test defs Adds the bzl_library target gazelle generates for defs.bzl, keeping the BUILD file consistent with the repo's gazelle check. Co-Authored-By: Claude Opus 4.8 (1M context) --- uv/private/pep517_whl/tests/native_dep/BUILD.bazel | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/uv/private/pep517_whl/tests/native_dep/BUILD.bazel b/uv/private/pep517_whl/tests/native_dep/BUILD.bazel index 114798017..c59745795 100644 --- a/uv/private/pep517_whl/tests/native_dep/BUILD.bazel +++ b/uv/private/pep517_whl/tests/native_dep/BUILD.bazel @@ -1,3 +1,5 @@ +load("@bazel_lib//:bzl_library.bzl", "bzl_library") + # Regression: a native sdist consuming an in-repo cc_library via # override_package(env=/toolchains=) must build. The dep's include dir and # static archive reach the build as workspace-relative paths, which resolve @@ -64,3 +66,10 @@ build_test( name = "native_dep_env_test", targets = [":whl"], ) + +bzl_library( + name = "defs", + srcs = ["defs.bzl"], + visibility = ["//uv:__subpackages__"], + deps = ["@rules_cc//cc/common:cc_info"], +) From 352f05b5d4e71710cbb0580f36ffeb9deed53e1d Mon Sep 17 00:00:00 2001 From: Connor McEntee Date: Mon, 6 Jul 2026 22:58:31 -0600 Subject: [PATCH 4/4] fix(uv): correct native dep bzl_library dependency --- uv/private/pep517_whl/tests/native_dep/BUILD.bazel | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/uv/private/pep517_whl/tests/native_dep/BUILD.bazel b/uv/private/pep517_whl/tests/native_dep/BUILD.bazel index c59745795..8cadf8315 100644 --- a/uv/private/pep517_whl/tests/native_dep/BUILD.bazel +++ b/uv/private/pep517_whl/tests/native_dep/BUILD.bazel @@ -71,5 +71,5 @@ bzl_library( name = "defs", srcs = ["defs.bzl"], visibility = ["//uv:__subpackages__"], - deps = ["@rules_cc//cc/common:cc_info"], + deps = ["@rules_cc//cc/common:cc_info.bzl"], )