From d139d8a1962fa6fba646b3d8f13b42354fd3dbb7 Mon Sep 17 00:00:00 2001 From: Henry Schreiner Date: Wed, 10 Sep 2025 16:51:05 -0400 Subject: [PATCH 1/5] feat: support uv with Android Signed-off-by: Henry Schreiner --- cibuildwheel/platforms/android.py | 22 ++++++++++++++-------- test/test_android.py | 7 ++++--- 2 files changed, 18 insertions(+), 11 deletions(-) diff --git a/cibuildwheel/platforms/android.py b/cibuildwheel/platforms/android.py index b3f614fba..e96933ba8 100644 --- a/cibuildwheel/platforms/android.py +++ b/cibuildwheel/platforms/android.py @@ -34,7 +34,7 @@ from ..util.helpers import prepare_command from ..util.packaging import find_compatible_wheel from ..util.python_build_standalone import create_python_build_standalone_environment -from ..venv import constraint_flags, virtualenv +from ..venv import constraint_flags, find_uv, virtualenv def android_triplet(identifier: str) -> str: @@ -187,6 +187,13 @@ def setup_env( * android_env, which uses the environment while simulating running on Android. """ log.step("Setting up build environment...") + build_frontend = build_options.build_frontend.name + use_uv = build_frontend == "build[uv]" + uv_path = find_uv() + if use_uv and uv_path is None: + msg = "uv not found" + raise AssertionError(msg) + pip = ["pip"] if not use_uv else [str(uv_path), "pip"] # Create virtual environment python_exe = create_python_build_standalone_environment( @@ -197,14 +204,14 @@ def setup_env( version=config.version, tmp_dir=build_path ) build_env = virtualenv( - config.version, python_exe, venv_dir, dependency_constraint, use_uv=False + config.version, python_exe, venv_dir, dependency_constraint, use_uv=use_uv ) create_cmake_toolchain(config, build_path, python_dir, build_env) # Apply custom environment variables, and check environment is still valid build_env = build_options.environment.as_dictionary(build_env) build_env["PIP_DISABLE_PIP_VERSION_CHECK"] = "1" - for command in ["python", "pip"]: + for command in ["python"] if use_uv else ["python", "pip"]: command_path = call("which", command, env=build_env, capture_stdout=True).strip() if command_path != f"{venv_dir}/bin/{command}": msg = ( @@ -219,11 +226,10 @@ def setup_env( android_env = setup_android_env(config, python_dir, venv_dir, build_env) # Install build tools - build_frontend = build_options.build_frontend - if build_frontend.name != "build": + if build_frontend not in {"build", "build[uv]"}: msg = "Android requires the build frontend to be 'build'" raise errors.FatalError(msg) - call("pip", "install", "build", *constraint_flags(dependency_constraint), env=build_env) + call(*pip, "install", "build", *constraint_flags(dependency_constraint), env=build_env) # Build-time requirements must be queried within android_env, because # `get_requires_for_build` can run arbitrary code in setup.py scripts, which may be @@ -243,13 +249,13 @@ def make_extra_environ(self) -> dict[str, str]: pb = ProjectBuilder.from_isolated_env(AndroidEnv(), build_options.package_dir) if pb.build_system_requires: - call("pip", "install", *pb.build_system_requires, env=build_env) + call(*pip, "install", *pb.build_system_requires, env=build_env) requires_for_build = pb.get_requires_for_build( "wheel", parse_config_settings(build_options.config_settings) ) if requires_for_build: - call("pip", "install", *requires_for_build, env=build_env) + call(*pip, "install", *requires_for_build, env=build_env) return build_env, android_env diff --git a/test/test_android.py b/test/test_android.py index a391674e8..ead99ac13 100644 --- a/test/test_android.py +++ b/test/test_android.py @@ -103,16 +103,17 @@ def test_expected_wheels(tmp_path): ) -def test_frontend_good(tmp_path): +@pytest.mark.parametrize("frontend", ["build[uv]", "pip"]) +def test_frontend_good(tmp_path, frontend): new_c_project().generate(tmp_path) wheels = cibuildwheel_run( tmp_path, - add_env={**cp313_env, "CIBW_BUILD_FRONTEND": "build"}, + add_env={**cp313_env, "CIBW_BUILD_FRONTEND": frontend}, ) assert wheels == [f"spam-0.1.0-cp313-cp313-android_21_{native_arch.android_abi}.whl"] -@pytest.mark.parametrize("frontend", ["build[uv]", "pip"]) +@pytest.mark.parametrize("frontend", ["pip"]) def test_frontend_bad(frontend, tmp_path, capfd): new_c_project().generate(tmp_path) with pytest.raises(CalledProcessError): From 5969e1da4038a19e69362e985ce3c32a275e4ead Mon Sep 17 00:00:00 2001 From: Henry Schreiner Date: Wed, 10 Sep 2025 23:42:52 -0400 Subject: [PATCH 2/5] tests: rework skips to show uv vs. build Signed-off-by: Henry Schreiner --- test/conftest.py | 20 ++++++++++++++------ test/test_android.py | 5 ++--- 2 files changed, 16 insertions(+), 9 deletions(-) diff --git a/test/conftest.py b/test/conftest.py index 88d6175b6..d5b7febdc 100644 --- a/test/conftest.py +++ b/test/conftest.py @@ -160,13 +160,21 @@ def build_frontend_env_nouv(request: pytest.FixtureRequest) -> dict[str, str]: return {"CIBW_BUILD_FRONTEND": frontend} -@pytest.fixture -def build_frontend_env(build_frontend_env_nouv: dict[str, str]) -> dict[str, str]: - frontend = build_frontend_env_nouv["CIBW_BUILD_FRONTEND"] - if frontend != "build" or get_platform() == "pyodide" or find_uv() is None: - return build_frontend_env_nouv +@pytest.fixture(params=["pip", "build", "build[uv]"]) +def build_frontend_env(request: pytest.FixtureRequest) -> dict[str, str]: + frontend = request.param + platform = get_platform() + if platform in {"pyodide", "ios", "android"} and frontend == "pip": + pytest.skip("Can't use pip as build frontend for pyodide/ios/android platform") + if platform == "pyodide" and frontend == "build[uv]": + pytest.skip("Can't use uv with pyodide yet") + uv_path = find_uv() + if uv_path is None and frontend == "build[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") - return {"CIBW_BUILD_FRONTEND": "build[uv]"} + return {"CIBW_BUILD_FRONTEND": frontend} @pytest.fixture diff --git a/test/test_android.py b/test/test_android.py index ead99ac13..0f22eb502 100644 --- a/test/test_android.py +++ b/test/test_android.py @@ -103,12 +103,11 @@ def test_expected_wheels(tmp_path): ) -@pytest.mark.parametrize("frontend", ["build[uv]", "pip"]) -def test_frontend_good(tmp_path, frontend): +def test_frontend_good(tmp_path, build_frontend_env): new_c_project().generate(tmp_path) wheels = cibuildwheel_run( tmp_path, - add_env={**cp313_env, "CIBW_BUILD_FRONTEND": frontend}, + add_env={**cp313_env, **build_frontend_env}, ) assert wheels == [f"spam-0.1.0-cp313-cp313-android_21_{native_arch.android_abi}.whl"] From dbf338560ace6db9cb8dc416fcd15b62e19dd58c Mon Sep 17 00:00:00 2001 From: Henry Schreiner Date: Thu, 11 Sep 2025 00:41:32 -0400 Subject: [PATCH 3/5] tests: rework skips to show uv vs. build (again) Signed-off-by: Henry Schreiner --- cibuildwheel/platforms/android.py | 2 +- test/conftest.py | 67 +++++++++++++++++++++++-------- 2 files changed, 52 insertions(+), 17 deletions(-) diff --git a/cibuildwheel/platforms/android.py b/cibuildwheel/platforms/android.py index e96933ba8..2293a6884 100644 --- a/cibuildwheel/platforms/android.py +++ b/cibuildwheel/platforms/android.py @@ -227,7 +227,7 @@ def setup_env( # Install build tools if build_frontend not in {"build", "build[uv]"}: - msg = "Android requires the build frontend to be 'build'" + msg = f"Android requires the build frontend to be 'build' or 'build[uv'], not {build_frontend!r}" raise errors.FatalError(msg) call(*pip, "install", "build", *constraint_flags(dependency_constraint), env=build_env) diff --git a/test/conftest.py b/test/conftest.py index d5b7febdc..23f84045c 100644 --- a/test/conftest.py +++ b/test/conftest.py @@ -151,29 +151,64 @@ def docker_warmup_fixture( return None -@pytest.fixture(params=["pip", "build"]) +PLATFORM = get_platform() +UV_PATH = find_uv() + +BUILD_FRONTEND_PARAMS = [ + pytest.param( + "build[uv]", + marks=[ + pytest.mark.skipif( + PLATFORM == "pyodide", + reason="Can't use uv with pyodide yet", + ), + pytest.mark.skipif( + UV_PATH is None, + reason="Can't find uv, so skipping uv tests", + ), + ], + id="build[uv]", + ), + pytest.param( + "build", + marks=pytest.mark.skipif( + UV_PATH is not None and PLATFORM not in {"android", "ios"}, + reason="No need to check build when build[uv] is checked (faster)", + ), + id="build", + ), + pytest.param( + "pip", + marks=pytest.mark.skipif( + PLATFORM in {"pyodide", "ios", "android"}, + reason=f"Can't use pip as build frontend for {PLATFORM}", + ), + id="pip", + ), +] + + +@pytest.fixture( + params=[ + pytest.param( + "pip", + marks=pytest.mark.skipif( + PLATFORM in {"pyodide", "ios", "android"}, + reason=f"Can't use pip as build frontend for {PLATFORM}", + ), + ), + "build", + ] +) 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") - return {"CIBW_BUILD_FRONTEND": frontend} -@pytest.fixture(params=["pip", "build", "build[uv]"]) +@pytest.fixture(params=BUILD_FRONTEND_PARAMS) def build_frontend_env(request: pytest.FixtureRequest) -> dict[str, str]: + print(f"{PLATFORM=}") frontend = request.param - platform = get_platform() - if platform in {"pyodide", "ios", "android"} and frontend == "pip": - pytest.skip("Can't use pip as build frontend for pyodide/ios/android platform") - if platform == "pyodide" and frontend == "build[uv]": - pytest.skip("Can't use uv with pyodide yet") - uv_path = find_uv() - if uv_path is None and frontend == "build[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") - return {"CIBW_BUILD_FRONTEND": frontend} From d0f6668468af142297a2b4025024b080811bac31 Mon Sep 17 00:00:00 2001 From: Henry Schreiner Date: Thu, 11 Sep 2025 00:57:53 -0400 Subject: [PATCH 4/5] Revert "tests: rework skips to show uv vs. build (again)" This reverts commit dbf338560ace6db9cb8dc416fcd15b62e19dd58c. --- cibuildwheel/platforms/android.py | 2 +- test/conftest.py | 67 ++++++++----------------------- 2 files changed, 17 insertions(+), 52 deletions(-) diff --git a/cibuildwheel/platforms/android.py b/cibuildwheel/platforms/android.py index 2293a6884..e96933ba8 100644 --- a/cibuildwheel/platforms/android.py +++ b/cibuildwheel/platforms/android.py @@ -227,7 +227,7 @@ def setup_env( # Install build tools if build_frontend not in {"build", "build[uv]"}: - msg = f"Android requires the build frontend to be 'build' or 'build[uv'], not {build_frontend!r}" + msg = "Android requires the build frontend to be 'build'" raise errors.FatalError(msg) call(*pip, "install", "build", *constraint_flags(dependency_constraint), env=build_env) diff --git a/test/conftest.py b/test/conftest.py index 23f84045c..d5b7febdc 100644 --- a/test/conftest.py +++ b/test/conftest.py @@ -151,64 +151,29 @@ def docker_warmup_fixture( return None -PLATFORM = get_platform() -UV_PATH = find_uv() - -BUILD_FRONTEND_PARAMS = [ - pytest.param( - "build[uv]", - marks=[ - pytest.mark.skipif( - PLATFORM == "pyodide", - reason="Can't use uv with pyodide yet", - ), - pytest.mark.skipif( - UV_PATH is None, - reason="Can't find uv, so skipping uv tests", - ), - ], - id="build[uv]", - ), - pytest.param( - "build", - marks=pytest.mark.skipif( - UV_PATH is not None and PLATFORM not in {"android", "ios"}, - reason="No need to check build when build[uv] is checked (faster)", - ), - id="build", - ), - pytest.param( - "pip", - marks=pytest.mark.skipif( - PLATFORM in {"pyodide", "ios", "android"}, - reason=f"Can't use pip as build frontend for {PLATFORM}", - ), - id="pip", - ), -] - - -@pytest.fixture( - params=[ - pytest.param( - "pip", - marks=pytest.mark.skipif( - PLATFORM in {"pyodide", "ios", "android"}, - reason=f"Can't use pip as build frontend for {PLATFORM}", - ), - ), - "build", - ] -) +@pytest.fixture(params=["pip", "build"]) 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") + return {"CIBW_BUILD_FRONTEND": frontend} -@pytest.fixture(params=BUILD_FRONTEND_PARAMS) +@pytest.fixture(params=["pip", "build", "build[uv]"]) def build_frontend_env(request: pytest.FixtureRequest) -> dict[str, str]: - print(f"{PLATFORM=}") frontend = request.param + platform = get_platform() + if platform in {"pyodide", "ios", "android"} and frontend == "pip": + pytest.skip("Can't use pip as build frontend for pyodide/ios/android platform") + if platform == "pyodide" and frontend == "build[uv]": + pytest.skip("Can't use uv with pyodide yet") + uv_path = find_uv() + if uv_path is None and frontend == "build[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") + return {"CIBW_BUILD_FRONTEND": frontend} From 18c45a56521e0504882eda4222de655892078d64 Mon Sep 17 00:00:00 2001 From: Henry Schreiner Date: Thu, 11 Sep 2025 01:03:06 -0400 Subject: [PATCH 5/5] tests: make skips aware of markers Signed-off-by: Henry Schreiner --- test/conftest.py | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/test/conftest.py b/test/conftest.py index d5b7febdc..e05ef82d9 100644 --- a/test/conftest.py +++ b/test/conftest.py @@ -154,7 +154,10 @@ def docker_warmup_fixture( @pytest.fixture(params=["pip", "build"]) def build_frontend_env_nouv(request: pytest.FixtureRequest) -> dict[str, str]: frontend = request.param - if get_platform() == "pyodide" and frontend == "pip": + marks = {m.name for m in request.node.iter_markers()} + + platform = "pyodide" if "pyodide" in marks else get_platform() + if platform == "pyodide" and frontend == "pip": pytest.skip("Can't use pip as build frontend for pyodide platform") return {"CIBW_BUILD_FRONTEND": frontend} @@ -163,9 +166,18 @@ def build_frontend_env_nouv(request: pytest.FixtureRequest) -> dict[str, str]: @pytest.fixture(params=["pip", "build", "build[uv]"]) def build_frontend_env(request: pytest.FixtureRequest) -> dict[str, str]: frontend = request.param - platform = get_platform() + marks = {m.name for m in request.node.iter_markers()} + if "android" in marks: + platform = "android" + elif "ios" in marks: + platform = "ios" + elif "pyodide" in marks: + platform = "pyodide" + else: + platform = get_platform() + if platform in {"pyodide", "ios", "android"} and frontend == "pip": - pytest.skip("Can't use pip as build frontend for pyodide/ios/android platform") + pytest.skip(f"Can't use pip as build frontend for {platform}") if platform == "pyodide" and frontend == "build[uv]": pytest.skip("Can't use uv with pyodide yet") uv_path = find_uv()