diff --git a/cibuildwheel/platforms/android.py b/cibuildwheel/platforms/android.py index cb57b907a..2a30537f0 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: @@ -147,7 +147,7 @@ def build(options: Options, tmp_path: Path) -> None: built_wheel = build_wheel(state) repaired_wheel = repair_wheel(state, built_wheel) - test_wheel(state, repaired_wheel) + test_wheel(state, repaired_wheel, build_frontend=build_options.build_frontend.name) output_wheel: Path | None = None if compatible_wheel is None: @@ -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 @@ -559,12 +565,19 @@ def soname_with_hash(src_path: Path) -> str: return src_name -def test_wheel(state: BuildState, wheel: Path) -> None: +def test_wheel(state: BuildState, wheel: Path, *, build_frontend: str) -> None: test_command = state.options.test_command if not (test_command and state.options.test_selector(state.config.identifier)): return log.step("Testing wheel...") + 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"] + native_arch = arch_synonym(platform.machine(), platforms.native_platform(), "android") if state.config.arch != native_arch: log.warning( @@ -580,15 +593,26 @@ def test_wheel(state: BuildState, wheel: Path) -> None: env=state.build_env, ) + platform_args = ( + [ + "--python-platforma", + "x86_64-linux-android" if state.config.arch == "x86_64" else "aarch64-linux-android", + ] + if use_uv + else [ + "--platform", + sysconfig_print("get_platform()", state.android_env).replace("-", "_"), + ] + ) + # Install the wheel and test-requires. site_packages_dir = state.build_path / "site-packages" site_packages_dir.mkdir() call( - "pip", + *pip, "install", "--only-binary=:all:", - "--platform", - sysconfig_print("get_platform()", state.android_env).replace("-", "_"), + *platform_args, "--target", site_packages_dir, f"{wheel}{state.options.test_extras}", diff --git a/docs/options.md b/docs/options.md index b8072ea4c..22cc8ca9b 100644 --- a/docs/options.md +++ b/docs/options.md @@ -476,10 +476,10 @@ all build and test environments. This will generally speed up cibuildwheel. Make sure you have an external uv on Windows and macOS, either by pre-installing it, or installing cibuildwheel with the uv extra, `cibuildwheel[uv]`. uv currently does not support Windows on ARM, -musllinux on s390x, Android, or iOS. Legacy dependencies like +musllinux on s390x, Pyodide, or iOS. Legacy dependencies like setuptools on Python < 3.12 and pip are not installed if using uv. -On Android and Pyodide, only "build" is supported. +On Android and Pyodide, the "pip" frontend is not supported. You can specify extra arguments to pass to the build frontend using the optional `args` option. diff --git a/test/conftest.py b/test/conftest.py index 88d6175b6..e05ef82d9 100644 --- a/test/conftest.py +++ b/test/conftest.py @@ -154,19 +154,39 @@ 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} -@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 + 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(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() + 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 a391674e8..ef551b5f6 100644 --- a/test/test_android.py +++ b/test/test_android.py @@ -103,16 +103,16 @@ def test_expected_wheels(tmp_path): ) -def test_frontend_good(tmp_path): +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": "build"}, + add_env={**cp313_env, **build_frontend_env, "CIBW_TEST_COMMAND": "python -m site"}, ) 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):