feat: support uv with Android (#2587)
This commit is contained in:
@@ -34,14 +34,12 @@ from ..util.file import CIBW_CACHE_PATH, copy_test_sources, download, move_file
|
|||||||
from ..util.helpers import prepare_command
|
from ..util.helpers import prepare_command
|
||||||
from ..util.packaging import find_compatible_wheel
|
from ..util.packaging import find_compatible_wheel
|
||||||
from ..util.python_build_standalone import create_python_build_standalone_environment
|
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
|
||||||
|
|
||||||
|
ANDROID_TRIPLET = {
|
||||||
def android_triplet(identifier: str) -> str:
|
"arm64_v8a": "aarch64-linux-android",
|
||||||
return {
|
"x86_64": "x86_64-linux-android",
|
||||||
"arm64_v8a": "aarch64-linux-android",
|
}
|
||||||
"x86_64": "x86_64-linux-android",
|
|
||||||
}[parse_identifier(identifier)[1]]
|
|
||||||
|
|
||||||
|
|
||||||
def parse_identifier(identifier: str) -> tuple[str, str]:
|
def parse_identifier(identifier: str) -> tuple[str, str]:
|
||||||
@@ -53,6 +51,10 @@ def parse_identifier(identifier: str) -> tuple[str, str]:
|
|||||||
return (f"{major}.{minor}", arch)
|
return (f"{major}.{minor}", arch)
|
||||||
|
|
||||||
|
|
||||||
|
def android_triplet(identifier: str) -> str:
|
||||||
|
return ANDROID_TRIPLET[parse_identifier(identifier)[1]]
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
@dataclass(frozen=True)
|
||||||
class PythonConfiguration:
|
class PythonConfiguration:
|
||||||
version: str
|
version: str
|
||||||
@@ -147,7 +149,7 @@ def build(options: Options, tmp_path: Path) -> None:
|
|||||||
built_wheel = build_wheel(state)
|
built_wheel = build_wheel(state)
|
||||||
repaired_wheel = repair_wheel(state, built_wheel)
|
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
|
output_wheel: Path | None = None
|
||||||
if compatible_wheel is None:
|
if compatible_wheel is None:
|
||||||
@@ -187,6 +189,13 @@ def setup_env(
|
|||||||
* android_env, which uses the environment while simulating running on Android.
|
* android_env, which uses the environment while simulating running on Android.
|
||||||
"""
|
"""
|
||||||
log.step("Setting up build environment...")
|
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
|
# Create virtual environment
|
||||||
python_exe = create_python_build_standalone_environment(
|
python_exe = create_python_build_standalone_environment(
|
||||||
@@ -197,14 +206,14 @@ def setup_env(
|
|||||||
version=config.version, tmp_dir=build_path
|
version=config.version, tmp_dir=build_path
|
||||||
)
|
)
|
||||||
build_env = virtualenv(
|
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)
|
create_cmake_toolchain(config, build_path, python_dir, build_env)
|
||||||
|
|
||||||
# Apply custom environment variables, and check environment is still valid
|
# Apply custom environment variables, and check environment is still valid
|
||||||
build_env = build_options.environment.as_dictionary(build_env)
|
build_env = build_options.environment.as_dictionary(build_env)
|
||||||
build_env["PIP_DISABLE_PIP_VERSION_CHECK"] = "1"
|
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()
|
command_path = call("which", command, env=build_env, capture_stdout=True).strip()
|
||||||
if command_path != f"{venv_dir}/bin/{command}":
|
if command_path != f"{venv_dir}/bin/{command}":
|
||||||
msg = (
|
msg = (
|
||||||
@@ -219,11 +228,10 @@ def setup_env(
|
|||||||
android_env = setup_android_env(config, python_dir, venv_dir, build_env)
|
android_env = setup_android_env(config, python_dir, venv_dir, build_env)
|
||||||
|
|
||||||
# Install build tools
|
# Install build tools
|
||||||
build_frontend = build_options.build_frontend
|
if build_frontend not in {"build", "build[uv]"}:
|
||||||
if build_frontend.name != "build":
|
|
||||||
msg = "Android requires the build frontend to be 'build'"
|
msg = "Android requires the build frontend to be 'build'"
|
||||||
raise errors.FatalError(msg)
|
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
|
# 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
|
# `get_requires_for_build` can run arbitrary code in setup.py scripts, which may be
|
||||||
@@ -243,13 +251,13 @@ def setup_env(
|
|||||||
|
|
||||||
pb = ProjectBuilder.from_isolated_env(AndroidEnv(), build_options.package_dir)
|
pb = ProjectBuilder.from_isolated_env(AndroidEnv(), build_options.package_dir)
|
||||||
if pb.build_system_requires:
|
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(
|
requires_for_build = pb.get_requires_for_build(
|
||||||
"wheel", parse_config_settings(build_options.config_settings)
|
"wheel", parse_config_settings(build_options.config_settings)
|
||||||
)
|
)
|
||||||
if requires_for_build:
|
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
|
return build_env, android_env
|
||||||
|
|
||||||
@@ -559,12 +567,19 @@ def soname_with_hash(src_path: Path) -> str:
|
|||||||
return src_name
|
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
|
test_command = state.options.test_command
|
||||||
if not (test_command and state.options.test_selector(state.config.identifier)):
|
if not (test_command and state.options.test_selector(state.config.identifier)):
|
||||||
return
|
return
|
||||||
|
|
||||||
log.step("Testing wheel...")
|
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")
|
native_arch = arch_synonym(platform.machine(), platforms.native_platform(), "android")
|
||||||
if state.config.arch != native_arch:
|
if state.config.arch != native_arch:
|
||||||
log.warning(
|
log.warning(
|
||||||
@@ -580,15 +595,23 @@ def test_wheel(state: BuildState, wheel: Path) -> None:
|
|||||||
env=state.build_env,
|
env=state.build_env,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
platform_args = (
|
||||||
|
["--python-platform", android_triplet(state.config.identifier)]
|
||||||
|
if use_uv
|
||||||
|
else [
|
||||||
|
"--platform",
|
||||||
|
sysconfig_print("get_platform()", state.android_env).replace("-", "_"),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
# Install the wheel and test-requires.
|
# Install the wheel and test-requires.
|
||||||
site_packages_dir = state.build_path / "site-packages"
|
site_packages_dir = state.build_path / "site-packages"
|
||||||
site_packages_dir.mkdir()
|
site_packages_dir.mkdir()
|
||||||
call(
|
call(
|
||||||
"pip",
|
*pip,
|
||||||
"install",
|
"install",
|
||||||
"--only-binary=:all:",
|
"--only-binary=:all:",
|
||||||
"--platform",
|
*platform_args,
|
||||||
sysconfig_print("get_platform()", state.android_env).replace("-", "_"),
|
|
||||||
"--target",
|
"--target",
|
||||||
site_packages_dir,
|
site_packages_dir,
|
||||||
f"{wheel}{state.options.test_extras}",
|
f"{wheel}{state.options.test_extras}",
|
||||||
|
|||||||
+5
-6
@@ -470,18 +470,17 @@ Default: `build`
|
|||||||
|
|
||||||
Choose which build frontend to use.
|
Choose which build frontend to use.
|
||||||
|
|
||||||
You can use "build\[uv\]", which will use an external [UV][] everywhere
|
You can use "build\[uv\]", which will use an external [uv][] everywhere
|
||||||
possible, both through `--installer=uv` passed to build, as well as when making
|
possible, both through `--installer=uv` passed to build, as well as when making
|
||||||
all build and test environments. This will generally speed up cibuildwheel.
|
all build and test environments. This will generally speed up cibuildwheel.
|
||||||
Make sure you have an external UV on Windows and macOS, either by
|
Make sure you have an external uv on Windows and macOS, either by
|
||||||
pre-installing it, or installing cibuildwheel with the `uv` extra, which is
|
pre-installing it, or installing cibuildwheel with the `uv` extra, which is
|
||||||
possible by manually passing `cibuildwheel[uv]` to installers or by using the
|
possible by manually passing `cibuildwheel[uv]` to installers or by using the
|
||||||
`extras` option in the [cibuildwheel action](ci-services.md#github-actions).
|
`extras` option in the [cibuildwheel action](ci-services.md#github-actions).
|
||||||
UV currently does not support Android, iOS nor musllinux on s390x. Legacy
|
uv currently does not support iOS or musllinux on s390x. Legacy dependencies
|
||||||
dependencies like setuptools on Python < 3.12 and pip are not installed if
|
like setuptools on Python < 3.12 and pip are not installed if using uv.
|
||||||
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
|
You can specify extra arguments to pass to the build frontend using the
|
||||||
optional `args` option.
|
optional `args` option.
|
||||||
|
|||||||
+27
-7
@@ -154,19 +154,39 @@ def docker_warmup_fixture(
|
|||||||
@pytest.fixture(params=["pip", "build"])
|
@pytest.fixture(params=["pip", "build"])
|
||||||
def build_frontend_env_nouv(request: pytest.FixtureRequest) -> dict[str, str]:
|
def build_frontend_env_nouv(request: pytest.FixtureRequest) -> dict[str, str]:
|
||||||
frontend = request.param
|
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")
|
pytest.skip("Can't use pip as build frontend for pyodide platform")
|
||||||
|
|
||||||
return {"CIBW_BUILD_FRONTEND": frontend}
|
return {"CIBW_BUILD_FRONTEND": frontend}
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture(params=["pip", "build", "build[uv]"])
|
||||||
def build_frontend_env(build_frontend_env_nouv: dict[str, str]) -> dict[str, str]:
|
def build_frontend_env(request: pytest.FixtureRequest) -> dict[str, str]:
|
||||||
frontend = build_frontend_env_nouv["CIBW_BUILD_FRONTEND"]
|
frontend = request.param
|
||||||
if frontend != "build" or get_platform() == "pyodide" or find_uv() is None:
|
marks = {m.name for m in request.node.iter_markers()}
|
||||||
return build_frontend_env_nouv
|
if "android" in marks:
|
||||||
|
platform = "android"
|
||||||
|
elif "ios" in marks:
|
||||||
|
platform = "ios"
|
||||||
|
elif "pyodide" in marks:
|
||||||
|
platform = "pyodide"
|
||||||
|
else:
|
||||||
|
platform = get_platform()
|
||||||
|
|
||||||
return {"CIBW_BUILD_FRONTEND": "build[uv]"}
|
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": frontend}
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
|
|||||||
@@ -118,16 +118,17 @@ def test_expected_wheels(tmp_path, spam_env):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def test_frontend_good(tmp_path):
|
@needs_emulator
|
||||||
|
def test_frontend_good(tmp_path, build_frontend_env):
|
||||||
new_c_project().generate(tmp_path)
|
new_c_project().generate(tmp_path)
|
||||||
wheels = cibuildwheel_run(
|
wheels = cibuildwheel_run(
|
||||||
tmp_path,
|
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"]
|
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):
|
def test_frontend_bad(frontend, tmp_path, capfd):
|
||||||
new_c_project().generate(tmp_path)
|
new_c_project().generate(tmp_path)
|
||||||
with pytest.raises(CalledProcessError):
|
with pytest.raises(CalledProcessError):
|
||||||
|
|||||||
Reference in New Issue
Block a user