Skip to content
Open
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
50 changes: 37 additions & 13 deletions cibuildwheel/platforms/android.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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(
Expand All @@ -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 = (
Expand All @@ -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
Expand All @@ -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

Expand Down Expand Up @@ -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(
Expand All @@ -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}",
Expand Down
4 changes: 2 additions & 2 deletions docs/options.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
34 changes: 27 additions & 7 deletions test/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
6 changes: 3 additions & 3 deletions test/test_android.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down
Loading