Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions docs/uv.md
Original file line number Diff line number Diff line change
Expand Up @@ -460,6 +460,15 @@ maturin is told not to download a Rust toolchain of its own
(`MATURIN_NO_INSTALL_RUST=1`): a Rust sdist with no toolchain wired fails
instead of fetching rustc inside the build.

The rustc cargo runs is a wrapper that also makes the extension independent
of where the action ran: sandbox and execroot paths are remapped out of the
binaries (`--remap-path-prefix`) and target crates compile as one codegen
unit, and maturin's SBOM, which records sandbox paths, is turned off unless
the sdist configures it; the wheel's bytes then match across hosts and
downstream actions hit the cache. Crates that compile C or C++ through cc-rs (`ring`, `zstd-sys`) find
the wired C toolchain under `CC_<triple>`, `CXX_<triple>`, `AR_<triple>` and
`RANLIB_<triple>` instead of whatever is on the PATH.

### Backend config settings

PEP 517 backends take a free-form `config_settings` dictionary. Declare it
Expand Down
96 changes: 95 additions & 1 deletion uv/private/pep517_whl/tests/build_helper_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -681,7 +681,7 @@ def test_ranlib_routes_ar_s(self) -> None:
# that is the host's ranlib against the target's archives. The wrapper
# must route `ar s` (ranlib's POSIX spelling) through our AR.
content, tmp = self._toolchain("linux", "x86_64")
ranlib = path.join(tmp, "cmake_ranlib")
ranlib = path.join(tmp, ".aspect_rules_py_compilers", "ranlib")
self.assertIn('set(CMAKE_RANLIB "{}")'.format(ranlib), content)
with open(ranlib) as f:
wrapper = f.read()
Expand Down Expand Up @@ -860,6 +860,100 @@ def test_no_vendor_dir_leaves_cargo_online(self) -> None:
self.assertFalse(path.exists(path.join(tmp, "config.toml")))


class RustcWrapperTest(unittest.TestCase):
"""Runs the generated rustc wrapper around an argv-echoing fake rustc."""

def _fake_rustc(self, tmp: str) -> str:
rustc = path.join(tmp, "tc", "bin", "rustc")
makedirs(path.dirname(rustc))
with open(rustc, "w") as f:
f.write('#!/bin/sh\nprintf \'%s\\n\' "$@"\n')
os.chmod(rustc, 0o755)
return rustc

def test_paths_are_remapped_and_target_crates_get_one_codegen_unit(self) -> None:
tmp = tempfile.mkdtemp()
wrapper = build_helper._write_rustc_wrapper(tmp, self._fake_rustc(tmp), "/tc/sysroot", "aarch64-unknown-linux-gnu")
argv = _run_wrapper(wrapper, ["--crate-name", "ext", "--target", "aarch64-unknown-linux-gnu"])
self.assertEqual(["--sysroot", "/tc/sysroot"], argv[:2])
self.assertIn("--remap-path-prefix", argv)
remapped = [argv[i + 1] for i, a in enumerate(argv) if a == "--remap-path-prefix"]
self.assertIn(path.abspath(tmp) + "/=", remapped, "the sandbox root is remapped away")
self.assertIn(os.getcwd() + "=", remapped, "the execroot is remapped away")
self.assertIn("codegen-units=1", argv)
self.assertEqual(["--crate-name", "ext", "--target", "aarch64-unknown-linux-gnu"], argv[-4:], "cargo's own arguments come last, untouched")

def test_exec_platform_crates_keep_cargo_codegen(self) -> None:
tmp = tempfile.mkdtemp()
wrapper = build_helper._write_rustc_wrapper(tmp, self._fake_rustc(tmp), "/tc/sysroot", "aarch64-unknown-linux-gnu")
argv = _run_wrapper(wrapper, ["--crate-name", "build_script_build"])
self.assertNotIn("codegen-units=1", argv, "build scripts and proc-macros never reach the wheel")
self.assertIn("--remap-path-prefix", argv)

def test_native_build_treats_every_crate_as_target(self) -> None:
tmp = tempfile.mkdtemp()
wrapper = build_helper._write_rustc_wrapper(tmp, self._fake_rustc(tmp), "/tc/sysroot", None)
argv = _run_wrapper(wrapper, ["--crate-name", "ext"])
self.assertIn("codegen-units=1", argv)


class DisableMaturinSbomTest(unittest.TestCase):
def _worktree(self, pyproject: str | None) -> str:
tmp = tempfile.mkdtemp()
if pyproject is not None:
with open(path.join(tmp, "pyproject.toml"), "w") as f:
f.write(pyproject)
return tmp

def test_sbom_is_turned_off(self) -> None:
tmp = self._worktree('[build-system]\nbuild-backend = "maturin"\n')
self.assertTrue(build_helper._disable_maturin_sbom(tmp))
with open(path.join(tmp, "pyproject.toml")) as f:
content = f.read()
self.assertIn("[tool.maturin.sbom]\nrust = false\nauditwheel = false\n", content)
self.assertTrue(content.startswith("[build-system]"), "the sdist's own configuration is kept")

def test_explicit_sbom_configuration_is_respected(self) -> None:
original = '[tool.maturin.sbom]\nrust = true\n'
tmp = self._worktree(original)
self.assertFalse(build_helper._disable_maturin_sbom(tmp))
with open(path.join(tmp, "pyproject.toml")) as f:
self.assertEqual(original, f.read())

def test_no_pyproject_no_change(self) -> None:
self.assertFalse(build_helper._disable_maturin_sbom(self._worktree(None)))


class CcRsEnvTest(unittest.TestCase):
def test_cc_rs_finds_the_wired_toolchain_under_both_spellings(self) -> None:
tmp = tempfile.mkdtemp()
env = {"CC": "/w/cc", "CXX": "/w/c++", "AR": "/w/ar"}
build_helper._cc_rs_env(env, tmp, "aarch64-unknown-linux-gnu")
for spelling in ("aarch64-unknown-linux-gnu", "aarch64_unknown_linux_gnu"):
self.assertEqual("/w/cc", env["CC_" + spelling])
self.assertEqual("/w/c++", env["CXX_" + spelling])
self.assertEqual("/w/ar", env["AR_" + spelling])
ranlib = env["RANLIB_" + spelling]
self.assertTrue(os.access(ranlib, os.X_OK))
with open(ranlib) as f:
self.assertIn('exec "/w/ar" s "$@"', f.read())

def test_no_ar_no_archiver_vars(self) -> None:
env = {"CC": "/w/cc", "CXX": "/w/c++"}
build_helper._cc_rs_env(env, tempfile.mkdtemp(), "x86_64-unknown-linux-gnu")
self.assertNotIn("AR_x86_64-unknown-linux-gnu", env)
self.assertNotIn("RANLIB_x86_64_unknown_linux_gnu", env)
self.assertEqual("/w/cc", env["CC_x86_64_unknown_linux_gnu"])

def test_cross_env_exports_cc_rs_vars(self) -> None:
tmp = tempfile.mkdtemp()
env = {"CARGO": "/tc/bin/cargo", "RUSTC": "/tc/bin/rustc", "CC": "/w/cc", "CXX": "/w/c++", "AR": "/w/ar"}
build_helper._configure_cargo_cross_env(env, tmp, "linux", "x86_64", "musl")
self.assertEqual("/w/cc", env["CARGO_TARGET_X86_64_UNKNOWN_LINUX_MUSL_LINKER"])
self.assertEqual("/w/cc", env["CC_x86_64-unknown-linux-musl"])
self.assertEqual("/w/c++", env["CXX_x86_64_unknown_linux_musl"])


class CargoNativeEnvTest(unittest.TestCase):
def test_rustc_gets_the_toolchain_sysroot(self) -> None:
tmp = tempfile.mkdtemp()
Expand Down
101 changes: 85 additions & 16 deletions uv/private/pep517_whl/tools/build_helper.py
Original file line number Diff line number Diff line change
Expand Up @@ -886,11 +886,7 @@ def _generate_cmake_toolchain_file(
contain spaces, which unquoted set() would parse as list separators.
"""
ar = build_env.get("AR", "ar")
ranlib = _write_generated_file(
path.join(tmpdir, "cmake_ranlib"),
'#!/bin/sh\nexec "{}" s "$@"\n'.format(ar),
executable=True,
)
ranlib = _ranlib_wrapper(tmpdir, ar)
return _write_generated_file(
path.join(tmpdir, "cross_toolchain.cmake"),
textwrap.dedent("""\
Expand Down Expand Up @@ -923,14 +919,68 @@ def _generate_cmake_toolchain_file(
("darwin", "libsystem"): "apple-darwin",
}

# Beyond the explicit sysroot, the wrapper makes the compiled artifacts
# independent of where the action ran: rustc bakes source paths into debug
# info and panic messages, and the sandbox root and execroot differ per host
# and per action, so they are remapped away. A single codegen unit keeps
# LLVM's module ids, which leak into symbol names, stable across hosts; it
# is applied to target crates only (build scripts and proc-macros never
# reach the wheel), or to everything in a native build where cargo passes
# no --target.
_RUSTC_WRAPPER = """#!/usr/bin/env python3
import os
import sys

os.execv({rustc!r}, [{rustc!r}, "--sysroot", {sysroot!r}] + sys.argv[1:])
args = sys.argv[1:]
final = [{rustc!r}, "--sysroot", {sysroot!r}]
for prefix in {remap_prefixes!r}:
final += ["--remap-path-prefix", prefix + "/=", "--remap-path-prefix", prefix + "="]
target = {target_triple!r}
if target is None or target in args:
final += ["-C", "codegen-units=1"]
os.execv(final[0], final + args)
"""


def _write_rustc_wrapper(tmpdir: str, rustc: str, sysroot: str, target_triple: str | None) -> str:
"""The rustc cargo runs: explicit sysroot, reproducible paths and codegen (see _RUSTC_WRAPPER)."""
return _write_generated_file(
path.join(tmpdir, ".aspect_rules_py_rustc", "rustc"),
_RUSTC_WRAPPER.format(
rustc=rustc,
sysroot=sysroot,
remap_prefixes=[path.abspath(tmpdir), os.getcwd()],
target_triple=target_triple,
),
executable=True,
)


def _ranlib_wrapper(tmpdir: str, ar: str) -> str:
"""`ar s` is ranlib: a ranlib for toolchains that ship none as a separate tool."""
return _write_generated_file(
path.join(tmpdir, ".aspect_rules_py_compilers", "ranlib"),
'#!/bin/sh\nexec "{}" s "$@"\n'.format(ar),
executable=True,
)


def _cc_rs_env(build_env: dict[str, str], tmpdir: str, triple: str) -> None:
"""Point cc-rs at the wired C toolchain for crates that compile C or C++.

cc-rs (ring, zstd-sys, ...) looks up CC_<triple>, CXX_<triple>, AR_<triple>
and RANLIB_<triple>, in the dashed and the underscored spelling, before
falling back to whatever `cc` is on PATH. The generic CC/CXX/AR only
steer host compiles.
"""
ar = build_env.get("AR", "")
tools = {"CC": build_env.get("CC", ""), "CXX": build_env.get("CXX", ""), "AR": ar, "RANLIB": _ranlib_wrapper(tmpdir, ar) if ar else ""}
for spelling in (triple, triple.replace("-", "_")):
for tool, value in tools.items():
if value:
build_env["{}_{}".format(tool, spelling)] = value


def _merge_rust_sysroot(tmpdir: str, target_rustc: str, host_sysroot: str, target_sysroot: str | None = None) -> str:
"""Symlink-merge the target toolchain's sysroot with the host's rust-std.

Expand Down Expand Up @@ -1002,6 +1052,7 @@ def _configure_cargo_cross_env(build_env: dict[str, str], tmpdir: str, target_os
build_env["CARGO_BUILD_TARGET"] = triple
linker_var = "CARGO_TARGET_{}_LINKER".format(triple.upper().replace("-", "_"))
build_env[linker_var] = build_env["CC"]
_cc_rs_env(build_env, tmpdir, triple)

# pyo3-ffi refuses to cross-compile without an explicit target Python
# version. Unused (harmless) for non-PyO3 crates.
Expand All @@ -1010,11 +1061,7 @@ def _configure_cargo_cross_env(build_env: dict[str, str], tmpdir: str, target_os
host_sysroot = build_env.get("RULES_PY_RUST_HOST_SYSROOT")
if host_sysroot:
merged_sysroot = _merge_rust_sysroot(tmpdir, build_env["RUSTC"], host_sysroot, build_env.get("RULES_PY_RUST_SYSROOT"))
build_env["RUSTC"] = _write_generated_file(
path.join(tmpdir, ".aspect_rules_py_rustc", "rustc"),
_RUSTC_WRAPPER.format(rustc=build_env["RUSTC"], sysroot=merged_sysroot),
executable=True,
)
build_env["RUSTC"] = _write_rustc_wrapper(tmpdir, build_env["RUSTC"], merged_sysroot, triple)

# In cross mode maturin name-parses its -i interpreter argument for a
# "pythonX.Y"-shaped basename instead of executing it; our venv's
Expand All @@ -1027,6 +1074,30 @@ def _configure_cargo_cross_env(build_env: dict[str, str], tmpdir: str, target_os
build_env["MATURIN_PEP517_ARGS"] = (interpreter_arg + " " + existing).strip()


_MATURIN_SBOM_OFF = "\n[tool.maturin.sbom]\nrust = false\nauditwheel = false\n"


def _disable_maturin_sbom(worktree: str) -> bool:
"""Turn off maturin's CycloneDX SBOM unless the sdist configures it itself.

The SBOM records every crate as `path+file:///<sandbox>/...`: the sandbox
id differs per action, so two builds of one sdist never produce the same
wheel while it is on. maturin 1.15 offers no flag or variable for it, only
the `[tool.maturin.sbom]` table, so it is appended to the extracted
pyproject.toml. Returns whether the file was changed.
"""
pyproject = path.join(worktree, "pyproject.toml")
if not path.exists(pyproject):
return False
with open(pyproject, encoding="utf-8") as f:
content = f.read()
if "[tool.maturin.sbom]" in content:
return False
with open(pyproject, "a", encoding="utf-8") as f:
f.write(_MATURIN_SBOM_OFF)
return True


def _inject_cargo_lock(worktree: str, lock_path: str) -> str | None:
"""Copy a user-supplied Cargo.lock next to the source tree's top-level Cargo.toml.

Expand Down Expand Up @@ -1086,11 +1157,7 @@ def _configure_cargo_native_env(build_env: dict[str, str], tmpdir: str) -> None:
sysroot = build_env.get("RULES_PY_RUST_SYSROOT") or build_env.get("RULES_PY_RUST_HOST_SYSROOT")
if not (build_env.get("CARGO") and sysroot):
return
build_env["RUSTC"] = _write_generated_file(
path.join(tmpdir, ".aspect_rules_py_rustc", "rustc"),
_RUSTC_WRAPPER.format(rustc=build_env["RUSTC"], sysroot=sysroot),
executable=True,
)
build_env["RUSTC"] = _write_rustc_wrapper(tmpdir, build_env["RUSTC"], sysroot, None)


def _build_backend(pyproject_data: dict[str, object] | None) -> str | None:
Expand Down Expand Up @@ -1284,6 +1351,8 @@ def main() -> None:
_forbid_backend_toolchain_downloads(build_env)
_configure_cargo_offline(build_env, opts.cargo_vendor_dir)
_inject_cargo_lock(t, opts.cargo_lock)
if _build_backend(_load_pyproject_data(t)) == "maturin":
_disable_maturin_sbom(t)

if _legacy_metadata_conflicts_with_pyproject(t):
print(
Expand Down
Loading