Skip to content

Commit ccbae30

Browse files
authored
feat: support uv with Android (#2587)
1 parent 1337e50 commit ccbae30

File tree

4 files changed

+78
-35
lines changed

4 files changed

+78
-35
lines changed

cibuildwheel/platforms/android.py

Lines changed: 42 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -34,14 +34,12 @@
3434
from ..util.helpers import prepare_command
3535
from ..util.packaging import find_compatible_wheel
3636
from ..util.python_build_standalone import create_python_build_standalone_environment
37-
from ..venv import constraint_flags, virtualenv
37+
from ..venv import constraint_flags, find_uv, virtualenv
3838

39-
40-
def android_triplet(identifier: str) -> str:
41-
return {
42-
"arm64_v8a": "aarch64-linux-android",
43-
"x86_64": "x86_64-linux-android",
44-
}[parse_identifier(identifier)[1]]
39+
ANDROID_TRIPLET = {
40+
"arm64_v8a": "aarch64-linux-android",
41+
"x86_64": "x86_64-linux-android",
42+
}
4543

4644

4745
def parse_identifier(identifier: str) -> tuple[str, str]:
@@ -53,6 +51,10 @@ def parse_identifier(identifier: str) -> tuple[str, str]:
5351
return (f"{major}.{minor}", arch)
5452

5553

54+
def android_triplet(identifier: str) -> str:
55+
return ANDROID_TRIPLET[parse_identifier(identifier)[1]]
56+
57+
5658
@dataclass(frozen=True)
5759
class PythonConfiguration:
5860
version: str
@@ -147,7 +149,7 @@ def build(options: Options, tmp_path: Path) -> None:
147149
built_wheel = build_wheel(state)
148150
repaired_wheel = repair_wheel(state, built_wheel)
149151

150-
test_wheel(state, repaired_wheel)
152+
test_wheel(state, repaired_wheel, build_frontend=build_options.build_frontend.name)
151153

152154
output_wheel: Path | None = None
153155
if compatible_wheel is None:
@@ -187,6 +189,13 @@ def setup_env(
187189
* android_env, which uses the environment while simulating running on Android.
188190
"""
189191
log.step("Setting up build environment...")
192+
build_frontend = build_options.build_frontend.name
193+
use_uv = build_frontend == "build[uv]"
194+
uv_path = find_uv()
195+
if use_uv and uv_path is None:
196+
msg = "uv not found"
197+
raise AssertionError(msg)
198+
pip = ["pip"] if not use_uv else [str(uv_path), "pip"]
190199

191200
# Create virtual environment
192201
python_exe = create_python_build_standalone_environment(
@@ -197,14 +206,14 @@ def setup_env(
197206
version=config.version, tmp_dir=build_path
198207
)
199208
build_env = virtualenv(
200-
config.version, python_exe, venv_dir, dependency_constraint, use_uv=False
209+
config.version, python_exe, venv_dir, dependency_constraint, use_uv=use_uv
201210
)
202211
create_cmake_toolchain(config, build_path, python_dir, build_env)
203212

204213
# Apply custom environment variables, and check environment is still valid
205214
build_env = build_options.environment.as_dictionary(build_env)
206215
build_env["PIP_DISABLE_PIP_VERSION_CHECK"] = "1"
207-
for command in ["python", "pip"]:
216+
for command in ["python"] if use_uv else ["python", "pip"]:
208217
command_path = call("which", command, env=build_env, capture_stdout=True).strip()
209218
if command_path != f"{venv_dir}/bin/{command}":
210219
msg = (
@@ -219,11 +228,10 @@ def setup_env(
219228
android_env = setup_android_env(config, python_dir, venv_dir, build_env)
220229

221230
# Install build tools
222-
build_frontend = build_options.build_frontend
223-
if build_frontend.name != "build":
231+
if build_frontend not in {"build", "build[uv]"}:
224232
msg = "Android requires the build frontend to be 'build'"
225233
raise errors.FatalError(msg)
226-
call("pip", "install", "build", *constraint_flags(dependency_constraint), env=build_env)
234+
call(*pip, "install", "build", *constraint_flags(dependency_constraint), env=build_env)
227235

228236
# Build-time requirements must be queried within android_env, because
229237
# `get_requires_for_build` can run arbitrary code in setup.py scripts, which may be
@@ -243,13 +251,13 @@ def make_extra_environ(self) -> dict[str, str]:
243251

244252
pb = ProjectBuilder.from_isolated_env(AndroidEnv(), build_options.package_dir)
245253
if pb.build_system_requires:
246-
call("pip", "install", *pb.build_system_requires, env=build_env)
254+
call(*pip, "install", *pb.build_system_requires, env=build_env)
247255

248256
requires_for_build = pb.get_requires_for_build(
249257
"wheel", parse_config_settings(build_options.config_settings)
250258
)
251259
if requires_for_build:
252-
call("pip", "install", *requires_for_build, env=build_env)
260+
call(*pip, "install", *requires_for_build, env=build_env)
253261

254262
return build_env, android_env
255263

@@ -559,12 +567,19 @@ def soname_with_hash(src_path: Path) -> str:
559567
return src_name
560568

561569

562-
def test_wheel(state: BuildState, wheel: Path) -> None:
570+
def test_wheel(state: BuildState, wheel: Path, *, build_frontend: str) -> None:
563571
test_command = state.options.test_command
564572
if not (test_command and state.options.test_selector(state.config.identifier)):
565573
return
566574

567575
log.step("Testing wheel...")
576+
use_uv = build_frontend == "build[uv]"
577+
uv_path = find_uv()
578+
if use_uv and uv_path is None:
579+
msg = "uv not found"
580+
raise AssertionError(msg)
581+
pip = ["pip"] if not use_uv else [str(uv_path), "pip"]
582+
568583
native_arch = arch_synonym(platform.machine(), platforms.native_platform(), "android")
569584
if state.config.arch != native_arch:
570585
log.warning(
@@ -580,15 +595,23 @@ def test_wheel(state: BuildState, wheel: Path) -> None:
580595
env=state.build_env,
581596
)
582597

598+
platform_args = (
599+
["--python-platform", android_triplet(state.config.identifier)]
600+
if use_uv
601+
else [
602+
"--platform",
603+
sysconfig_print("get_platform()", state.android_env).replace("-", "_"),
604+
]
605+
)
606+
583607
# Install the wheel and test-requires.
584608
site_packages_dir = state.build_path / "site-packages"
585609
site_packages_dir.mkdir()
586610
call(
587-
"pip",
611+
*pip,
588612
"install",
589613
"--only-binary=:all:",
590-
"--platform",
591-
sysconfig_print("get_platform()", state.android_env).replace("-", "_"),
614+
*platform_args,
592615
"--target",
593616
site_packages_dir,
594617
f"{wheel}{state.options.test_extras}",

docs/options.md

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -470,18 +470,17 @@ Default: `build`
470470

471471
Choose which build frontend to use.
472472

473-
You can use "build\[uv\]", which will use an external [UV][] everywhere
473+
You can use "build\[uv\]", which will use an external [uv][] everywhere
474474
possible, both through `--installer=uv` passed to build, as well as when making
475475
all build and test environments. This will generally speed up cibuildwheel.
476-
Make sure you have an external UV on Windows and macOS, either by
476+
Make sure you have an external uv on Windows and macOS, either by
477477
pre-installing it, or installing cibuildwheel with the `uv` extra, which is
478478
possible by manually passing `cibuildwheel[uv]` to installers or by using the
479479
`extras` option in the [cibuildwheel action](ci-services.md#github-actions).
480-
UV currently does not support Android, iOS nor musllinux on s390x. Legacy
481-
dependencies like setuptools on Python < 3.12 and pip are not installed if
482-
using UV.
480+
uv currently does not support iOS or musllinux on s390x. Legacy dependencies
481+
like setuptools on Python < 3.12 and pip are not installed if using uv.
483482

484-
On Android and Pyodide, only "build" is supported.
483+
On Android and Pyodide, the "pip" frontend is not supported.
485484

486485
You can specify extra arguments to pass to the build frontend using the
487486
optional `args` option.

test/conftest.py

Lines changed: 27 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -154,19 +154,39 @@ def docker_warmup_fixture(
154154
@pytest.fixture(params=["pip", "build"])
155155
def build_frontend_env_nouv(request: pytest.FixtureRequest) -> dict[str, str]:
156156
frontend = request.param
157-
if get_platform() == "pyodide" and frontend == "pip":
157+
marks = {m.name for m in request.node.iter_markers()}
158+
159+
platform = "pyodide" if "pyodide" in marks else get_platform()
160+
if platform == "pyodide" and frontend == "pip":
158161
pytest.skip("Can't use pip as build frontend for pyodide platform")
159162

160163
return {"CIBW_BUILD_FRONTEND": frontend}
161164

162165

163-
@pytest.fixture
164-
def build_frontend_env(build_frontend_env_nouv: dict[str, str]) -> dict[str, str]:
165-
frontend = build_frontend_env_nouv["CIBW_BUILD_FRONTEND"]
166-
if frontend != "build" or get_platform() == "pyodide" or find_uv() is None:
167-
return build_frontend_env_nouv
166+
@pytest.fixture(params=["pip", "build", "build[uv]"])
167+
def build_frontend_env(request: pytest.FixtureRequest) -> dict[str, str]:
168+
frontend = request.param
169+
marks = {m.name for m in request.node.iter_markers()}
170+
if "android" in marks:
171+
platform = "android"
172+
elif "ios" in marks:
173+
platform = "ios"
174+
elif "pyodide" in marks:
175+
platform = "pyodide"
176+
else:
177+
platform = get_platform()
178+
179+
if platform in {"pyodide", "ios", "android"} and frontend == "pip":
180+
pytest.skip(f"Can't use pip as build frontend for {platform}")
181+
if platform == "pyodide" and frontend == "build[uv]":
182+
pytest.skip("Can't use uv with pyodide yet")
183+
uv_path = find_uv()
184+
if uv_path is None and frontend == "build[uv]":
185+
pytest.skip("Can't find uv, so skipping uv tests")
186+
if uv_path is not None and frontend == "build" and platform not in {"android", "ios"}:
187+
pytest.skip("No need to check build when uv is present")
168188

169-
return {"CIBW_BUILD_FRONTEND": "build[uv]"}
189+
return {"CIBW_BUILD_FRONTEND": frontend}
170190

171191

172192
@pytest.fixture

test/test_android.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -118,16 +118,17 @@ def test_expected_wheels(tmp_path, spam_env):
118118
)
119119

120120

121-
def test_frontend_good(tmp_path):
121+
@needs_emulator
122+
def test_frontend_good(tmp_path, build_frontend_env):
122123
new_c_project().generate(tmp_path)
123124
wheels = cibuildwheel_run(
124125
tmp_path,
125-
add_env={**cp313_env, "CIBW_BUILD_FRONTEND": "build"},
126+
add_env={**cp313_env, **build_frontend_env, "CIBW_TEST_COMMAND": "python -m site"},
126127
)
127128
assert wheels == [f"spam-0.1.0-cp313-cp313-android_21_{native_arch.android_abi}.whl"]
128129

129130

130-
@pytest.mark.parametrize("frontend", ["build[uv]", "pip"])
131+
@pytest.mark.parametrize("frontend", ["pip"])
131132
def test_frontend_bad(frontend, tmp_path, capfd):
132133
new_c_project().generate(tmp_path)
133134
with pytest.raises(CalledProcessError):

0 commit comments

Comments
 (0)