2025-07-23 22:18:33 +01:00
|
|
|
import os
|
|
|
|
|
import platform
|
|
|
|
|
import re
|
2025-09-08 15:58:03 +01:00
|
|
|
import sys
|
2026-04-01 10:23:02 -04:00
|
|
|
import typing
|
|
|
|
|
from collections.abc import Callable
|
2025-07-23 22:18:33 +01:00
|
|
|
from dataclasses import dataclass
|
2025-09-08 15:58:03 +01:00
|
|
|
from pathlib import Path
|
2026-01-25 22:13:03 +08:00
|
|
|
from shutil import rmtree, which
|
2025-07-23 22:18:33 +01:00
|
|
|
from subprocess import CalledProcessError
|
|
|
|
|
from textwrap import dedent
|
|
|
|
|
from zipfile import ZipFile
|
|
|
|
|
|
|
|
|
|
import pytest
|
|
|
|
|
|
2026-05-29 05:27:28 +01:00
|
|
|
from .test_projects import new_c_project, new_meson_project
|
2025-07-23 22:18:33 +01:00
|
|
|
from .utils import cibuildwheel_run, expected_wheels
|
|
|
|
|
|
2025-07-27 06:12:42 -04:00
|
|
|
pytestmark = pytest.mark.android
|
|
|
|
|
|
|
|
|
|
|
2025-07-23 22:18:33 +01:00
|
|
|
CIBW_PLATFORM = os.environ.get("CIBW_PLATFORM", "android")
|
|
|
|
|
if CIBW_PLATFORM != "android":
|
|
|
|
|
pytest.skip(f"{CIBW_PLATFORM=}", allow_module_level=True)
|
|
|
|
|
|
|
|
|
|
if (platform.system(), platform.machine()) not in [
|
|
|
|
|
("Linux", "x86_64"),
|
|
|
|
|
("Darwin", "arm64"),
|
|
|
|
|
("Darwin", "x86_64"),
|
|
|
|
|
]:
|
|
|
|
|
pytest.skip(
|
|
|
|
|
f"cibuildwheel does not support building Android wheels on "
|
|
|
|
|
f"{platform.system()} {platform.machine()}",
|
|
|
|
|
allow_module_level=True,
|
|
|
|
|
)
|
|
|
|
|
|
2025-10-25 15:29:52 +01:00
|
|
|
# Azure Pipelines does not set the CI variable.
|
|
|
|
|
ci = any(key in os.environ for key in ["CI", "TF_BUILD"])
|
2025-07-23 22:18:33 +01:00
|
|
|
|
|
|
|
|
if "ANDROID_HOME" not in os.environ:
|
|
|
|
|
msg = "ANDROID_HOME environment variable is not set"
|
2025-10-25 15:29:52 +01:00
|
|
|
|
|
|
|
|
# Fail if we're on a CI service which is supposed to have the Android SDK
|
|
|
|
|
# pre-installed; otherwise skip the module.
|
2026-04-10 13:08:09 -04:00
|
|
|
if "GITHUB_ACTIONS" in os.environ or "TF_BUILD" in os.environ:
|
2025-07-23 22:18:33 +01:00
|
|
|
pytest.fail(msg)
|
|
|
|
|
else:
|
|
|
|
|
pytest.skip(msg, allow_module_level=True)
|
|
|
|
|
|
|
|
|
|
# Many CI services don't support running the Android emulator: see platforms.md.
|
2025-10-25 15:29:52 +01:00
|
|
|
supports_emulator = (not ci) or ("GITHUB_ACTIONS" in os.environ and platform.system() == "Linux")
|
2025-07-23 22:18:33 +01:00
|
|
|
|
2026-04-01 10:23:02 -04:00
|
|
|
T = typing.TypeVar("T", bound=Callable[..., typing.Any])
|
2025-07-23 22:18:33 +01:00
|
|
|
|
2026-04-01 10:23:02 -04:00
|
|
|
|
|
|
|
|
def needs_emulator(test: T) -> T:
|
2025-07-23 22:18:33 +01:00
|
|
|
# All copies of the testbed app run on the same emulator with the same
|
|
|
|
|
# application ID, so these tests must be run serially.
|
|
|
|
|
test = pytest.mark.serial(test)
|
|
|
|
|
|
2025-10-25 15:29:52 +01:00
|
|
|
if not supports_emulator:
|
2025-07-23 22:18:33 +01:00
|
|
|
test = pytest.mark.skip("This CI platform doesn't support the emulator")(test)
|
|
|
|
|
return test
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@dataclass
|
|
|
|
|
class Architecture:
|
|
|
|
|
linux_machine: str
|
|
|
|
|
macos_machine: str
|
|
|
|
|
android_abi: str
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
archs = [
|
|
|
|
|
Architecture("aarch64", "arm64", "arm64_v8a"),
|
|
|
|
|
Architecture("x86_64", "x86_64", "x86_64"),
|
|
|
|
|
]
|
|
|
|
|
native_arch = next(
|
|
|
|
|
arch for arch in archs if platform.machine() in [arch.linux_machine, arch.macos_machine]
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
cp313_env = {
|
|
|
|
|
"CIBW_PLATFORM": "android",
|
|
|
|
|
"CIBW_BUILD": "cp313-*",
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
2026-04-01 10:23:02 -04:00
|
|
|
def test_android_home(tmp_path: Path, capfd: pytest.CaptureFixture[str]) -> None:
|
2025-07-23 22:18:33 +01:00
|
|
|
new_c_project().generate(tmp_path)
|
|
|
|
|
env = os.environ.copy()
|
|
|
|
|
del env["ANDROID_HOME"]
|
|
|
|
|
|
|
|
|
|
with pytest.raises(CalledProcessError):
|
|
|
|
|
cibuildwheel_run(tmp_path, env={**env, **cp313_env})
|
|
|
|
|
assert "ANDROID_HOME environment variable is not set" in capfd.readouterr().err
|
|
|
|
|
|
|
|
|
|
|
2025-10-25 15:29:52 +01:00
|
|
|
# android-env.sh may need to install the NDK, and it isn't safe to do that multiple
|
|
|
|
|
# times in parallel. So make sure there's at least one test which gets as far as doing
|
|
|
|
|
# a build, which is marked as serial so it will run before the parallel tests, but isn't
|
|
|
|
|
# marked as needs_emulator so it will run on all CI platforms.
|
2025-08-13 08:52:27 -04:00
|
|
|
@pytest.mark.serial
|
2026-04-01 10:23:02 -04:00
|
|
|
def test_expected_wheels(tmp_path: Path, spam_env: dict[str, str]) -> None:
|
2025-10-25 15:29:52 +01:00
|
|
|
# Since this test covers all Python versions, check the cross venv.
|
|
|
|
|
test_module = "_cross_venv_test_android"
|
|
|
|
|
project = new_c_project(setup_py_add=f"import {test_module}")
|
|
|
|
|
project.files[f"{test_module}.py"] = (Path(__file__).parent / f"{test_module}.py").read_text()
|
|
|
|
|
project.generate(tmp_path)
|
|
|
|
|
|
|
|
|
|
# Build wheels for all Python versions on the current architecture.
|
|
|
|
|
del spam_env["CIBW_BUILD"]
|
|
|
|
|
if not supports_emulator:
|
|
|
|
|
del spam_env["CIBW_TEST_COMMAND"]
|
|
|
|
|
|
|
|
|
|
wheels = cibuildwheel_run(tmp_path, add_env=spam_env)
|
2025-08-13 08:52:27 -04:00
|
|
|
assert wheels == expected_wheels(
|
|
|
|
|
"spam", "0.1.0", platform="android", machine_arch=native_arch.android_abi
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
2025-11-10 19:43:52 -05:00
|
|
|
@needs_emulator
|
2026-04-01 10:23:02 -04:00
|
|
|
def test_frontend_good(tmp_path: Path, build_frontend_env: dict[str, str]) -> None:
|
2025-07-23 22:18:33 +01:00
|
|
|
new_c_project().generate(tmp_path)
|
|
|
|
|
wheels = cibuildwheel_run(
|
|
|
|
|
tmp_path,
|
2025-11-10 19:43:52 -05:00
|
|
|
add_env={**cp313_env, **build_frontend_env, "CIBW_TEST_COMMAND": "python -m site"},
|
2025-07-23 22:18:33 +01:00
|
|
|
)
|
2026-05-29 05:27:28 +01:00
|
|
|
assert wheels == [f"spam-0.1.0-cp313-cp313-android_24_{native_arch.android_abi}.whl"]
|
2025-07-23 22:18:33 +01:00
|
|
|
|
|
|
|
|
|
2026-02-08 08:35:05 -05:00
|
|
|
@pytest.mark.parametrize("frontend", ["pip"])
|
2026-04-01 10:23:02 -04:00
|
|
|
def test_frontend_bad(frontend: str, tmp_path: Path, capfd: pytest.CaptureFixture[str]) -> None:
|
2025-07-23 22:18:33 +01:00
|
|
|
new_c_project().generate(tmp_path)
|
|
|
|
|
with pytest.raises(CalledProcessError):
|
|
|
|
|
cibuildwheel_run(
|
|
|
|
|
tmp_path,
|
|
|
|
|
add_env={**cp313_env, "CIBW_BUILD_FRONTEND": frontend},
|
|
|
|
|
)
|
2026-05-29 05:27:28 +01:00
|
|
|
assert (
|
|
|
|
|
f"Android requires the build frontend to be 'build' or 'uv', not '{frontend}'"
|
|
|
|
|
in capfd.readouterr().err
|
|
|
|
|
)
|
2025-07-23 22:18:33 +01:00
|
|
|
|
|
|
|
|
|
|
|
|
|
@needs_emulator
|
2026-04-01 10:23:02 -04:00
|
|
|
def test_archs(tmp_path: Path, capfd: pytest.CaptureFixture[str]) -> None:
|
2025-07-23 22:18:33 +01:00
|
|
|
new_c_project().generate(tmp_path)
|
|
|
|
|
|
|
|
|
|
# Build all architectures while checking the handling of the `before` commands.
|
|
|
|
|
command_pattern = 'echo "Hello from {0}, package={{package}}, python=$(which python)"'
|
|
|
|
|
output_pattern = (
|
|
|
|
|
f"Hello from {{0}}, package={tmp_path}, python=/.+/cp313-android_{{1}}/venv/bin/python"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
wheels = cibuildwheel_run(
|
|
|
|
|
tmp_path,
|
|
|
|
|
add_env={
|
|
|
|
|
**cp313_env,
|
|
|
|
|
"CIBW_ARCHS": "all",
|
|
|
|
|
"CIBW_BEFORE_ALL": "echo 'Hello from before_all'",
|
|
|
|
|
"CIBW_BEFORE_BUILD": command_pattern.format("before_build"),
|
|
|
|
|
"CIBW_BEFORE_TEST": command_pattern.format("before_test"),
|
|
|
|
|
"CIBW_TEST_COMMAND": (
|
|
|
|
|
"python -c 'import platform; print(f\"Hello from {platform.machine()}\")'"
|
|
|
|
|
),
|
|
|
|
|
},
|
|
|
|
|
)
|
2026-05-29 05:27:28 +01:00
|
|
|
assert wheels == [f"spam-0.1.0-cp313-cp313-android_24_{arch.android_abi}.whl" for arch in archs]
|
2025-07-23 22:18:33 +01:00
|
|
|
|
|
|
|
|
stdout, stderr = capfd.readouterr()
|
|
|
|
|
lines = (line for line in stdout.splitlines() if line.startswith("Hello from"))
|
|
|
|
|
assert next(lines) == "Hello from before_all"
|
|
|
|
|
|
|
|
|
|
# All architectures should be built, but only the native architecture should run tests.
|
|
|
|
|
for arch in archs:
|
|
|
|
|
abi = arch.android_abi
|
|
|
|
|
assert re.fullmatch(output_pattern.format("before_build", abi), next(lines))
|
|
|
|
|
if arch == native_arch:
|
|
|
|
|
assert re.fullmatch(output_pattern.format("before_test", abi), next(lines))
|
|
|
|
|
assert next(lines) == f"Hello from {arch.linux_machine}"
|
|
|
|
|
else:
|
|
|
|
|
assert (
|
|
|
|
|
f"Skipping tests for {arch.android_abi}, as the build machine "
|
|
|
|
|
f"only supports {native_arch.android_abi}"
|
|
|
|
|
) in stderr
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
line = next(lines)
|
|
|
|
|
except StopIteration:
|
|
|
|
|
pass
|
|
|
|
|
else:
|
|
|
|
|
pytest.fail(f"Unexpected line: {line!r}")
|
|
|
|
|
|
|
|
|
|
|
2026-04-01 10:23:02 -04:00
|
|
|
def test_build_requires(tmp_path: Path, capfd: pytest.CaptureFixture[str]) -> None:
|
2025-07-23 22:18:33 +01:00
|
|
|
# Build-time requirements should be installed for the build platform, not for Android. Prove
|
|
|
|
|
# this by installing some non-pure-Python requirements and using them in setup.py.
|
|
|
|
|
#
|
|
|
|
|
# setup_requires is installed via ProjectBuilder.get_requires_for_build.
|
|
|
|
|
project = new_c_project(
|
|
|
|
|
setup_py_setup_args_add="setup_requires=['cmake==3.31.4']",
|
|
|
|
|
setup_py_add=dedent(
|
|
|
|
|
"""\
|
|
|
|
|
if "egg_info" not in sys.argv:
|
|
|
|
|
import subprocess
|
|
|
|
|
subprocess.run(["cmake", "--version"], check=True)
|
|
|
|
|
|
|
|
|
|
from bitarray import bitarray
|
|
|
|
|
print(f"{bitarray('10110').count()=}")
|
|
|
|
|
"""
|
|
|
|
|
),
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
# [build_system] requires is installed via ProjectBuilder.build_system_requires.
|
|
|
|
|
project.files["pyproject.toml"] = dedent(
|
|
|
|
|
"""\
|
|
|
|
|
[build-system]
|
|
|
|
|
requires = ["setuptools", "wheel", "bitarray==3.3.2"]
|
|
|
|
|
"""
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
project.generate(tmp_path)
|
|
|
|
|
cibuildwheel_run(tmp_path, add_env={**cp313_env})
|
|
|
|
|
|
|
|
|
|
# Test for a specific version to minimize the chance that we ran a system cmake.
|
|
|
|
|
stdout = capfd.readouterr().out
|
|
|
|
|
assert "cmake version 3.31.4" in stdout
|
|
|
|
|
assert "bitarray('10110').count()=3" in stdout
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.fixture
|
2026-04-01 10:23:02 -04:00
|
|
|
def spam_env(tmp_path: Path) -> dict[str, str]:
|
2025-07-23 22:18:33 +01:00
|
|
|
project = new_c_project()
|
|
|
|
|
project.files["test_spam.py"] = dedent(
|
|
|
|
|
"""\
|
|
|
|
|
import spam
|
|
|
|
|
|
2026-04-01 10:23:02 -04:00
|
|
|
def test_spam() -> None:
|
2025-07-23 22:18:33 +01:00
|
|
|
assert spam.filter("ham")
|
|
|
|
|
assert not spam.filter("spam")
|
|
|
|
|
print("Spam test passed")
|
|
|
|
|
"""
|
|
|
|
|
)
|
2025-10-25 15:29:52 +01:00
|
|
|
project.files["test_empty.py"] = dedent(
|
|
|
|
|
"""\
|
2026-04-01 10:23:02 -04:00
|
|
|
def test_empty() -> None:
|
2025-10-25 15:29:52 +01:00
|
|
|
pass
|
|
|
|
|
"""
|
|
|
|
|
)
|
|
|
|
|
|
2025-07-23 22:18:33 +01:00
|
|
|
project.generate(tmp_path)
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
**cp313_env,
|
2025-10-25 15:29:52 +01:00
|
|
|
"CIBW_TEST_SOURCES": "test_spam.py test_empty.py",
|
2025-07-23 22:18:33 +01:00
|
|
|
"CIBW_TEST_REQUIRES": "pytest==8.3.5",
|
2025-10-25 15:29:52 +01:00
|
|
|
"CIBW_TEST_COMMAND": "python -m pytest",
|
2025-07-23 22:18:33 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@needs_emulator
|
|
|
|
|
@pytest.mark.parametrize(
|
|
|
|
|
("command", "expected_output"),
|
|
|
|
|
[
|
2025-10-25 15:29:52 +01:00
|
|
|
("python3 -c 'import test_spam; test_spam.test_spam()'", "Spam test passed"),
|
|
|
|
|
("python -m pytest", "=== 2 passed in "),
|
2025-07-23 22:18:33 +01:00
|
|
|
("python -m pytest test_spam.py", "=== 1 passed in "),
|
|
|
|
|
("pytest test_spam.py", "=== 1 passed in "),
|
|
|
|
|
],
|
|
|
|
|
)
|
2026-04-01 10:23:02 -04:00
|
|
|
def test_test_command_good(
|
|
|
|
|
command: str,
|
|
|
|
|
expected_output: str,
|
|
|
|
|
tmp_path: Path,
|
|
|
|
|
spam_env: dict[str, str],
|
|
|
|
|
capfd: pytest.CaptureFixture[str],
|
|
|
|
|
) -> None:
|
2025-07-23 22:18:33 +01:00
|
|
|
cibuildwheel_run(tmp_path, add_env={**spam_env, "CIBW_TEST_COMMAND": command})
|
|
|
|
|
stdout, stderr = capfd.readouterr()
|
|
|
|
|
assert expected_output in stdout
|
|
|
|
|
|
|
|
|
|
if not command.startswith("python"):
|
|
|
|
|
assert (
|
|
|
|
|
f"Test command {command!r} is not supported on Android. cibuildwheel "
|
|
|
|
|
"will try to execute it as if it started with 'python -m'."
|
|
|
|
|
) in stderr
|
|
|
|
|
|
|
|
|
|
|
2025-10-25 15:29:52 +01:00
|
|
|
BAD_FORMAT_ERROR = (
|
|
|
|
|
"Test command '{}' is not supported on Android. "
|
|
|
|
|
"Command must begin with 'python' or 'python3', and contain '-m' or '-c'."
|
|
|
|
|
)
|
|
|
|
|
BAD_PLACEHOLDER_ERROR = (
|
|
|
|
|
"Test command '{}' with a '{{project}}' or '{{package}}' placeholder "
|
|
|
|
|
"is not supported on Android"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
2025-07-23 22:18:33 +01:00
|
|
|
@needs_emulator
|
|
|
|
|
@pytest.mark.parametrize(
|
|
|
|
|
("command", "expected_output"),
|
|
|
|
|
[
|
2025-10-25 15:29:52 +01:00
|
|
|
# Build-time failure
|
|
|
|
|
("./test_spam.py", BAD_FORMAT_ERROR.format("./test_spam.py")),
|
|
|
|
|
("python test_spam.py", BAD_FORMAT_ERROR.format("python test_spam.py")),
|
|
|
|
|
("pytest {project}", BAD_PLACEHOLDER_ERROR.format("pytest {project}")),
|
|
|
|
|
("pytest {package}", BAD_PLACEHOLDER_ERROR.format("pytest {package}")),
|
2025-07-23 22:18:33 +01:00
|
|
|
# Runtime failure
|
|
|
|
|
("pytest test_ham.py", "not found: test_ham.py"),
|
|
|
|
|
],
|
|
|
|
|
)
|
2026-04-01 10:23:02 -04:00
|
|
|
def test_test_command_bad(
|
|
|
|
|
command: str,
|
|
|
|
|
expected_output: str,
|
|
|
|
|
tmp_path: Path,
|
|
|
|
|
spam_env: dict[str, str],
|
|
|
|
|
capfd: pytest.CaptureFixture[str],
|
|
|
|
|
) -> None:
|
2025-07-23 22:18:33 +01:00
|
|
|
with pytest.raises(CalledProcessError):
|
|
|
|
|
cibuildwheel_run(tmp_path, add_env={**spam_env, "CIBW_TEST_COMMAND": command})
|
|
|
|
|
assert expected_output in capfd.readouterr().err
|
|
|
|
|
|
|
|
|
|
|
2025-10-25 15:29:52 +01:00
|
|
|
@needs_emulator
|
|
|
|
|
@pytest.mark.parametrize(
|
|
|
|
|
("options", "expected"),
|
|
|
|
|
[
|
|
|
|
|
("", 0),
|
|
|
|
|
("-E", 1),
|
|
|
|
|
],
|
|
|
|
|
)
|
2026-04-01 10:23:02 -04:00
|
|
|
def test_test_command_python_options(
|
|
|
|
|
options: str, expected: int, tmp_path: Path, capfd: pytest.CaptureFixture[str]
|
|
|
|
|
) -> None:
|
2025-10-25 15:29:52 +01:00
|
|
|
project = new_c_project()
|
|
|
|
|
project.generate(tmp_path)
|
|
|
|
|
|
|
|
|
|
command = 'import sys; print(f"{sys.flags.ignore_environment=}")'
|
|
|
|
|
cibuildwheel_run(
|
|
|
|
|
tmp_path,
|
|
|
|
|
add_env={
|
|
|
|
|
**cp313_env,
|
|
|
|
|
"CIBW_TEST_COMMAND": f"python {options} -c '{command}'",
|
|
|
|
|
},
|
|
|
|
|
)
|
|
|
|
|
assert f"sys.flags.ignore_environment={expected}" in capfd.readouterr().out
|
|
|
|
|
|
|
|
|
|
|
2025-07-23 22:18:33 +01:00
|
|
|
@needs_emulator
|
2026-04-01 10:23:02 -04:00
|
|
|
def test_package_subdir(
|
|
|
|
|
tmp_path: Path, spam_env: dict[str, str], capfd: pytest.CaptureFixture[str]
|
|
|
|
|
) -> None:
|
2025-07-23 22:18:33 +01:00
|
|
|
spam_paths = list(tmp_path.iterdir())
|
|
|
|
|
package_dir = tmp_path / "package"
|
|
|
|
|
package_dir.mkdir()
|
|
|
|
|
for path in spam_paths:
|
|
|
|
|
path.rename(package_dir / path.name)
|
|
|
|
|
|
2025-10-25 15:29:52 +01:00
|
|
|
spam_env["CIBW_TEST_SOURCES"] = " ".join(
|
|
|
|
|
f"package/{path}" for path in spam_env["CIBW_TEST_SOURCES"].split()
|
2025-07-23 22:18:33 +01:00
|
|
|
)
|
2025-10-25 15:29:52 +01:00
|
|
|
cibuildwheel_run(tmp_path, package_dir, add_env=spam_env)
|
|
|
|
|
assert "=== 2 passed in " in capfd.readouterr().out
|
2025-07-23 22:18:33 +01:00
|
|
|
|
|
|
|
|
|
|
|
|
|
@needs_emulator
|
2026-04-01 10:23:02 -04:00
|
|
|
def test_no_test_sources(tmp_path: Path, capfd: pytest.CaptureFixture[str]) -> None:
|
2025-07-23 22:18:33 +01:00
|
|
|
new_c_project().generate(tmp_path)
|
|
|
|
|
with pytest.raises(CalledProcessError):
|
|
|
|
|
cibuildwheel_run(
|
|
|
|
|
tmp_path,
|
|
|
|
|
add_env={**cp313_env, "CIBW_TEST_COMMAND": "python -m unittest discover"},
|
|
|
|
|
)
|
|
|
|
|
assert (
|
|
|
|
|
"On this platform, you must copy your test files to the testbed app by "
|
|
|
|
|
"setting the `test-sources` option"
|
|
|
|
|
) in capfd.readouterr().err
|
|
|
|
|
|
|
|
|
|
|
2025-09-08 15:58:43 +01:00
|
|
|
@needs_emulator
|
2026-04-01 10:23:02 -04:00
|
|
|
def test_environment_markers(tmp_path: Path) -> None:
|
2025-09-08 15:58:43 +01:00
|
|
|
project = new_c_project()
|
|
|
|
|
test_filename = "test_environment_markers.py"
|
|
|
|
|
project.files[test_filename] = dedent(
|
|
|
|
|
"""\
|
|
|
|
|
import pytest
|
|
|
|
|
|
2026-04-01 10:23:02 -04:00
|
|
|
def test_android() -> None:
|
2025-09-08 15:58:43 +01:00
|
|
|
import certifi
|
|
|
|
|
|
2026-04-01 10:23:02 -04:00
|
|
|
def test_not_android() -> None:
|
2025-09-08 15:58:43 +01:00
|
|
|
try:
|
|
|
|
|
import platformdirs
|
|
|
|
|
except ImportError:
|
|
|
|
|
pass
|
|
|
|
|
else:
|
|
|
|
|
pytest.fail("`platformdirs` should not have been installed")
|
|
|
|
|
"""
|
|
|
|
|
)
|
|
|
|
|
project.generate(tmp_path)
|
|
|
|
|
|
|
|
|
|
cibuildwheel_run(
|
|
|
|
|
tmp_path,
|
|
|
|
|
add_env={
|
|
|
|
|
**cp313_env,
|
|
|
|
|
"CIBW_TEST_COMMAND": f"python -m pytest {test_filename}",
|
|
|
|
|
"CIBW_TEST_SOURCES": test_filename,
|
2026-06-13 06:04:58 +05:30
|
|
|
"CIBW_TEST_REQUIRES": (
|
|
|
|
|
"pytest"
|
|
|
|
|
" 'certifi; sys_platform == \"android\"'"
|
|
|
|
|
" 'platformdirs; sys_platform != \"android\"'"
|
|
|
|
|
),
|
2025-09-08 15:58:43 +01:00
|
|
|
},
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@needs_emulator
|
2026-04-01 10:23:02 -04:00
|
|
|
def test_verbosity(tmp_path: Path, capfd: pytest.CaptureFixture[str]) -> None:
|
2025-09-08 15:58:43 +01:00
|
|
|
new_c_project().generate(tmp_path)
|
|
|
|
|
test_env = {
|
|
|
|
|
**cp313_env,
|
|
|
|
|
"CIBW_TEST_COMMAND": """python -c 'print("Hello world")'""",
|
|
|
|
|
}
|
|
|
|
|
verbose_lines = [
|
|
|
|
|
"> Task :app:packageDebug", # Gradle
|
|
|
|
|
"I/TestRunner: run started: 1 tests", # Logcat
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
cibuildwheel_run(tmp_path, add_env=test_env)
|
|
|
|
|
stdout = capfd.readouterr().out
|
|
|
|
|
for line in verbose_lines:
|
|
|
|
|
assert line not in stdout
|
|
|
|
|
|
|
|
|
|
cibuildwheel_run(
|
|
|
|
|
tmp_path,
|
|
|
|
|
add_env={**test_env, "CIBW_BUILD_VERBOSITY": "1"},
|
|
|
|
|
)
|
|
|
|
|
stdout = capfd.readouterr().out
|
|
|
|
|
for line in verbose_lines:
|
|
|
|
|
assert line in stdout
|
|
|
|
|
|
|
|
|
|
|
2025-07-23 22:18:33 +01:00
|
|
|
@needs_emulator
|
2026-04-01 10:23:02 -04:00
|
|
|
def test_api_level(tmp_path: Path, capfd: pytest.CaptureFixture[str]) -> None:
|
2026-05-29 05:27:28 +01:00
|
|
|
project = new_c_project(
|
|
|
|
|
# Check that the the compiler options are set correctly.
|
|
|
|
|
spam_c_top_level_add=dedent(
|
|
|
|
|
"""\
|
|
|
|
|
#if __ANDROID_API__ != 33
|
|
|
|
|
#error Unexpected API level; the following syntax error will show the actual value:
|
|
|
|
|
__ANDROID_API__
|
|
|
|
|
#endif
|
|
|
|
|
"""
|
|
|
|
|
)
|
|
|
|
|
)
|
2025-07-23 22:18:33 +01:00
|
|
|
project.files["pyproject.toml"] = dedent(
|
|
|
|
|
"""\
|
|
|
|
|
[build-system]
|
|
|
|
|
requires = ["setuptools"]
|
|
|
|
|
|
|
|
|
|
[tool.cibuildwheel]
|
2026-05-29 05:27:28 +01:00
|
|
|
# Test setting API level in pyproject.toml (test_libcxx covers setting
|
|
|
|
|
# it in the outer environment.)
|
2025-07-23 22:18:33 +01:00
|
|
|
android.environment.ANDROID_API_LEVEL = "33"
|
|
|
|
|
android.environment.PIP_EXTRA_INDEX_URL = "https://chaquo.com/pypi-13.1"
|
|
|
|
|
"""
|
|
|
|
|
)
|
|
|
|
|
project.generate(tmp_path)
|
|
|
|
|
|
|
|
|
|
wheels = cibuildwheel_run(
|
|
|
|
|
tmp_path,
|
|
|
|
|
add_env={
|
|
|
|
|
**cp313_env,
|
|
|
|
|
# Verify that Android dependencies can be installed from the Chaquopy repository, and
|
|
|
|
|
# that wheels tagged with an older version of Android (in this case 24) are still
|
|
|
|
|
# accepted.
|
|
|
|
|
"CIBW_TEST_REQUIRES": "bitarray==3.0.0",
|
|
|
|
|
"CIBW_TEST_COMMAND": (
|
|
|
|
|
"python -c 'from bitarray import bitarray; print(~bitarray(\"01100\"))'"
|
|
|
|
|
),
|
|
|
|
|
},
|
|
|
|
|
)
|
|
|
|
|
assert wheels == [f"spam-0.1.0-cp313-cp313-android_33_{native_arch.android_abi}.whl"]
|
|
|
|
|
assert "bitarray('10011')" in capfd.readouterr().out
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@needs_emulator
|
2026-04-01 10:23:02 -04:00
|
|
|
def test_libcxx(tmp_path: Path, capfd: pytest.CaptureFixture[str]) -> None:
|
2025-07-23 22:18:33 +01:00
|
|
|
project_dir = tmp_path / "project"
|
|
|
|
|
output_dir = tmp_path / "output"
|
|
|
|
|
|
2025-09-08 15:58:03 +01:00
|
|
|
# cibuildwheel should be able to run `patchelf` and `wheel` even when its
|
|
|
|
|
# environment's `bin` directory is not on the PATH.
|
|
|
|
|
non_venv_path = ":".join(
|
|
|
|
|
item for item in os.environ["PATH"].split(":") if Path(item) != Path(sys.executable).parent
|
|
|
|
|
)
|
|
|
|
|
|
2025-07-23 22:18:33 +01:00
|
|
|
# A C++ package should include libc++, and the extension module should be able to
|
|
|
|
|
# find it using DT_RUNPATH.
|
|
|
|
|
new_c_project(setup_py_extension_args_add="language='c++'").generate(project_dir)
|
|
|
|
|
script = 'import spam; print(", ".join(f"{s}: {spam.filter(s)}" for s in ["ham", "spam"]))'
|
2025-09-08 15:58:03 +01:00
|
|
|
cp313_test_env = {
|
|
|
|
|
**cp313_env,
|
|
|
|
|
"CIBW_TEST_COMMAND": f"python -c '{script}'",
|
|
|
|
|
"PATH": non_venv_path,
|
|
|
|
|
}
|
2025-07-23 22:18:33 +01:00
|
|
|
|
2026-05-29 05:27:28 +01:00
|
|
|
# Including external libraries requires API level 24. This is enforced by auditwheel.
|
|
|
|
|
cp313_android_21_env = {
|
|
|
|
|
**cp313_test_env,
|
|
|
|
|
# Test setting API level in the outer environment (test_api_level covers setting
|
|
|
|
|
# it in pyproject.toml.)
|
|
|
|
|
"ANDROID_API_LEVEL": "21",
|
|
|
|
|
}
|
2025-07-23 22:18:33 +01:00
|
|
|
with pytest.raises(CalledProcessError):
|
2026-05-29 05:27:28 +01:00
|
|
|
cibuildwheel_run(project_dir, add_env=cp313_android_21_env, output_dir=output_dir)
|
|
|
|
|
assert (
|
|
|
|
|
"Grafting libraries with RUNPATH requires API level 24 or higher" in capfd.readouterr().err
|
2025-07-23 22:18:33 +01:00
|
|
|
)
|
2026-05-29 05:27:28 +01:00
|
|
|
|
|
|
|
|
wheels = cibuildwheel_run(project_dir, add_env=cp313_test_env, output_dir=output_dir)
|
|
|
|
|
assert wheels == [f"spam-0.1.0-cp313-cp313-android_24_{native_arch.android_abi}.whl"]
|
2025-07-23 22:18:33 +01:00
|
|
|
names = ZipFile(output_dir / wheels[0]).namelist()
|
|
|
|
|
libcxx_names = [
|
|
|
|
|
name for name in names if re.fullmatch(r"spam\.libs/libc\+\+_shared-[0-9a-f]{8}\.so", name)
|
|
|
|
|
]
|
|
|
|
|
assert len(libcxx_names) == 1
|
|
|
|
|
assert "ham: 1, spam: 0" in capfd.readouterr().out
|
|
|
|
|
|
2026-05-29 05:27:28 +01:00
|
|
|
# A C package should not include libc++, and can therefore use an older API level.
|
2025-07-23 22:18:33 +01:00
|
|
|
rmtree(project_dir)
|
|
|
|
|
rmtree(output_dir)
|
|
|
|
|
new_c_project().generate(project_dir)
|
2026-05-29 05:27:28 +01:00
|
|
|
wheels = cibuildwheel_run(project_dir, add_env=cp313_android_21_env, output_dir=output_dir)
|
|
|
|
|
assert wheels == [f"spam-0.1.0-cp313-cp313-android_21_{native_arch.android_abi}.whl"]
|
2025-07-23 22:18:33 +01:00
|
|
|
for name in ZipFile(output_dir / wheels[0]).namelist():
|
|
|
|
|
assert ".libs" not in name
|
2026-01-25 22:13:03 +08:00
|
|
|
|
|
|
|
|
|
2026-05-29 05:27:28 +01:00
|
|
|
@needs_emulator
|
|
|
|
|
def test_repair_none(tmp_path: Path, capfd: pytest.CaptureFixture[str]) -> None:
|
|
|
|
|
new_c_project(setup_py_extension_args_add="language='c++'").generate(tmp_path)
|
|
|
|
|
with pytest.raises(CalledProcessError):
|
|
|
|
|
cibuildwheel_run(
|
|
|
|
|
tmp_path,
|
|
|
|
|
add_env={
|
|
|
|
|
**cp313_env,
|
|
|
|
|
"CIBW_REPAIR_WHEEL_COMMAND": "",
|
|
|
|
|
"CIBW_TEST_COMMAND": "python -c 'import spam'",
|
|
|
|
|
},
|
|
|
|
|
)
|
|
|
|
|
assert 'dlopen failed: library "libc++_shared.so" not found' in capfd.readouterr().err
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_repair_ldpaths(tmp_path: Path) -> None:
|
|
|
|
|
new_c_project().generate(tmp_path)
|
|
|
|
|
repair_path = tmp_path / "repair.py"
|
|
|
|
|
repair_path.write_text(
|
|
|
|
|
dedent(
|
|
|
|
|
"""\
|
|
|
|
|
#!/usr/bin/env python
|
|
|
|
|
import shutil
|
|
|
|
|
import sys
|
|
|
|
|
from pathlib import Path
|
|
|
|
|
|
|
|
|
|
assert len(sys.argv) == 4, sys.argv
|
|
|
|
|
ldpaths = list(map(Path, sys.argv[1].split(":")))
|
|
|
|
|
dest_dir = sys.argv[2]
|
|
|
|
|
wheel = sys.argv[3]
|
|
|
|
|
|
|
|
|
|
for name in ["libc++_shared.so", "libomp.so"]:
|
|
|
|
|
assert any((lp / name).exists() for lp in ldpaths), (name, ldpaths)
|
|
|
|
|
|
|
|
|
|
shutil.copy(wheel, dest_dir)
|
|
|
|
|
"""
|
|
|
|
|
)
|
|
|
|
|
)
|
|
|
|
|
repair_path.chmod(0o755)
|
|
|
|
|
|
|
|
|
|
wheels = cibuildwheel_run(
|
|
|
|
|
tmp_path,
|
|
|
|
|
add_env={
|
|
|
|
|
**cp313_env,
|
|
|
|
|
"CIBW_REPAIR_WHEEL_COMMAND": f"{repair_path} {{ldpaths}} {{dest_dir}} {{wheel}}",
|
|
|
|
|
},
|
|
|
|
|
)
|
|
|
|
|
assert wheels == [f"spam-0.1.0-cp313-cp313-android_24_{native_arch.android_abi}.whl"]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
|
|
|
("script", "error"),
|
|
|
|
|
[
|
|
|
|
|
("", "did not produce a wheel"),
|
|
|
|
|
("touch $dest_dir/one.whl $dest_dir/two.whl", "produced multiple wheels"),
|
|
|
|
|
("touch $dest_dir/one-0.0.1-py3-none-any.whl", "pure Python wheel was generated"),
|
|
|
|
|
],
|
|
|
|
|
)
|
|
|
|
|
def test_repair_error(
|
|
|
|
|
script: str, error: str, tmp_path: Path, capfd: pytest.CaptureFixture[str]
|
|
|
|
|
) -> None:
|
|
|
|
|
new_c_project().generate(tmp_path)
|
|
|
|
|
repair_path = tmp_path / "repair.sh"
|
|
|
|
|
repair_path.write_text(
|
|
|
|
|
dedent(
|
|
|
|
|
f"""\
|
|
|
|
|
#!/bin/sh
|
|
|
|
|
dest_dir=$1
|
|
|
|
|
{script}
|
|
|
|
|
"""
|
|
|
|
|
)
|
|
|
|
|
)
|
|
|
|
|
repair_path.chmod(0o755)
|
|
|
|
|
|
|
|
|
|
with pytest.raises(CalledProcessError):
|
|
|
|
|
cibuildwheel_run(
|
|
|
|
|
tmp_path,
|
|
|
|
|
add_env={**cp313_env, "CIBW_REPAIR_WHEEL_COMMAND": f"{repair_path} {{dest_dir}}"},
|
|
|
|
|
)
|
|
|
|
|
assert error in capfd.readouterr().err
|
|
|
|
|
|
|
|
|
|
|
2026-08-15 09:05:08 +01:00
|
|
|
# This also tests integration with pkgconf, which Meson uses to find Python and
|
|
|
|
|
# OpenBLAS.
|
2026-05-29 05:27:28 +01:00
|
|
|
@needs_emulator
|
|
|
|
|
def test_meson(tmp_path: Path, capfd: pytest.CaptureFixture[str]) -> None:
|
|
|
|
|
new_meson_project(
|
|
|
|
|
project_args_add="'fortran',",
|
2026-08-15 09:05:08 +01:00
|
|
|
extension_args_add=dedent(
|
|
|
|
|
"""\
|
|
|
|
|
'fortran.f90',
|
|
|
|
|
link_language: 'fortran',
|
|
|
|
|
dependencies: [dependency('openblas', method: 'pkg-config')],
|
|
|
|
|
"""
|
|
|
|
|
),
|
|
|
|
|
# Alter spam.filter to call an OpenBLAS function, and return the value of a
|
|
|
|
|
# Fortran function.
|
|
|
|
|
spam_c_top_level_add=dedent(
|
|
|
|
|
"""\
|
|
|
|
|
#include <cblas.h>
|
|
|
|
|
int fortran_func_();
|
|
|
|
|
"""
|
|
|
|
|
),
|
|
|
|
|
spam_c_function_add=dedent(
|
|
|
|
|
"""\
|
|
|
|
|
openblas_get_config();
|
|
|
|
|
sts = fortran_func_();
|
|
|
|
|
"""
|
|
|
|
|
),
|
2026-05-29 05:27:28 +01:00
|
|
|
).generate(tmp_path)
|
|
|
|
|
|
2026-08-15 09:05:08 +01:00
|
|
|
# Add a before-build script to download OpenBLAS.
|
|
|
|
|
before_build = tmp_path / "cibw_before_build.sh"
|
|
|
|
|
before_build.write_text(
|
|
|
|
|
dedent(
|
|
|
|
|
"""\
|
|
|
|
|
#!/bin/sh
|
|
|
|
|
set -eu
|
|
|
|
|
|
|
|
|
|
openblas_dir=$(pwd)/openblas
|
|
|
|
|
rm -rf $openblas_dir
|
|
|
|
|
mkdir -p $openblas_dir
|
|
|
|
|
pip install \\
|
|
|
|
|
--target $openblas_dir \\
|
|
|
|
|
--index-url https://chaquo.com/pypi-upstream/ \\
|
|
|
|
|
chaquopy-openblas==0.3.33
|
|
|
|
|
"""
|
2026-05-29 05:27:28 +01:00
|
|
|
)
|
|
|
|
|
)
|
2026-08-15 09:05:08 +01:00
|
|
|
before_build.chmod(0o755)
|
2026-05-29 05:27:28 +01:00
|
|
|
|
|
|
|
|
# Add Fortran code.
|
|
|
|
|
(tmp_path / "fortran.f90").write_text(
|
|
|
|
|
dedent(
|
|
|
|
|
"""\
|
|
|
|
|
integer*4 function fortran_func()
|
|
|
|
|
fortran_func = 42
|
|
|
|
|
end
|
|
|
|
|
"""
|
|
|
|
|
)
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
script = 'import spam; print(f"result: {spam.filter("")}")'
|
|
|
|
|
cibuildwheel_run(
|
|
|
|
|
tmp_path,
|
2026-08-15 09:05:08 +01:00
|
|
|
add_env={
|
|
|
|
|
**cp313_env,
|
|
|
|
|
"CIBW_BEFORE_BUILD": str(before_build),
|
|
|
|
|
"CIBW_ENVIRONMENT": (
|
|
|
|
|
'PKG_CONFIG_PATH="$(pwd)/openblas/chaquopy/lib/pkgconfig" '
|
|
|
|
|
'AUDITWHEEL_LD_LIBRARY_PATH="$(pwd)/openblas/chaquopy/lib"'
|
|
|
|
|
),
|
|
|
|
|
"CIBW_TEST_COMMAND": f"python -c '{script}'",
|
|
|
|
|
},
|
2026-05-29 05:27:28 +01:00
|
|
|
)
|
|
|
|
|
assert "result: 42" in capfd.readouterr().out
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@needs_emulator
|
|
|
|
|
def test_xbuild_files(tmp_path: Path, capfd: pytest.CaptureFixture[str]) -> None:
|
|
|
|
|
# Verify that we've replaced the correct files by compiling against a non-trivial
|
|
|
|
|
# function from libnpymath.a.
|
|
|
|
|
new_c_project(
|
|
|
|
|
setup_py_add=dedent(
|
|
|
|
|
"""\
|
|
|
|
|
import numpy as np
|
|
|
|
|
np_include = np.get_include()
|
|
|
|
|
np_lib = f"{np_include}/../lib"
|
|
|
|
|
libraries.append("npymath")
|
|
|
|
|
"""
|
|
|
|
|
),
|
|
|
|
|
setup_py_extension_args_add="include_dirs=[np_include], library_dirs=[np_lib]",
|
|
|
|
|
spam_c_top_level_add="#include <numpy/halffloat.h>",
|
|
|
|
|
spam_c_function_add="sts = npy_float_to_half(42);",
|
|
|
|
|
).generate(tmp_path)
|
|
|
|
|
|
|
|
|
|
(tmp_path / "pyproject.toml").write_text(
|
|
|
|
|
dedent(
|
|
|
|
|
"""\
|
|
|
|
|
[build-system]
|
|
|
|
|
requires = ["setuptools", "numpy==2.3.2"]
|
|
|
|
|
"""
|
|
|
|
|
)
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
script = 'import spam; print(f"result: {spam.filter(""):#x}")'
|
|
|
|
|
cibuildwheel_run(
|
|
|
|
|
tmp_path,
|
|
|
|
|
add_env={
|
|
|
|
|
**cp313_env,
|
|
|
|
|
# TODO: remove this once there are official Android NumPy wheels on PyPI.
|
2026-06-28 06:05:08 +01:00
|
|
|
"PIP_EXTRA_INDEX_URL": "https://chaquo.com/pypi-upstream",
|
2026-05-29 05:27:28 +01:00
|
|
|
"CIBW_ARCHS": "all", # Include both native and non-native archs.
|
|
|
|
|
"CIBW_TEST_COMMAND": f"python -c '{script}'",
|
|
|
|
|
},
|
|
|
|
|
)
|
|
|
|
|
assert "result: 0x5140" in capfd.readouterr().out
|
|
|
|
|
|
|
|
|
|
|
2026-01-25 22:13:03 +08:00
|
|
|
@needs_emulator
|
2026-04-01 10:23:02 -04:00
|
|
|
def test_setuptools_rust(tmp_path: Path, capfd: pytest.CaptureFixture[str]) -> None:
|
2026-01-25 22:13:03 +08:00
|
|
|
"""
|
|
|
|
|
Test Android cross-compilation using the setuptools-rust toolchain.
|
|
|
|
|
"""
|
|
|
|
|
if not which("rustup") or not which("cargo"):
|
|
|
|
|
pytest.skip("rustup and cargo are required for this test")
|
|
|
|
|
|
|
|
|
|
# Initialize a basic project and override files for setuptools-rust
|
|
|
|
|
project = new_c_project()
|
|
|
|
|
project.files["pyproject.toml"] = dedent(
|
|
|
|
|
"""\
|
|
|
|
|
[build-system]
|
|
|
|
|
requires = ["setuptools", "wheel", "setuptools-rust"]
|
|
|
|
|
"""
|
|
|
|
|
)
|
|
|
|
|
project.files["setup.py"] = dedent(
|
|
|
|
|
"""\
|
|
|
|
|
from setuptools import setup
|
|
|
|
|
from setuptools_rust import Binding, RustExtension
|
|
|
|
|
|
|
|
|
|
setup(
|
|
|
|
|
name="spam",
|
|
|
|
|
version="0.1.0",
|
|
|
|
|
rust_extensions=[RustExtension("spam.rust_ext", binding=Binding.PyO3)],
|
|
|
|
|
zip_safe=False,
|
|
|
|
|
)
|
|
|
|
|
"""
|
|
|
|
|
)
|
|
|
|
|
project.files["Cargo.toml"] = dedent(
|
|
|
|
|
"""\
|
|
|
|
|
[package]
|
|
|
|
|
name = "rust_ext"
|
|
|
|
|
version = "0.1.0"
|
|
|
|
|
edition = "2021"
|
|
|
|
|
|
|
|
|
|
[lib]
|
|
|
|
|
name = "rust_ext"
|
|
|
|
|
crate-type = ["cdylib"]
|
|
|
|
|
|
|
|
|
|
[dependencies]
|
|
|
|
|
pyo3 = { version = "0.23.3", features = ["extension-module"] }
|
|
|
|
|
"""
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
# Create the Rust source directory and file
|
|
|
|
|
(tmp_path / "src").mkdir()
|
|
|
|
|
project.files["src/lib.rs"] = dedent(
|
|
|
|
|
"""\
|
|
|
|
|
use pyo3::prelude::*;
|
|
|
|
|
|
|
|
|
|
#[pyfunction]
|
|
|
|
|
fn hello() -> PyResult<String> {
|
|
|
|
|
Ok("Hello from Rust via setuptools-rust!".to_string())
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[pymodule]
|
|
|
|
|
fn rust_ext(_py: Python, m: &Bound<'_, PyModule>) -> PyResult<()> {
|
|
|
|
|
m.add_function(wrap_pyfunction!(hello, m)?)?;
|
|
|
|
|
Ok(())
|
|
|
|
|
}
|
|
|
|
|
"""
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
project.generate(tmp_path)
|
|
|
|
|
|
|
|
|
|
# Command to verify the built extension on the Android emulator
|
|
|
|
|
test_command = "python -c 'import spam.rust_ext; print(spam.rust_ext.hello())'"
|
|
|
|
|
|
|
|
|
|
# Run cibuildwheel to build and test the wheel
|
|
|
|
|
wheels = cibuildwheel_run(
|
|
|
|
|
tmp_path,
|
|
|
|
|
add_env={
|
|
|
|
|
**cp313_env,
|
|
|
|
|
"CIBW_TEST_COMMAND": test_command,
|
|
|
|
|
"CIBW_BUILD_VERBOSITY": "1",
|
|
|
|
|
},
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
# Verification
|
|
|
|
|
assert len(wheels) == 1
|
|
|
|
|
stdout = capfd.readouterr().out
|
|
|
|
|
assert "Hello from Rust via setuptools-rust!" in stdout
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@needs_emulator
|
2026-04-01 10:23:02 -04:00
|
|
|
def test_maturin(tmp_path: Path, capfd: pytest.CaptureFixture[str]) -> None:
|
2026-01-25 22:13:03 +08:00
|
|
|
"""
|
|
|
|
|
Test Android cross-compilation using the maturin backend.
|
|
|
|
|
"""
|
|
|
|
|
if not which("rustup") or not which("cargo"):
|
|
|
|
|
pytest.skip("rustup and cargo are required for this test")
|
|
|
|
|
|
|
|
|
|
project = new_c_project()
|
|
|
|
|
project.files["pyproject.toml"] = dedent(
|
|
|
|
|
"""\
|
|
|
|
|
[build-system]
|
|
|
|
|
# maturin >= 1.11.0 is required for reliable Android cross-compilation support.
|
|
|
|
|
requires = ["maturin>=1.11.0,<2.0"]
|
|
|
|
|
build-backend = "maturin"
|
|
|
|
|
|
|
|
|
|
[project]
|
|
|
|
|
name = "spam"
|
|
|
|
|
version = "0.1.0"
|
|
|
|
|
"""
|
|
|
|
|
)
|
|
|
|
|
project.files["Cargo.toml"] = dedent(
|
|
|
|
|
"""\
|
|
|
|
|
[package]
|
|
|
|
|
name = "spam"
|
|
|
|
|
version = "0.1.0"
|
|
|
|
|
edition = "2021"
|
|
|
|
|
|
|
|
|
|
[lib]
|
|
|
|
|
name = "spam"
|
|
|
|
|
crate-type = ["cdylib"]
|
|
|
|
|
|
|
|
|
|
[dependencies]
|
|
|
|
|
pyo3 = { version = "0.23.3", features = ["extension-module"] }
|
|
|
|
|
"""
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
(tmp_path / "src").mkdir()
|
|
|
|
|
project.files["src/lib.rs"] = dedent(
|
|
|
|
|
"""\
|
|
|
|
|
use pyo3::prelude::*;
|
|
|
|
|
|
|
|
|
|
#[pyfunction]
|
|
|
|
|
fn hello() -> PyResult<String> {
|
|
|
|
|
Ok("Hello from Rust via maturin!".to_string())
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[pymodule]
|
|
|
|
|
fn spam(_py: Python, m: &Bound<'_, PyModule>) -> PyResult<()> {
|
|
|
|
|
m.add_function(wrap_pyfunction!(hello, m)?)?;
|
|
|
|
|
Ok(())
|
|
|
|
|
}
|
|
|
|
|
"""
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
project.generate(tmp_path)
|
|
|
|
|
(tmp_path / "setup.py").unlink()
|
|
|
|
|
|
|
|
|
|
test_command = "python -c 'import spam; print(spam.hello())'"
|
|
|
|
|
|
|
|
|
|
wheels = cibuildwheel_run(
|
|
|
|
|
tmp_path,
|
|
|
|
|
add_env={
|
|
|
|
|
**cp313_env,
|
|
|
|
|
"CIBW_TEST_COMMAND": test_command,
|
|
|
|
|
"CIBW_BUILD_VERBOSITY": "1",
|
|
|
|
|
},
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
assert len(wheels) == 1
|
|
|
|
|
stdout = capfd.readouterr().out
|
|
|
|
|
assert "Hello from Rust via maturin!" in stdout
|