feat: option to build directly with uv (#2322)

* feat: option to build directly with uv

Signed-off-by: Henry Schreiner <henryschreineriii@gmail.com>

tests: add uv to tests

Signed-off-by: Henry Schreiner <henryschreineriii@gmail.com>

fix: uv doesn't pick special Pythons at the top of the path

Signed-off-by: Henry Schreiner <henryschreineriii@gmail.com>

fix: set uv build constraints too

Signed-off-by: Henry Schreiner <henryschreineriii@gmail.com>

fix: uv doesn't support PyPy 3.8

Signed-off-by: Henry Schreiner <henryschreineriii@gmail.com>

tests: add pyproject.toml for failing test

Signed-off-by: Henry Schreiner <henryschreineriii@gmail.com>

Revert "tests: add pyproject.toml for failing test"

This reverts commit 619bde368a67dfdd8f02dc33a1525c27bc8cbe91.

tests: skip uv output test

Signed-off-by: Henry Schreiner <henryschreineriii@gmail.com>

* tests: uv was in the wrong fixture

Signed-off-by: Henry Schreiner <henryfs@princeton.edu>

* fix: from @trim21

Signed-off-by: Henry Schreiner <henryfs@princeton.edu>

* tests: improve skipping

Signed-off-by: Henry Schreiner <henryfs@princeton.edu>

* fix: some fixes from copilot review

Signed-off-by: Henry Schreiner <henryfs@princeton.edu>

---------

Signed-off-by: Henry Schreiner <henryschreineriii@gmail.com>
Signed-off-by: Henry Schreiner <henryfs@princeton.edu>
This commit is contained in:
Henry Schreiner
2026-02-05 12:08:36 -05:00
committed by GitHub
parent 6088ea2c85
commit d7dbf5a358
9 changed files with 75 additions and 16 deletions
+5 -3
View File
@@ -57,21 +57,23 @@ properties:
type: string_array
build-frontend:
default: default
description: Set the tool to use to build, either "build" (default), "build[uv]", or "pip"
description: Set the tool to use to build, either "build" (default), "build[uv]", "uv", or "pip"
oneOf:
- enum: [pip, build, "build[uv]", default]
- enum: [pip, build, "build[uv]", uv, default]
- type: string
pattern: '^pip; ?args:'
- type: string
pattern: '^build; ?args:'
- type: string
pattern: '^build\\[uv\\]; ?args:'
- type: string
pattern: '^uv; ?args:'
- type: object
additionalProperties: false
required: [name]
properties:
name:
enum: [pip, build, "build[uv]"]
enum: [pip, build, "build[uv]", uv]
args:
type: array
items:
+1 -1
View File
@@ -7,7 +7,7 @@ from typing import Literal, Self, get_args
from .logger import log
from .util.helpers import parse_key_value_string
BuildFrontendName = Literal["pip", "build", "build[uv]"]
BuildFrontendName = Literal["pip", "build", "build[uv]", "uv"]
@dataclasses.dataclass(frozen=True)
+2 -2
View File
@@ -284,7 +284,7 @@ def setup_python(
build_frontend: BuildFrontendName,
xbuild_tools: Sequence[str] | None,
) -> tuple[Path, dict[str, str]]:
if build_frontend == "build[uv]":
if build_frontend == "build[uv]" or build_frontend == "uv":
msg = "uv doesn't support iOS"
raise errors.FatalError(msg)
@@ -441,7 +441,7 @@ def build(options: Options, tmp_path: Path) -> None:
build_options = options.build_options(config.identifier)
build_frontend = build_options.build_frontend
# uv doesn't support iOS
if build_frontend.name == "build[uv]":
if build_frontend.name == "build[uv]" or build_frontend.name == "uv":
msg = "uv doesn't support iOS"
raise errors.FatalError(msg)
+15 -1
View File
@@ -207,7 +207,7 @@ def build_in_container(
local_identifier_tmp_dir = local_tmp_dir / config.identifier
build_options = options.build_options(config.identifier)
build_frontend = build_options.build_frontend
use_uv = build_frontend.name == "build[uv]"
use_uv = build_frontend.name in {"build[uv]", "uv"}
pip = ["uv", "pip"] if use_uv else ["pip"]
log.step("Setting up build environment...")
@@ -307,6 +307,19 @@ def build_in_container(
],
env=env,
)
case "uv":
container.call(
[
"uv",
"build",
"--python=python",
container_package_dir,
"--wheel",
f"--out-dir={built_wheel_dir}",
*extra_flags,
],
env=env,
)
case _:
assert_never(build_frontend)
@@ -509,6 +522,7 @@ def _matches_prepared_command(error_cmd: Sequence[str], command_template: str) -
def troubleshoot(options: Options, error: Exception) -> None:
if isinstance(error, subprocess.CalledProcessError) and (
error.cmd[0:4] == ["python", "-m", "pip", "wheel"]
or error.cmd[0:2] == ["uv", "build"]
or error.cmd[0:3] == ["python", "-m", "build"]
or _matches_prepared_command(
error.cmd, options.build_options(None).repair_command
+25 -2
View File
@@ -218,7 +218,7 @@ def setup_python(
build_frontend: BuildFrontendName,
) -> tuple[Path, dict[str, str]]:
uv_path = find_uv()
use_uv = build_frontend == "build[uv]"
use_uv = build_frontend in {"build[uv]", "uv"}
tmp.mkdir()
implementation_id = python_configuration.identifier.split("-")[0]
@@ -381,6 +381,17 @@ def setup_python(
*constraint_flags(dependency_constraint),
env=env,
)
case "uv":
assert uv_path is not None
call(
uv_path,
"pip",
"install",
"--upgrade",
"delocate",
*constraint_flags(dependency_constraint),
env=env,
)
case _:
assert_never(build_frontend)
@@ -413,7 +424,7 @@ def build(options: Options, tmp_path: Path) -> None:
for config in python_configurations:
build_options = options.build_options(config.identifier)
build_frontend = build_options.build_frontend
use_uv = build_frontend.name == "build[uv]"
use_uv = build_frontend.name in {"build[uv]", "uv"}
uv_path = find_uv()
if use_uv and uv_path is None:
msg = "uv not found"
@@ -498,6 +509,18 @@ def build(options: Options, tmp_path: Path) -> None:
*extra_flags,
env=build_env,
)
case "uv":
assert uv_path is not None
call(
uv_path,
"build",
"--python=python",
build_options.package_dir,
"--wheel",
f"--out-dir={built_wheel_dir}",
*extra_flags,
env=build_env,
)
case _:
assert_never(build_frontend)
+16 -3
View File
@@ -271,7 +271,7 @@ def setup_python(
if build_frontend == "build[uv]" and not can_use_uv(python_configuration):
build_frontend = "build"
use_uv = build_frontend == "build[uv]"
use_uv = build_frontend in {"build[uv]", "uv"}
uv_path = find_uv()
log.step("Setting up build environment...")
@@ -386,6 +386,8 @@ def build(options: Options, tmp_path: Path) -> None:
if not python_configurations:
return
uv_path = find_uv()
try:
before_all_options_identifier = python_configurations[0].identifier
before_all_options = options.build_options(before_all_options_identifier)
@@ -403,8 +405,7 @@ def build(options: Options, tmp_path: Path) -> None:
for config in python_configurations:
build_options = options.build_options(config.identifier)
build_frontend = build_options.build_frontend
use_uv = build_frontend.name == "build[uv]" and can_use_uv(config)
use_uv = build_frontend.name in {"build[uv]", "uv"} and can_use_uv(config)
log.build_start(config.identifier)
identifier_tmp_dir = tmp_path / config.identifier
@@ -501,6 +502,18 @@ def build(options: Options, tmp_path: Path) -> None:
*extra_flags,
env=env,
)
case "uv":
assert uv_path is not None
call(
uv_path,
"build",
"--python=python",
build_options.package_dir,
"--wheel",
f"--out-dir={built_wheel_dir}",
*extra_flags,
env=env,
)
case _:
assert_never(build_frontend)
+5 -3
View File
@@ -163,7 +163,7 @@ def build_frontend_env_nouv(request: pytest.FixtureRequest) -> dict[str, str]:
return {"CIBW_BUILD_FRONTEND": frontend}
@pytest.fixture(params=["pip", "build", "build[uv]"])
@pytest.fixture(params=["pip", "build", "build[uv]", "uv"])
def build_frontend_env(request: pytest.FixtureRequest) -> dict[str, str]:
frontend = request.param
marks = {m.name for m in request.node.iter_markers()}
@@ -178,10 +178,12 @@ def build_frontend_env(request: pytest.FixtureRequest) -> dict[str, str]:
if platform in {"pyodide", "ios", "android"} and frontend == "pip":
pytest.skip(f"Can't use pip as build frontend for {platform}")
if platform == "pyodide" and frontend == "build[uv]":
if platform == "android" and frontend == "uv":
pytest.skip(f"Can't use uv as build frontend for {platform}")
if platform == "pyodide" and frontend in {"build[uv]", "uv"}:
pytest.skip("Can't use uv with pyodide yet")
uv_path = find_uv()
if uv_path is None and frontend == "build[uv]":
if uv_path is None and frontend in {"build[uv]", "uv"}:
pytest.skip("Can't find uv, so skipping uv tests")
if uv_path is not None and frontend == "build" and platform not in {"android", "ios"}:
pytest.skip("No need to check build when uv is present")
+1 -1
View File
@@ -128,7 +128,7 @@ def test_frontend_good(tmp_path, build_frontend_env):
assert wheels == [f"spam-0.1.0-cp313-cp313-android_21_{native_arch.android_abi}.whl"]
@pytest.mark.parametrize("frontend", ["pip"])
@pytest.mark.parametrize("frontend", ["pip", "uv"])
def test_frontend_bad(frontend, tmp_path, capfd):
new_c_project().generate(tmp_path)
with pytest.raises(CalledProcessError):
+5
View File
@@ -328,6 +328,11 @@ def test_environment_pass_references():
"build",
[],
),
(
'build-frontend = "uv"',
"uv",
[],
),
(
'build-frontend = {name = "build"}',
"build",