From 9ec37d84b6dae19787d53e0a206fd57a877c9564 Mon Sep 17 00:00:00 2001 From: Henry Schreiner Date: Mon, 17 Mar 2025 16:16:40 -0400 Subject: [PATCH 1/8] feat: option to build directly with uv Signed-off-by: Henry Schreiner --- bin/generate_schema.py | 8 +++++--- cibuildwheel/frontend.py | 2 +- cibuildwheel/platforms/ios.py | 4 ++-- cibuildwheel/platforms/linux.py | 14 +++++++++++++- cibuildwheel/platforms/macos.py | 25 +++++++++++++++++++++++-- cibuildwheel/platforms/windows.py | 15 ++++++++++++--- unit_test/options_test.py | 5 +++++ 7 files changed, 61 insertions(+), 12 deletions(-) diff --git a/bin/generate_schema.py b/bin/generate_schema.py index fe5f09187..d06013788 100755 --- a/bin/generate_schema.py +++ b/bin/generate_schema.py @@ -58,21 +58,23 @@ 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: diff --git a/cibuildwheel/frontend.py b/cibuildwheel/frontend.py index 7973354fe..8856a7117 100644 --- a/cibuildwheel/frontend.py +++ b/cibuildwheel/frontend.py @@ -7,7 +7,7 @@ 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) diff --git a/cibuildwheel/platforms/ios.py b/cibuildwheel/platforms/ios.py index eba04c35d..fcf7d3302 100644 --- a/cibuildwheel/platforms/ios.py +++ b/cibuildwheel/platforms/ios.py @@ -282,7 +282,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) @@ -439,7 +439,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) diff --git a/cibuildwheel/platforms/linux.py b/cibuildwheel/platforms/linux.py index 8411a935a..ab558d90a 100644 --- a/cibuildwheel/platforms/linux.py +++ b/cibuildwheel/platforms/linux.py @@ -205,7 +205,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...") @@ -305,6 +305,18 @@ def build_in_container( ], env=env, ) + case "uv": + container.call( + [ + "uv", + "build", + container_package_dir, + "--wheel", + f"--out-dir={built_wheel_dir}", + *extra_flags, + ], + env=env, + ) case _: assert_never(build_frontend) diff --git a/cibuildwheel/platforms/macos.py b/cibuildwheel/platforms/macos.py index 05d5d1623..d734befb9 100644 --- a/cibuildwheel/platforms/macos.py +++ b/cibuildwheel/platforms/macos.py @@ -217,7 +217,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] @@ -377,6 +377,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) @@ -409,7 +420,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" @@ -500,6 +511,16 @@ def build(options: Options, tmp_path: Path) -> None: *extra_flags, env=build_env, ) + case "uv": + call( + "uv", + "build", + build_options.package_dir, + "--wheel", + f"--out-dir={built_wheel_dir}", + *extra_flags, + env=build_env, + ) case _: assert_never(build_frontend) diff --git a/cibuildwheel/platforms/windows.py b/cibuildwheel/platforms/windows.py index ffc0d7761..e7e23a825 100644 --- a/cibuildwheel/platforms/windows.py +++ b/cibuildwheel/platforms/windows.py @@ -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...") @@ -403,8 +403,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 @@ -508,6 +507,16 @@ def build(options: Options, tmp_path: Path) -> None: *extra_flags, env=build_env, ) + case "uv": + call( + "uv", + "build", + build_options.package_dir, + "--wheel", + f"--out-dir={built_wheel_dir}", + *extra_flags, + env=build_env, + ) case _: assert_never(build_frontend) diff --git a/unit_test/options_test.py b/unit_test/options_test.py index 52f2f1adf..4a225628e 100644 --- a/unit_test/options_test.py +++ b/unit_test/options_test.py @@ -327,6 +327,11 @@ def test_environment_pass_references(): "build", [], ), + ( + 'build-frontend = "uv"', + "uv", + [], + ), ( 'build-frontend = {name = "build"}', "build", From 2ae26f9b68099fad27f14b2ec0f7eaea4cadcfb2 Mon Sep 17 00:00:00 2001 From: Henry Schreiner Date: Wed, 26 Mar 2025 10:35:50 -0400 Subject: [PATCH 2/8] tests: add uv to tests Signed-off-by: Henry Schreiner --- test/conftest.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/test/conftest.py b/test/conftest.py index 88d6175b6..e9c6a3216 100644 --- a/test/conftest.py +++ b/test/conftest.py @@ -151,11 +151,13 @@ def docker_warmup_fixture( return None -@pytest.fixture(params=["pip", "build"]) +@pytest.fixture(params=["pip", "build", "uv"]) def build_frontend_env_nouv(request: pytest.FixtureRequest) -> dict[str, str]: frontend = request.param - if get_platform() == "pyodide" and frontend == "pip": - pytest.skip("Can't use pip as build frontend for pyodide platform") + if get_platform() == "pyodide" and frontend in {"pip", "uv"}: + pytest.skip("Can't use pip or uv as build frontend for pyodide platform") + if frontend == "uv" and find_uv() is None: + pytest.skip("Can't find uv") return {"CIBW_BUILD_FRONTEND": frontend} From e0fae531eaa7c1e747844fc0703b6ebf199c2bbc Mon Sep 17 00:00:00 2001 From: Henry Schreiner Date: Wed, 26 Mar 2025 12:35:48 -0400 Subject: [PATCH 3/8] fix: uv doesn't pick special Pythons at the top of the path Signed-off-by: Henry Schreiner --- cibuildwheel/platforms/linux.py | 1 + cibuildwheel/platforms/macos.py | 1 + cibuildwheel/platforms/windows.py | 1 + 3 files changed, 3 insertions(+) diff --git a/cibuildwheel/platforms/linux.py b/cibuildwheel/platforms/linux.py index ab558d90a..c954e8e3b 100644 --- a/cibuildwheel/platforms/linux.py +++ b/cibuildwheel/platforms/linux.py @@ -310,6 +310,7 @@ def build_in_container( [ "uv", "build", + "--python=python", container_package_dir, "--wheel", f"--out-dir={built_wheel_dir}", diff --git a/cibuildwheel/platforms/macos.py b/cibuildwheel/platforms/macos.py index d734befb9..89981f2b9 100644 --- a/cibuildwheel/platforms/macos.py +++ b/cibuildwheel/platforms/macos.py @@ -515,6 +515,7 @@ def build(options: Options, tmp_path: Path) -> None: call( "uv", "build", + "--python=python", build_options.package_dir, "--wheel", f"--out-dir={built_wheel_dir}", diff --git a/cibuildwheel/platforms/windows.py b/cibuildwheel/platforms/windows.py index e7e23a825..31dec987a 100644 --- a/cibuildwheel/platforms/windows.py +++ b/cibuildwheel/platforms/windows.py @@ -511,6 +511,7 @@ def build(options: Options, tmp_path: Path) -> None: call( "uv", "build", + "--python=python", build_options.package_dir, "--wheel", f"--out-dir={built_wheel_dir}", From 831e7e8eb815a43c927457b1ed894a83eff3f1ac Mon Sep 17 00:00:00 2001 From: Henry Schreiner Date: Fri, 28 Mar 2025 12:40:17 -0400 Subject: [PATCH 4/8] fix: set uv build constraints too Signed-off-by: Henry Schreiner --- cibuildwheel/util/packaging.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cibuildwheel/util/packaging.py b/cibuildwheel/util/packaging.py index 578f82e44..712f4167e 100644 --- a/cibuildwheel/util/packaging.py +++ b/cibuildwheel/util/packaging.py @@ -202,6 +202,6 @@ def combine_constraints( user_constraints = env.get("PIP_CONSTRAINT") - env["UV_CONSTRAINT"] = env["PIP_CONSTRAINT"] = " ".join( + env["UV_BUILD_CONSTRAINT"] = env["UV_CONSTRAINT"] = env["PIP_CONSTRAINT"] = " ".join( c for c in [our_constraints, user_constraints] if c ) From 19699c7de3d90de5c6027797bac10458b6fcedc4 Mon Sep 17 00:00:00 2001 From: Henry Schreiner Date: Thu, 3 Apr 2025 10:38:20 -0400 Subject: [PATCH 5/8] fix: uv doesn't support PyPy 3.8 Signed-off-by: Henry Schreiner --- cibuildwheel/platforms/windows.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cibuildwheel/platforms/windows.py b/cibuildwheel/platforms/windows.py index 31dec987a..33a470086 100644 --- a/cibuildwheel/platforms/windows.py +++ b/cibuildwheel/platforms/windows.py @@ -268,7 +268,7 @@ def setup_python( raise ValueError(msg) assert base_python.exists() - if build_frontend == "build[uv]" and not can_use_uv(python_configuration): + if build_frontend in {"build[uv]", "uv"} and not can_use_uv(python_configuration): build_frontend = "build" use_uv = build_frontend in {"build[uv]", "uv"} From 619bde368a67dfdd8f02dc33a1525c27bc8cbe91 Mon Sep 17 00:00:00 2001 From: Henry Schreiner Date: Tue, 19 Aug 2025 11:33:24 -0400 Subject: [PATCH 6/8] tests: add pyproject.toml for failing test Signed-off-by: Henry Schreiner --- test/test_troubleshooting.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/test/test_troubleshooting.py b/test/test_troubleshooting.py index 1b8e8a37f..f6e412944 100644 --- a/test/test_troubleshooting.py +++ b/test/test_troubleshooting.py @@ -7,11 +7,18 @@ SO_FILE_WARNING = "NOTE: Shared object (.so) files found in this project." +PYPROJECT_TOML = """ +[build-system] +requires = ["setuptools"] +build-backend = "setuptools.build_meta:__legacy__" +""" + @pytest.mark.parametrize("project_contains_so_files", [False, True]) def test_failed_build_with_so_files(tmp_path, capfd, build_frontend_env, project_contains_so_files): project = TestProject() project.files["setup.py"] = "raise Exception('this build will fail')\n" + project.files["pyproject.toml"] = PYPROJECT_TOML if project_contains_so_files: project.files["libnothing.so"] = "" From 15a5095c955fe8ab32733a65101b81bed47cb77e Mon Sep 17 00:00:00 2001 From: Henry Schreiner Date: Wed, 10 Sep 2025 10:09:26 -0400 Subject: [PATCH 7/8] Revert "tests: add pyproject.toml for failing test" This reverts commit 619bde368a67dfdd8f02dc33a1525c27bc8cbe91. --- test/test_troubleshooting.py | 7 ------- 1 file changed, 7 deletions(-) diff --git a/test/test_troubleshooting.py b/test/test_troubleshooting.py index f6e412944..1b8e8a37f 100644 --- a/test/test_troubleshooting.py +++ b/test/test_troubleshooting.py @@ -7,18 +7,11 @@ SO_FILE_WARNING = "NOTE: Shared object (.so) files found in this project." -PYPROJECT_TOML = """ -[build-system] -requires = ["setuptools"] -build-backend = "setuptools.build_meta:__legacy__" -""" - @pytest.mark.parametrize("project_contains_so_files", [False, True]) def test_failed_build_with_so_files(tmp_path, capfd, build_frontend_env, project_contains_so_files): project = TestProject() project.files["setup.py"] = "raise Exception('this build will fail')\n" - project.files["pyproject.toml"] = PYPROJECT_TOML if project_contains_so_files: project.files["libnothing.so"] = "" From 75fddedabd1c6b47b1718a19dea794ced4c2b519 Mon Sep 17 00:00:00 2001 From: Henry Schreiner Date: Wed, 10 Sep 2025 10:11:37 -0400 Subject: [PATCH 8/8] tests: skip uv output test Signed-off-by: Henry Schreiner --- test/test_troubleshooting.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/test/test_troubleshooting.py b/test/test_troubleshooting.py index 1b8e8a37f..afb918b9c 100644 --- a/test/test_troubleshooting.py +++ b/test/test_troubleshooting.py @@ -10,6 +10,8 @@ @pytest.mark.parametrize("project_contains_so_files", [False, True]) def test_failed_build_with_so_files(tmp_path, capfd, build_frontend_env, project_contains_so_files): + if build_frontend_env == "uv" and project_contains_so_files: + pytest.skip("UV doesn't show this output for some reason") project = TestProject() project.files["setup.py"] = "raise Exception('this build will fail')\n" if project_contains_so_files: