Skip to content
Draft
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
13 changes: 12 additions & 1 deletion uv/private/pep517_whl/BUILD.bazel
Original file line number Diff line number Diff line change
@@ -1,14 +1,25 @@
load("@bazel_lib//:bzl_library.bzl", "bzl_library")
load("//py/unstable:defs.bzl", "py_venv_test")

package(default_visibility = [
"//uv/private:__subpackages__",
])

exports_files(
["build_helper.py"],
["build_helper.py", "build_backend.py"],
visibility = ["//visibility:public"],
)

py_venv_test(
name = "build_backend_test",
srcs = [
"build_backend.py",
"build_backend_test.py",
],
imports = ["../../.."],
main = "build_backend_test.py",
)

bzl_library(
name = "rule",
srcs = ["rule.bzl"],
Expand Down
113 changes: 113 additions & 0 deletions uv/private/pep517_whl/build_backend.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
"""
Build backend compatibility helper for sdist wheel builds.

Extracted into its own module so it can be unit-tested independently of
the build_helper.py script entry point.
"""

import importlib.metadata
from os import path


def ensure_build_backend(src_dir):
"""Patch pyproject.toml and generate setup.cfg if needed for buildability.

When the declared build backend (e.g. hatchling) is not importable in the
build venv, rewrites pyproject.toml to use setuptools.build_meta. Also
generates setup.cfg for setuptools < 61.0 which does not support the
PEP 621 [project] table.
"""
pyproject_path = path.join(src_dir, "pyproject.toml")
if not path.exists(pyproject_path):
return

with open(pyproject_path) as f:
lines = f.readlines()

name = None
version = None
build_backend = None
in_project = False
in_build_system = False
in_project_scripts = False
has_src_layout = path.isdir(path.join(src_dir, "src"))
scripts = {}

for line in lines:
stripped = line.strip()
if stripped == "[project]":
in_project, in_build_system, in_project_scripts = True, False, False
continue
elif stripped == "[build-system]":
in_build_system, in_project, in_project_scripts = True, False, False
continue
elif stripped == "[project.scripts]":
in_project_scripts, in_project, in_build_system = True, False, False
continue
elif stripped.startswith("["):
in_project = in_build_system = in_project_scripts = False
continue
if in_project:
if stripped.startswith("name"):
_, _, val = stripped.partition("=")
name = val.strip().strip('"').strip("'")
elif stripped.startswith("version"):
_, _, val = stripped.partition("=")
version = val.strip().strip('"').strip("'")
elif in_build_system:
if stripped.startswith("build-backend"):
_, _, val = stripped.partition("=")
build_backend = val.strip().strip('"').strip("'")
elif in_project_scripts:
if "=" in stripped:
sname, _, sval = stripped.partition("=")
scripts[sname.strip().strip('"').strip("'")] = sval.strip().strip('"').strip("'")

backend_available = True
if build_backend and build_backend != "setuptools.build_meta":
backend_module = build_backend.split(":")[0].split(".")[0]
try:
__import__(backend_module)
except ImportError:
backend_available = False

if not backend_available:
new_lines = []
in_build_system_section = False
for line in lines:
stripped = line.strip()
if stripped == "[build-system]":
in_build_system_section = True
new_lines.append("[build-system]\n")
new_lines.append('requires = ["setuptools", "wheel"]\n')
new_lines.append('build-backend = "setuptools.build_meta"\n')
continue
elif stripped.startswith("[") and in_build_system_section:
in_build_system_section = False
if in_build_system_section:
continue
new_lines.append(line)
with open(pyproject_path, "w") as f:
f.writelines(new_lines)
build_backend = "setuptools.build_meta"

if build_backend == "setuptools.build_meta" or build_backend is None:
try:
st_ver = tuple(int(x) for x in importlib.metadata.version("setuptools").split(".")[:2])
except Exception:
st_ver = (0, 0)

setup_cfg_path = path.join(src_dir, "setup.cfg")
if st_ver < (61, 0) and name and version and not path.exists(setup_cfg_path):
cfg = "[metadata]\nname = {}\nversion = {}\n".format(name, version)
if has_src_layout:
cfg += "\n[options]\npackage_dir =\n = src\npackages = find:\n\n[options.packages.find]\nwhere = src\n"
else:
cfg += "\n[options]\npackages = find:\n"
cfg += "\n[options.package_data]\n* = *\n"
if scripts:
cfg += "\n[options.entry_points]\nconsole_scripts =\n"
for sname, sval in scripts.items():
cfg += " {} = {}\n".format(sname, sval)
with open(setup_cfg_path, "w") as f:
f.write(cfg)
176 changes: 176 additions & 0 deletions uv/private/pep517_whl/build_backend_test.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
"""Unit tests for build_backend.ensure_build_backend."""

import os
import sys
import tempfile

from uv.private.pep517_whl.build_backend import ensure_build_backend


def _make_project(members):
"""Write a dict of {relative_path: content} into a temp directory."""
d = tempfile.mkdtemp()
for rel, content in members.items():
full = os.path.join(d, rel)
os.makedirs(os.path.dirname(full), exist_ok=True)
with open(full, "w") as f:
f.write(content)
return d


def _read(d, rel):
with open(os.path.join(d, rel)) as f:
return f.read()


def _pyproject(backend, name="pkg", version="1.0"):
return (
"[project]\n"
'name = "{}"\n'
'version = "{}"\n'
"\n"
"[build-system]\n"
'requires = ["{}"]'
'\nbuild-backend = "{}"\n'
).format(name, version, backend.split(":")[0].split(".")[0], backend)


# ---------------------------------------------------------------------------
# Available backend — no mutation
# ---------------------------------------------------------------------------

def test_available_backend_not_mutated():
"""When the declared backend is importable, pyproject.toml is left alone."""
original = _pyproject("setuptools.build_meta")
d = _make_project({"pyproject.toml": original})
ensure_build_backend(d)
assert _read(d, "pyproject.toml") == original


# ---------------------------------------------------------------------------
# Unavailable backend — fallback to setuptools
# ---------------------------------------------------------------------------

def test_unavailable_backend_rewritten():
"""A non-importable backend causes pyproject.toml to be rewritten."""
d = _make_project({"pyproject.toml": _pyproject("_nonexistent_backend_xyz_")})
ensure_build_backend(d)
content = _read(d, "pyproject.toml")
assert 'build-backend = "setuptools.build_meta"' in content
assert "_nonexistent_backend_xyz_" not in content


def test_unavailable_backend_preserves_project_section():
"""Rewriting build-system must not strip the [project] table."""
d = _make_project({"pyproject.toml": _pyproject("_nonexistent_backend_xyz_", name="mypkg", version="2.3")})
ensure_build_backend(d)
content = _read(d, "pyproject.toml")
assert "[project]" in content
assert 'name = "mypkg"' in content
assert 'version = "2.3"' in content


# ---------------------------------------------------------------------------
# No pyproject.toml — no-op
# ---------------------------------------------------------------------------

def test_no_pyproject_is_noop():
d = tempfile.mkdtemp()
ensure_build_backend(d) # must not raise


# ---------------------------------------------------------------------------
# setup.cfg generation for old setuptools (mocked via monkeypatching)
# ---------------------------------------------------------------------------

def test_setup_cfg_generated_for_old_setuptools(monkeypatch=None):
"""When setuptools < 61, a setup.cfg is generated from [project] metadata."""
import importlib.metadata
import uv.private.pep517_whl.build_backend as mod

original_version = importlib.metadata.version

def fake_version(name):
if name == "setuptools":
return "60.0.0"
return original_version(name)

mod_metadata = mod.importlib.metadata
orig = mod_metadata.version
mod_metadata.version = fake_version
try:
d = _make_project({"pyproject.toml": _pyproject("setuptools.build_meta", name="mypkg", version="3.1")})
ensure_build_backend(d)
assert os.path.exists(os.path.join(d, "setup.cfg"))
cfg = _read(d, "setup.cfg")
assert "name = mypkg" in cfg
assert "version = 3.1" in cfg
finally:
mod_metadata.version = orig


def test_setup_cfg_not_generated_for_new_setuptools():
"""When setuptools >= 61, setup.cfg is NOT generated."""
d = _make_project({"pyproject.toml": _pyproject("setuptools.build_meta", name="mypkg", version="1.0")})
ensure_build_backend(d)
assert not os.path.exists(os.path.join(d, "setup.cfg"))


def test_existing_setup_cfg_not_overwritten():
"""An existing setup.cfg must never be overwritten."""
import importlib.metadata
import uv.private.pep517_whl.build_backend as mod

original_content = "[metadata]\nname = original\n"
mod_metadata = mod.importlib.metadata
orig = mod_metadata.version
mod_metadata.version = lambda n: "60.0.0" if n == "setuptools" else orig(n)
try:
d = _make_project({
"pyproject.toml": _pyproject("setuptools.build_meta", name="mypkg", version="1.0"),
"setup.cfg": original_content,
})
ensure_build_backend(d)
assert _read(d, "setup.cfg") == original_content
finally:
mod_metadata.version = orig


# ---------------------------------------------------------------------------
# [project.scripts] propagated into setup.cfg entry_points
# ---------------------------------------------------------------------------

def test_scripts_in_setup_cfg(monkeypatch=None):
import uv.private.pep517_whl.build_backend as mod

pyproject = (
'[project]\nname = "cli"\nversion = "1.0"\n\n'
'[project.scripts]\ncli-tool = "cli.main:main"\n\n'
'[build-system]\nrequires = ["setuptools"]\nbuild-backend = "setuptools.build_meta"\n'
)
mod_metadata = mod.importlib.metadata
orig = mod_metadata.version
mod_metadata.version = lambda n: "60.0.0" if n == "setuptools" else orig(n)
try:
d = _make_project({"pyproject.toml": pyproject})
ensure_build_backend(d)
cfg = _read(d, "setup.cfg")
assert "console_scripts" in cfg
assert "cli-tool" in cfg
finally:
mod_metadata.version = orig


if __name__ == "__main__":
failures = []
fns = [v for k, v in sorted(globals().items()) if k.startswith("test_") and callable(v)]
for fn in fns:
try:
fn()
print(" PASS {}".format(fn.__name__))
except Exception as e:
print(" FAIL {}: {}".format(fn.__name__, e))
failures.append(fn.__name__)
print("\n{} passed, {} failed".format(len(fns) - len(failures), len(failures)))
if failures:
sys.exit(1)
3 changes: 3 additions & 0 deletions uv/private/pep517_whl/build_helper.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,9 @@
)


from uv.private.pep517_whl.build_backend import ensure_build_backend
ensure_build_backend(t)

# Get a path to the outdir which will be valid after we cd
outdir = path.abspath(opts.outdir)

Expand Down
Loading