feat: Android improvements needed for building NumPy and related packages (#2695)
* Use auditwheel on Android * Add auditwheel command to defaults * pkgconfig fixes * Pre-import ctypes before monkey-patching in _cross_venv * Set PKG_CONFIG and PKG_CONFIG_RELOCATE_PATHS variables * Initial attempt at using mzakharo/android-gfortran * Switch to using termux/ndk-toolchain-clang-with-flang * Move PKG_CONFIG variables from build_env to android_env * Simplify flang installation * Add cross build files for NumPy * Add ldpaths entry for libomp * Add `--rm` to docker command line * Make Rust and Fortran shims consistent * Update documentation * Default to API level 24 on all Python versions * Clarify comments * Cleanups * Fix tests: * Set up Android env after installing pkgconf * Add tests for successfully using an older API level * Previous commit's auditwheel failure is fixed in the auditwheel PR * Update how-it-works diagram * Add tests for repair errors * Add more repair tests * Add test for Meson and Fortran * Add test for cross build files * Improve test_api_level error message * Add xbuild-files option * use pypa/auditwheel@main * Remove dependencies which are no longer needed * Fix README * Update to auditwheel 6.7.0 * Fix compatibility with pkgconf 2.5.1.post2 * Documentation clarifications Co-authored-by: Joe Rickerby <joerick@mac.com> * Fortran shim improvements * Add Jinja variables to new_meson_project * Update run_example_ci_configs for changed new_meson_project signature * Add missing dependency to run_example_ci_configs --------- Co-authored-by: mayeut <mayeut@users.noreply.github.com> Co-authored-by: Joe Rickerby <joerick@mac.com>
This commit is contained in:
co-authored by
Joe Rickerby
mayeut
parent
e3e7cc9e07
commit
1cf64d0d3c
@@ -39,7 +39,6 @@ repos:
|
||||
- nox>=2025.2.9
|
||||
- orjson
|
||||
- packaging
|
||||
- pyelftools
|
||||
- pygithub
|
||||
- pytest
|
||||
- rich
|
||||
|
||||
@@ -152,6 +152,7 @@ The following diagram summarises the steps that cibuildwheel takes on each platf
|
||||
| | [`before-all`](https://cibuildwheel.pypa.io/en/stable/options/#before-all) | Execute a shell command on the build system before any wheels are built. |
|
||||
| | [`before-build`](https://cibuildwheel.pypa.io/en/stable/options/#before-build) | Execute a shell command preparing each wheel's build |
|
||||
| | [`xbuild-tools`](https://cibuildwheel.pypa.io/en/stable/options/#xbuild-tools) | Binaries on the path that should be included in an isolated cross-build environment. |
|
||||
| | [`xbuild-files`](https://cibuildwheel.pypa.io/en/stable/options/#xbuild-files) | Platform-specific files in the build environment |
|
||||
| | [`repair-wheel-command`](https://cibuildwheel.pypa.io/en/stable/options/#repair-wheel-command) | Execute a shell command to repair each built wheel |
|
||||
| | [`manylinux-*-image`<br>`musllinux-*-image`](https://cibuildwheel.pypa.io/en/stable/options/#linux-image) | Specify manylinux / musllinux container images |
|
||||
| | [`container-engine`](https://cibuildwheel.pypa.io/en/stable/options/#container-engine) | Specify the container engine to use when building Linux wheels |
|
||||
@@ -173,7 +174,7 @@ The following diagram summarises the steps that cibuildwheel takes on each platf
|
||||
| | [`build-verbosity`](https://cibuildwheel.pypa.io/en/stable/options/#build-verbosity) | Increase/decrease the output of the build |
|
||||
|
||||
|
||||
<!--[[[end]]] (sum: b7YIjCyCkf) -->
|
||||
<!--[[[end]]] (sum: Of/28Z7Nut) -->
|
||||
|
||||
These options can be specified in a pyproject.toml file, or as environment variables, see [configuration docs](https://cibuildwheel.pypa.io/en/latest/configuration/).
|
||||
|
||||
|
||||
+15
-8
@@ -15,6 +15,8 @@ parser = argparse.ArgumentParser(allow_abbrev=False)
|
||||
parser.add_argument("--schemastore", action="store_true", help="Generate schema_store version")
|
||||
args = parser.parse_args()
|
||||
|
||||
# The defaults in the schema are used by external tools for validation and IDE support. They
|
||||
# should match the values in defaults.toml, which are used by cibuildwheel itself.
|
||||
starter = """
|
||||
$schema: http://json-schema.org/draft-07/schema#
|
||||
$id: https://github.com/pypa/cibuildwheel/blob/main/cibuildwheel/resources/cibuildwheel.schema.json
|
||||
@@ -200,6 +202,9 @@ properties:
|
||||
xbuild-tools:
|
||||
description: Binaries on the path that should be included in an isolated cross-build environment
|
||||
type: string_array
|
||||
xbuild-files:
|
||||
description: Platform-specific files in the build environment
|
||||
type: string_table_array
|
||||
pyodide-version:
|
||||
type: string
|
||||
description: Specify the version of Pyodide to use
|
||||
@@ -319,6 +324,7 @@ items:
|
||||
before-all: {"$ref": "#/$defs/inherit"}
|
||||
before-build: {"$ref": "#/$defs/inherit"}
|
||||
xbuild-tools: {"$ref": "#/$defs/inherit"}
|
||||
xbuild-files: {"$ref": "#/$defs/inherit"}
|
||||
before-test: {"$ref": "#/$defs/inherit"}
|
||||
config-settings: {"$ref": "#/$defs/inherit"}
|
||||
container-engine: {"$ref": "#/$defs/inherit"}
|
||||
@@ -374,14 +380,15 @@ oses = {
|
||||
"ios": as_object(not_linux),
|
||||
}
|
||||
|
||||
oses["linux"]["properties"]["repair-wheel-command"] = {
|
||||
**schema["properties"]["repair-wheel-command"],
|
||||
"default": "auditwheel repair -w {dest_dir} {wheel}",
|
||||
}
|
||||
oses["macos"]["properties"]["repair-wheel-command"] = {
|
||||
**schema["properties"]["repair-wheel-command"],
|
||||
"default": "delocate-wheel --require-archs {delocate_archs} -w {dest_dir} -v {wheel}",
|
||||
}
|
||||
for os_name, command in [
|
||||
("linux", "auditwheel repair -w {dest_dir} {wheel}"),
|
||||
("macos", "delocate-wheel --require-archs {delocate_archs} -w {dest_dir} -v {wheel}"),
|
||||
("android", "auditwheel repair --ldpaths {ldpaths} -w {dest_dir} {wheel}"),
|
||||
]:
|
||||
oses[os_name]["properties"]["repair-wheel-command"] = {
|
||||
**schema["properties"]["repair-wheel-command"],
|
||||
"default": command,
|
||||
}
|
||||
|
||||
del oses["linux"]["properties"]["dependency-versions"]
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
# /// script
|
||||
# dependencies = [
|
||||
# "click",
|
||||
# "jinja2",
|
||||
# ]
|
||||
# ///
|
||||
|
||||
@@ -39,13 +40,16 @@ def generate_project(path: Path, build_backend: BuildBackend) -> None:
|
||||
sys.path.insert(0, "")
|
||||
match build_backend:
|
||||
case "meson":
|
||||
from test.test_projects.meson import new_meson_project as new_project # noqa: PLC0415
|
||||
from test.test_projects.meson import new_meson_project # noqa: PLC0415
|
||||
|
||||
project = new_meson_project()
|
||||
case "setuptools":
|
||||
from test.test_projects.setuptools import new_c_project as new_project # noqa: PLC0415
|
||||
from test.test_projects.setuptools import new_c_project # noqa: PLC0415
|
||||
|
||||
project = new_c_project()
|
||||
case _:
|
||||
typing.assert_never(build_backend)
|
||||
|
||||
project = new_project()
|
||||
project.generate(path)
|
||||
|
||||
|
||||
|
||||
@@ -120,6 +120,7 @@ class BuildOptions:
|
||||
before_all: str
|
||||
before_build: str | None
|
||||
xbuild_tools: list[str] | None
|
||||
xbuild_files: dict[str, list[str]]
|
||||
repair_command: str
|
||||
manylinux_images: dict[str, str] | None
|
||||
musllinux_images: dict[str, str] | None
|
||||
@@ -772,6 +773,14 @@ class Options:
|
||||
if xbuild_tools == ["\u0000"]:
|
||||
xbuild_tools = None
|
||||
|
||||
xbuild_files = parse_key_value_string(
|
||||
self.reader.get(
|
||||
"xbuild-files",
|
||||
option_format=ShlexTableFormat(sep="; ", pair_sep=":", allow_merge=False),
|
||||
),
|
||||
kw_arg_names=["*"],
|
||||
)
|
||||
|
||||
test_sources = shlex.split(
|
||||
self.reader.get(
|
||||
"test-sources", option_format=ListFormat(sep=" ", quote=shlex.quote)
|
||||
@@ -925,6 +934,7 @@ class Options:
|
||||
before_all=before_all,
|
||||
build_verbosity=build_verbosity,
|
||||
xbuild_tools=xbuild_tools,
|
||||
xbuild_files=xbuild_files,
|
||||
repair_command=repair_command,
|
||||
environment=environment,
|
||||
dependency_constraints=dependency_constraints,
|
||||
|
||||
+130
-180
@@ -1,16 +1,13 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import csv
|
||||
import hashlib
|
||||
import os
|
||||
import platform
|
||||
import re
|
||||
import shlex
|
||||
import shutil
|
||||
import subprocess
|
||||
import sysconfig
|
||||
import sys
|
||||
from dataclasses import dataclass
|
||||
from os.path import relpath
|
||||
from pathlib import Path
|
||||
from pprint import pprint
|
||||
from runpy import run_path
|
||||
@@ -19,9 +16,8 @@ from typing import Any
|
||||
|
||||
from build import ProjectBuilder
|
||||
from build.env import IsolatedEnv
|
||||
from elftools.common.exceptions import ELFError
|
||||
from elftools.elf.elffile import ELFFile
|
||||
from filelock import FileLock
|
||||
from packaging.utils import canonicalize_name
|
||||
|
||||
from cibuildwheel import errors, platforms # pylint: disable=cyclic-import
|
||||
from cibuildwheel.architecture import Architecture, arch_synonym
|
||||
@@ -42,10 +38,9 @@ from cibuildwheel.venv import constraint_flags, find_uv, virtualenv
|
||||
|
||||
TYPE_CHECKING = False
|
||||
if TYPE_CHECKING:
|
||||
from collections.abc import Iterable, Iterator, MutableMapping
|
||||
|
||||
from cibuildwheel.options import BuildOptions, Options
|
||||
from cibuildwheel.selector import BuildSelector
|
||||
from cibuildwheel.typing import PathOrStr
|
||||
|
||||
RESOURCES_ANDROID = resources.PATH / "android"
|
||||
ANDROID_TRIPLET = {
|
||||
@@ -148,6 +143,7 @@ def build(options: Options, tmp_path: Path) -> None:
|
||||
state = BuildState(
|
||||
config, build_options, build_path, python_dir, build_env, android_env
|
||||
)
|
||||
setup_xbuild_files(state)
|
||||
|
||||
compatible_wheel = find_compatible_wheel(built_wheels, config.identifier)
|
||||
if compatible_wheel:
|
||||
@@ -162,7 +158,7 @@ def build(options: Options, tmp_path: Path) -> None:
|
||||
repaired_wheel = repair_wheel(state, built_wheel)
|
||||
run_audit(tmp_dir=tmp_path, build_options=build_options, wheel=repaired_wheel)
|
||||
|
||||
test_wheel(state, repaired_wheel, build_frontend=build_options.build_frontend.name)
|
||||
test_wheel(state, repaired_wheel)
|
||||
|
||||
output_wheel: Path | None = None
|
||||
if compatible_wheel is None:
|
||||
@@ -194,6 +190,11 @@ def setup_target_python(config: PythonConfiguration, build_path: Path) -> Path:
|
||||
# updated to Python versions that include the fix.
|
||||
call("patch", "-p1", "-i", RESOURCES_ANDROID / "android.patch", cwd=python_dir)
|
||||
|
||||
# Work around https://github.com/python/cpython/issues/138800. This can be removed
|
||||
# once we've updated to Python versions that include the fix.
|
||||
pc_path = python_dir / f"prefix/lib/pkgconfig/python-{config.version}.pc"
|
||||
pc_path.write_text(pc_path.read_text().replace("$(BLDLIBRARY)", f"-lpython{config.version}"))
|
||||
|
||||
return python_dir
|
||||
|
||||
|
||||
@@ -207,13 +208,7 @@ 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 in {"build[uv]", "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"]
|
||||
use_uv, pip = find_pip(build_options)
|
||||
|
||||
# Create virtual environment
|
||||
python_exe = create_python_build_standalone_environment(
|
||||
@@ -228,6 +223,9 @@ def setup_env(
|
||||
)
|
||||
create_cmake_toolchain(config, build_path, python_dir, build_env)
|
||||
|
||||
# See platforms.md for the reason why we use this default API level.
|
||||
build_env.setdefault("ANDROID_API_LEVEL", "24")
|
||||
|
||||
# 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"
|
||||
@@ -245,14 +243,14 @@ def setup_env(
|
||||
else:
|
||||
call(command, "--version", env=build_env)
|
||||
|
||||
# Construct an altered environment which simulates running on Android.
|
||||
android_env = setup_android_env(config, python_dir, venv_dir, build_env)
|
||||
|
||||
# Install build tools
|
||||
if build_frontend not in {"build", "build[uv]", "uv"}:
|
||||
msg = "Android requires the build frontend to be 'build' or 'uv'"
|
||||
raise errors.FatalError(msg)
|
||||
call(*pip, "install", "build", *constraint_flags(dependency_constraint), env=build_env)
|
||||
tools = ["auditwheel", "patchelf", "pkgconf"]
|
||||
if build_options.build_frontend.name in {"build", "build[uv]"}:
|
||||
tools.append("build")
|
||||
call(*pip, "install", *tools, *constraint_flags(dependency_constraint), env=build_env)
|
||||
|
||||
# Construct an altered environment which simulates running on Android.
|
||||
android_env = setup_android_env(config, python_dir, 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
|
||||
@@ -343,8 +341,7 @@ def localized_vars(
|
||||
final = final.replace(orig_prefix, str(prefix))
|
||||
|
||||
if key == "ANDROID_API_LEVEL":
|
||||
if api_level := build_env.get(key):
|
||||
final = int(api_level)
|
||||
final = int(build_env[key])
|
||||
|
||||
# Build systems vary in whether FLAGS variables are read from sysconfig, and if so,
|
||||
# whether they're replaced by environment variables or combined with them. Even
|
||||
@@ -367,15 +364,15 @@ def localized_vars(
|
||||
|
||||
|
||||
def setup_android_env(
|
||||
config: PythonConfiguration, python_dir: Path, venv_dir: Path, build_env: dict[str, str]
|
||||
config: PythonConfiguration, python_dir: Path, build_env: dict[str, str]
|
||||
) -> dict[str, str]:
|
||||
site_packages = next(venv_dir.glob("lib/python*/site-packages"))
|
||||
site_packages = find_site_packages(build_env)
|
||||
for suffix in ["pth", "py"]:
|
||||
shutil.copy(RESOURCES_ANDROID / f"_cross_venv.{suffix}", site_packages)
|
||||
|
||||
sysconfigdata_path = Path(
|
||||
shutil.copy(
|
||||
next(python_dir.glob("prefix/lib/python*/_sysconfigdata_*.py")),
|
||||
glob1(python_dir, "prefix/lib/python*/_sysconfigdata_*.py"),
|
||||
site_packages,
|
||||
)
|
||||
)
|
||||
@@ -414,6 +411,17 @@ def setup_android_env(
|
||||
# Cargo target linker needs to be specified after CC is set
|
||||
setup_rust(config, python_dir, android_env)
|
||||
|
||||
# Create shims which install additional build tools on first use.
|
||||
setup_fortran(android_env)
|
||||
|
||||
# `android.py env` returns PKG_CONFIG="pkg-config --define-prefix", but some build
|
||||
# systems can't handle arguments in that variable. Since we have a known version
|
||||
# of pkgconf, it's safe to use PKG_CONFIG_RELOCATE_PATHS instead.
|
||||
android_env["PKG_CONFIG"] = call(
|
||||
"which", "pkgconf-pypi", env=build_env, capture_stdout=True
|
||||
).strip()
|
||||
android_env["PKG_CONFIG_RELOCATE_PATHS"] = "1"
|
||||
|
||||
# Format the environment so it can be pasted into a shell when debugging.
|
||||
for key, value in sorted(android_env.items()):
|
||||
if os.environ.get(key) != value:
|
||||
@@ -422,11 +430,7 @@ def setup_android_env(
|
||||
return android_env
|
||||
|
||||
|
||||
def setup_rust(
|
||||
config: PythonConfiguration,
|
||||
python_dir: Path,
|
||||
env: MutableMapping[str, str],
|
||||
) -> None:
|
||||
def setup_rust(config: PythonConfiguration, python_dir: Path, env: dict[str, str]) -> None:
|
||||
cargo_target = android_triplet(config.identifier)
|
||||
|
||||
# CARGO_BUILD_TARGET is the variable used by Cargo and setuptools_rust
|
||||
@@ -450,13 +454,83 @@ def setup_rust(
|
||||
shim_path.chmod(0o755)
|
||||
|
||||
|
||||
def setup_fortran(env: dict[str, str]) -> None:
|
||||
# In case there's any autodetection based on the executable name, use the same name
|
||||
# as the real executable (see fortran_shim.run_flang)
|
||||
shim_in = RESOURCES_ANDROID / "fortran_shim.py"
|
||||
shim_out = Path(env["VIRTUAL_ENV"]) / "bin/flang-new"
|
||||
|
||||
# The hashbang line runs the shim in cibuildwheel's own virtual environment, so it
|
||||
# has access to utility functions for downloading and caching files.
|
||||
shim_out.write_text(f"#!{sys.executable}\n\n" + shim_in.read_text())
|
||||
shim_out.chmod(0o755)
|
||||
env["FC"] = str(shim_out)
|
||||
|
||||
|
||||
def setup_xbuild_files(state: BuildState) -> None:
|
||||
_, pip = find_pip(state.options)
|
||||
xbf_dir = state.build_path / "xbuild_files"
|
||||
xbf_dir.mkdir()
|
||||
|
||||
for requirement in call(*pip, "freeze", env=state.build_env, capture_stdout=True).splitlines():
|
||||
name, _, _ = requirement.strip().partition("==")
|
||||
xbuild_files = state.options.xbuild_files.get(canonicalize_name(name), [])
|
||||
if xbuild_files:
|
||||
log.step(f"Installing xbuild-files for {name}...")
|
||||
pip_install_android(state, xbf_dir, "--no-deps", requirement)
|
||||
for xbf in xbuild_files:
|
||||
if (xbf_dir / xbf).exists():
|
||||
shutil.copy(
|
||||
xbf_dir / xbf,
|
||||
find_site_packages(state.build_env) / xbf,
|
||||
)
|
||||
else:
|
||||
log.warning(f"{xbf_dir / xbf} does not exist")
|
||||
|
||||
|
||||
def pip_install_android(state: BuildState, target: Path, *args: PathOrStr) -> None:
|
||||
use_uv, pip = find_pip(state.options)
|
||||
call(
|
||||
*pip,
|
||||
"install",
|
||||
"--only-binary=:all:",
|
||||
*(["--python-platform", android_triplet(state.config.identifier)] if use_uv else []),
|
||||
"--target",
|
||||
target,
|
||||
*args,
|
||||
env=state.android_env,
|
||||
)
|
||||
|
||||
|
||||
def find_site_packages(env: dict[str, str]) -> Path:
|
||||
return glob1(Path(env["VIRTUAL_ENV"]), "lib/python*/site-packages")
|
||||
|
||||
|
||||
def glob1(base: Path, pattern: str) -> Path:
|
||||
results = list(base.glob(pattern))
|
||||
if len(results) != 1:
|
||||
msg = f"{base} contains {len(results)} paths matching '{pattern}'; expected 1"
|
||||
raise errors.FatalError(msg)
|
||||
return results[0]
|
||||
|
||||
|
||||
def find_pip(build_options: BuildOptions) -> tuple[bool, list[str]]:
|
||||
use_uv = build_options.build_frontend.name in {"build[uv]", "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"]
|
||||
return use_uv, pip
|
||||
|
||||
|
||||
def before_build(state: BuildState) -> None:
|
||||
if state.options.before_build:
|
||||
log.step("Running before_build...")
|
||||
shell_prepared(
|
||||
state.options.before_build,
|
||||
build_options=state.options,
|
||||
env=state.build_env,
|
||||
env=state.android_env,
|
||||
)
|
||||
|
||||
|
||||
@@ -507,15 +581,10 @@ def build_wheel(state: BuildState) -> Path:
|
||||
env=state.android_env,
|
||||
)
|
||||
case x:
|
||||
msg = f"Invalid build backend {x!r}"
|
||||
raise AssertionError(msg)
|
||||
|
||||
built_wheels = list(built_wheel_dir.glob("*.whl"))
|
||||
if len(built_wheels) != 1:
|
||||
msg = f"{built_wheel_dir} contains {len(built_wheels)} wheels; expected 1"
|
||||
raise errors.FatalError(msg)
|
||||
built_wheel = built_wheels[0]
|
||||
msg = f"Android requires the build frontend to be 'build' or 'uv', not {x!r}"
|
||||
raise errors.FatalError(msg)
|
||||
|
||||
built_wheel = glob1(built_wheel_dir, "*.whl")
|
||||
if built_wheel.name.endswith("none-any.whl"):
|
||||
raise errors.NonPlatformWheelError()
|
||||
return built_wheel
|
||||
@@ -527,9 +596,20 @@ def repair_wheel(state: BuildState, built_wheel: Path) -> Path:
|
||||
repaired_wheel_dir.mkdir()
|
||||
|
||||
if state.options.repair_command:
|
||||
# Tell auditwheel the locations of compiler libraries.
|
||||
toolchain = Path(state.android_env["CC"]).parent.parent
|
||||
triplet = android_triplet(state.config.identifier)
|
||||
ldpaths = ":".join(
|
||||
str(glob1(toolchain, pattern))
|
||||
for pattern in [
|
||||
f"lib/clang/*/lib/linux/{triplet.split('-')[0]}", # libomp
|
||||
f"sysroot/usr/lib/{triplet}", # libc++_shared
|
||||
]
|
||||
)
|
||||
shell(
|
||||
prepare_command(
|
||||
state.options.repair_command,
|
||||
ldpaths=ldpaths,
|
||||
wheel=built_wheel,
|
||||
dest_dir=repaired_wheel_dir,
|
||||
package=state.options.package_dir,
|
||||
@@ -538,14 +618,15 @@ def repair_wheel(state: BuildState, built_wheel: Path) -> Path:
|
||||
env=state.build_env,
|
||||
)
|
||||
else:
|
||||
repair_default(state.android_env, built_wheel, repaired_wheel_dir)
|
||||
shutil.move(built_wheel, repaired_wheel_dir)
|
||||
|
||||
repaired_wheels = list(repaired_wheel_dir.glob("*.whl"))
|
||||
if len(repaired_wheels) == 0:
|
||||
raise errors.RepairStepProducedNoWheelError()
|
||||
if len(repaired_wheels) != 1:
|
||||
msg = f"{repaired_wheel_dir} contains {len(repaired_wheels)} wheels; expected 1"
|
||||
raise errors.FatalError(msg)
|
||||
raise errors.RepairStepProducedMultipleWheelsError(
|
||||
[rw.name for rw in repaired_wheels],
|
||||
)
|
||||
repaired_wheel = repaired_wheels[0]
|
||||
|
||||
if repaired_wheel.name.endswith("none-any.whl"):
|
||||
@@ -553,119 +634,12 @@ def repair_wheel(state: BuildState, built_wheel: Path) -> Path:
|
||||
return repaired_wheel
|
||||
|
||||
|
||||
def repair_default(
|
||||
android_env: dict[str, str], built_wheel: Path, repaired_wheel_dir: Path
|
||||
) -> None:
|
||||
"""
|
||||
Adds libc++ to the wheel if anything links against it. In the future this should be
|
||||
moved to auditwheel and generalized to support more libraries.
|
||||
"""
|
||||
if (match := re.search(r"^(.+?)-", built_wheel.name)) is None:
|
||||
msg = f"Failed to parse wheel filename: {built_wheel.name}"
|
||||
raise errors.FatalError(msg)
|
||||
wheel_name = match[1]
|
||||
|
||||
unpacked_dir = repaired_wheel_dir / "unpacked"
|
||||
unpacked_dir.mkdir()
|
||||
shutil.unpack_archive(built_wheel, unpacked_dir, format="zip")
|
||||
|
||||
# Some build systems are inconsistent about name normalization, so don't assume the
|
||||
# dist-info name is identical to the wheel name.
|
||||
record_paths = list(unpacked_dir.glob("*.dist-info/RECORD"))
|
||||
if len(record_paths) != 1:
|
||||
msg = f"{built_wheel.name} contains {len(record_paths)} dist-info/RECORD files; expected 1"
|
||||
raise errors.FatalError(msg)
|
||||
|
||||
old_soname = "libc++_shared.so"
|
||||
paths_to_patch = []
|
||||
for path, elffile in elf_file_filter(
|
||||
unpacked_dir / filename
|
||||
for filename, *_ in csv.reader(record_paths[0].read_text().splitlines())
|
||||
):
|
||||
if (dynamic := elffile.get_section_by_name(".dynamic")) and any( # type: ignore[no-untyped-call]
|
||||
tag.entry.d_tag == "DT_NEEDED" and tag.needed == old_soname
|
||||
for tag in dynamic.iter_tags()
|
||||
):
|
||||
paths_to_patch.append(path)
|
||||
|
||||
if not paths_to_patch:
|
||||
shutil.copyfile(built_wheel, repaired_wheel_dir / built_wheel.name)
|
||||
else:
|
||||
# Android doesn't support DT_RPATH, but supports DT_RUNPATH since API level 24
|
||||
# (https://github.com/aosp-mirror/platform_bionic/blob/master/android-changes-for-ndk-developers.md).
|
||||
if int(sysconfig_print('get_config_vars()["ANDROID_API_LEVEL"]', android_env)) < 24:
|
||||
msg = f"Adding {old_soname} requires ANDROID_API_LEVEL to be at least 24"
|
||||
raise errors.FatalError(msg)
|
||||
|
||||
toolchain = Path(android_env["CC"]).parent.parent
|
||||
src_path = toolchain / f"sysroot/usr/lib/{android_env['CIBW_HOST_TRIPLET']}/{old_soname}"
|
||||
|
||||
# Use the same library location as auditwheel would.
|
||||
libs_dir = unpacked_dir / (wheel_name + ".libs")
|
||||
libs_dir.mkdir()
|
||||
new_soname = soname_with_hash(src_path)
|
||||
dst_path = libs_dir / new_soname
|
||||
shutil.copyfile(src_path, dst_path)
|
||||
call(which("patchelf"), "--set-soname", new_soname, dst_path)
|
||||
|
||||
for path in paths_to_patch:
|
||||
call(which("patchelf"), "--replace-needed", old_soname, new_soname, path)
|
||||
call(
|
||||
which("patchelf"),
|
||||
"--set-rpath",
|
||||
f"${{ORIGIN}}/{relpath(libs_dir, path.parent)}",
|
||||
path,
|
||||
)
|
||||
call(which("wheel"), "pack", unpacked_dir, "-d", repaired_wheel_dir)
|
||||
|
||||
|
||||
# If cibuildwheel was called without activating its environment, its scripts directory
|
||||
# will not be on the PATH.
|
||||
def which(cmd: str) -> str:
|
||||
scripts_dir = sysconfig.get_path("scripts")
|
||||
result = shutil.which(cmd, path=scripts_dir + os.pathsep + os.environ["PATH"])
|
||||
if result is None:
|
||||
msg = f"Couldn't find {cmd!r} in {scripts_dir} or on the PATH"
|
||||
raise errors.FatalError(msg)
|
||||
return result
|
||||
|
||||
|
||||
def elf_file_filter(paths: Iterable[Path]) -> Iterator[tuple[Path, ELFFile]]:
|
||||
"""Filter through an iterator of filenames and load up only ELF files"""
|
||||
for path in paths:
|
||||
if not path.name.endswith(".py"):
|
||||
try:
|
||||
with open(path, "rb") as f:
|
||||
candidate = ELFFile(f) # type: ignore[no-untyped-call]
|
||||
yield path, candidate
|
||||
except ELFError:
|
||||
pass # Not an ELF file
|
||||
|
||||
|
||||
def soname_with_hash(src_path: Path) -> str:
|
||||
"""Return the same library filename as auditwheel would"""
|
||||
shorthash = hashlib.sha256(src_path.read_bytes()).hexdigest()[:8]
|
||||
src_name = src_path.name
|
||||
base, ext = src_name.split(".", 1)
|
||||
if not base.endswith(f"-{shorthash}"):
|
||||
return f"{base}-{shorthash}.{ext}"
|
||||
else:
|
||||
return src_name
|
||||
|
||||
|
||||
def test_wheel(state: BuildState, wheel: Path, *, build_frontend: str) -> None:
|
||||
def test_wheel(state: BuildState, wheel: Path) -> 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 in {"build[uv]", "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(
|
||||
@@ -678,31 +652,17 @@ def test_wheel(state: BuildState, wheel: Path, *, build_frontend: str) -> None:
|
||||
shell_prepared(
|
||||
state.options.before_test,
|
||||
build_options=state.options,
|
||||
env=state.build_env,
|
||||
env=state.android_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.
|
||||
site_packages_dir = state.build_path / "site-packages"
|
||||
site_packages_dir.mkdir()
|
||||
call(
|
||||
*pip,
|
||||
"install",
|
||||
"--only-binary=:all:",
|
||||
*platform_args,
|
||||
"--target",
|
||||
pip_install_android(
|
||||
state,
|
||||
site_packages_dir,
|
||||
f"{wheel}{state.options.test_extras}",
|
||||
*state.options.test_requires,
|
||||
env=state.android_env,
|
||||
)
|
||||
|
||||
# Copy test-sources.
|
||||
@@ -772,13 +732,3 @@ def test_wheel(state: BuildState, wheel: Path, *, build_frontend: str) -> None:
|
||||
*test_args,
|
||||
env=state.build_env,
|
||||
)
|
||||
|
||||
|
||||
def sysconfig_print(method_call: str, env: dict[str, str]) -> str:
|
||||
return call(
|
||||
"python",
|
||||
"-c",
|
||||
f'import sysconfig; print(sysconfig.{method_call}, end="")',
|
||||
env=env,
|
||||
capture_stdout=True,
|
||||
)
|
||||
|
||||
@@ -14,6 +14,9 @@ def initialize() -> None:
|
||||
if not (host_triplet := os.environ.get("CIBW_HOST_TRIPLET")):
|
||||
return
|
||||
|
||||
# Pre-import any modules which would fail to import after the monkey-patching.
|
||||
import ctypes # noqa: F401, PLC0415 - uses get_config_var("LDLIBRARY")
|
||||
|
||||
# os ######################################################################
|
||||
def cross_os_uname() -> os.uname_result:
|
||||
return os.uname_result(
|
||||
|
||||
@@ -0,0 +1,121 @@
|
||||
# This file intentionally has no hashbang line in the source: cibuildwheel will add it
|
||||
# above this comment when the file is deployed.
|
||||
|
||||
import os
|
||||
import re
|
||||
import shutil
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from tempfile import gettempdir
|
||||
|
||||
from filelock import FileLock
|
||||
|
||||
from cibuildwheel.util.file import CIBW_CACHE_PATH, download
|
||||
|
||||
# In the future we might pick a different Flang release depending on the NDK version,
|
||||
# but so far all Python versions use the same NDK version, so there's no need.
|
||||
RELEASE_URL = "https://github.com/termux/ndk-toolchain-clang-with-flang/releases/download"
|
||||
RELEASE_VERSION = "r27c"
|
||||
ARCHS = ["aarch64", "x86_64"]
|
||||
|
||||
# The compiler is built for Linux x86_64, so we use Docker on macOS.
|
||||
DOCKER_IMAGE = "debian:trixie-slim"
|
||||
|
||||
|
||||
def main() -> None:
|
||||
cache_dir = CIBW_CACHE_PATH / f"flang-android-{RELEASE_VERSION}"
|
||||
with FileLock(f"{cache_dir}.lock"):
|
||||
if not cache_dir.exists():
|
||||
download_flang(cache_dir)
|
||||
|
||||
run_flang(cache_dir)
|
||||
|
||||
|
||||
def download_flang(cache_dir: Path) -> None:
|
||||
tmp_dir = Path(f"{cache_dir}.tmp")
|
||||
if tmp_dir.exists():
|
||||
shutil.rmtree(tmp_dir)
|
||||
tmp_dir.mkdir(parents=True)
|
||||
|
||||
for archive_name in [f"package-flang-{arch}.tar.bz2" for arch in ARCHS] + [
|
||||
"package-flang-host.tar.bz2",
|
||||
"package-install.tar.bz2",
|
||||
]:
|
||||
archive_path = tmp_dir / archive_name
|
||||
download(f"{RELEASE_URL}/{RELEASE_VERSION}/{archive_name}", archive_path)
|
||||
shutil.unpack_archive(archive_path, tmp_dir)
|
||||
archive_path.unlink()
|
||||
|
||||
# Merge the extracted trees together, along with the necessary parts of the NDK. Based on
|
||||
# https://github.com/kivy/python-for-android/blob/develop/pythonforandroid/recipes/fortran/__init__.py.
|
||||
flang_toolchain = tmp_dir / "toolchain"
|
||||
(tmp_dir / "out/install/linux-x86/clang-dev").rename(flang_toolchain)
|
||||
|
||||
ndk_toolchain = Path(os.environ["CC"]).parents[1]
|
||||
if (clang_ver_flang := clang_ver(flang_toolchain)) != (
|
||||
clang_ver_ndk := clang_ver(ndk_toolchain)
|
||||
):
|
||||
msg = f"Flang uses Clang {clang_ver_flang}, but NDK uses Clang {clang_ver_ndk}"
|
||||
raise ValueError(msg)
|
||||
|
||||
clang_lib_path = f"lib/clang/{clang_ver_ndk}/lib"
|
||||
shutil.rmtree(flang_toolchain / clang_lib_path)
|
||||
|
||||
for src, dst in [
|
||||
(f"{tmp_dir}/build-{arch}-install", f"sysroot/usr/lib/{arch}-linux-android")
|
||||
for arch in ARCHS
|
||||
] + [
|
||||
(f"{tmp_dir}/build-host-install", ""),
|
||||
(f"{ndk_toolchain}/{clang_lib_path}", clang_lib_path),
|
||||
(f"{ndk_toolchain}/sysroot", "sysroot"),
|
||||
]:
|
||||
shutil.copytree(src, flang_toolchain / dst, symlinks=True, dirs_exist_ok=True)
|
||||
|
||||
flang_toolchain.rename(cache_dir)
|
||||
shutil.rmtree(tmp_dir)
|
||||
|
||||
|
||||
def clang_ver(toolchain: Path) -> str:
|
||||
versions = [p.name for p in (toolchain / "lib/clang").iterdir()]
|
||||
assert len(versions) == 1
|
||||
return versions[0]
|
||||
|
||||
|
||||
def run_flang(cache_dir: Path) -> None:
|
||||
match = re.fullmatch(r".+/(.+)-clang", os.environ["CC"])
|
||||
assert match is not None
|
||||
target = match[1]
|
||||
|
||||
# In a future Flang version the executable name will change to "flang"
|
||||
# (https://blog.llvm.org/posts/2025-03-11-flang-new/).
|
||||
flang_args = [f"{cache_dir}/bin/flang-new", f"--target={target}", *sys.argv[1:]]
|
||||
|
||||
if sys.platform == "linux":
|
||||
args = flang_args
|
||||
elif sys.platform == "darwin":
|
||||
args = ["docker", "run", "--rm", "--platform", "linux/amd64"]
|
||||
# Docker on macOS only allows certain directories to be mounted as volumes
|
||||
# by default, but they include all the locations we're likely to need.
|
||||
for path in [
|
||||
# Location of CIBW_CACHE_PATH (containing the Fortran compiler), and
|
||||
# probably the project directory.
|
||||
Path.home(),
|
||||
# Temporary directories created by build backends and cibuildwheel itself.
|
||||
# This is controlled by the TMPDIR environment variable, which is set to
|
||||
# an unpredictable subdirectory of /var/folders. /var is itself a symlink
|
||||
# to /private/var.
|
||||
Path(gettempdir()),
|
||||
Path(gettempdir()).resolve(),
|
||||
]:
|
||||
args += ["-v", f"{path}:{path}"]
|
||||
args += ["--workdir", str(Path.cwd())]
|
||||
args += ["--entrypoint", flang_args[0], DOCKER_IMAGE, *flang_args[1:]]
|
||||
else:
|
||||
msg = f"unknown platform: {sys.platform}"
|
||||
raise ValueError(msg)
|
||||
|
||||
os.execvp(args[0], args)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -460,6 +460,34 @@
|
||||
],
|
||||
"title": "CIBW_XBUILD_TOOLS"
|
||||
},
|
||||
"xbuild-files": {
|
||||
"description": "Platform-specific files in the build environment",
|
||||
"oneOf": [
|
||||
{
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"patternProperties": {
|
||||
".+": {
|
||||
"oneOf": [
|
||||
{
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"title": "CIBW_XBUILD_FILES"
|
||||
},
|
||||
"pyodide-version": {
|
||||
"type": "string",
|
||||
"description": "Specify the version of Pyodide to use",
|
||||
@@ -679,6 +707,9 @@
|
||||
"xbuild-tools": {
|
||||
"$ref": "#/$defs/inherit"
|
||||
},
|
||||
"xbuild-files": {
|
||||
"$ref": "#/$defs/inherit"
|
||||
},
|
||||
"before-test": {
|
||||
"$ref": "#/$defs/inherit"
|
||||
},
|
||||
@@ -807,6 +838,9 @@
|
||||
"xbuild-tools": {
|
||||
"$ref": "#/properties/xbuild-tools"
|
||||
},
|
||||
"xbuild-files": {
|
||||
"$ref": "#/properties/xbuild-files"
|
||||
},
|
||||
"pyodide-version": {
|
||||
"$ref": "#/properties/pyodide-version"
|
||||
},
|
||||
@@ -931,6 +965,9 @@
|
||||
"xbuild-tools": {
|
||||
"$ref": "#/properties/xbuild-tools"
|
||||
},
|
||||
"xbuild-files": {
|
||||
"$ref": "#/properties/xbuild-files"
|
||||
},
|
||||
"pyodide-version": {
|
||||
"$ref": "#/properties/pyodide-version"
|
||||
},
|
||||
@@ -1013,6 +1050,9 @@
|
||||
"xbuild-tools": {
|
||||
"$ref": "#/properties/xbuild-tools"
|
||||
},
|
||||
"xbuild-files": {
|
||||
"$ref": "#/properties/xbuild-files"
|
||||
},
|
||||
"pyodide-version": {
|
||||
"$ref": "#/properties/pyodide-version"
|
||||
},
|
||||
@@ -1082,6 +1122,9 @@
|
||||
"xbuild-tools": {
|
||||
"$ref": "#/properties/xbuild-tools"
|
||||
},
|
||||
"xbuild-files": {
|
||||
"$ref": "#/properties/xbuild-files"
|
||||
},
|
||||
"pyodide-version": {
|
||||
"$ref": "#/properties/pyodide-version"
|
||||
},
|
||||
@@ -1164,6 +1207,9 @@
|
||||
"xbuild-tools": {
|
||||
"$ref": "#/properties/xbuild-tools"
|
||||
},
|
||||
"xbuild-files": {
|
||||
"$ref": "#/properties/xbuild-files"
|
||||
},
|
||||
"pyodide-version": {
|
||||
"$ref": "#/properties/pyodide-version"
|
||||
},
|
||||
@@ -1233,11 +1279,27 @@
|
||||
"xbuild-tools": {
|
||||
"$ref": "#/properties/xbuild-tools"
|
||||
},
|
||||
"xbuild-files": {
|
||||
"$ref": "#/properties/xbuild-files"
|
||||
},
|
||||
"pyodide-version": {
|
||||
"$ref": "#/properties/pyodide-version"
|
||||
},
|
||||
"repair-wheel-command": {
|
||||
"$ref": "#/properties/repair-wheel-command"
|
||||
"description": "Execute a shell command to repair each built wheel.",
|
||||
"oneOf": [
|
||||
{
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
],
|
||||
"title": "CIBW_REPAIR_WHEEL_COMMAND",
|
||||
"default": "auditwheel repair --ldpaths {ldpaths} -w {dest_dir} {wheel}"
|
||||
},
|
||||
"test-command": {
|
||||
"$ref": "#/properties/test-command"
|
||||
@@ -1302,6 +1364,9 @@
|
||||
"xbuild-tools": {
|
||||
"$ref": "#/properties/xbuild-tools"
|
||||
},
|
||||
"xbuild-files": {
|
||||
"$ref": "#/properties/xbuild-files"
|
||||
},
|
||||
"pyodide-version": {
|
||||
"$ref": "#/properties/pyodide-version"
|
||||
},
|
||||
|
||||
@@ -10,6 +10,8 @@ attrs==26.1.0
|
||||
# via
|
||||
# cattrs
|
||||
# requests-cache
|
||||
auditwheel==6.7.0
|
||||
# via -r cibuildwheel/resources/constraints.in
|
||||
build==1.5.0
|
||||
# via -r cibuildwheel/resources/constraints.in
|
||||
cattrs==26.1.0
|
||||
@@ -47,21 +49,28 @@ mdurl==0.1.2
|
||||
packaging==26.2
|
||||
# via
|
||||
# abi3audit
|
||||
# auditwheel
|
||||
# build
|
||||
# delocate
|
||||
patchelf==0.17.2.4
|
||||
# via -r cibuildwheel/resources/constraints.in
|
||||
pefile==2024.8.26
|
||||
# via
|
||||
# abi3audit
|
||||
# delvewheel
|
||||
pip==26.1.1
|
||||
# via -r cibuildwheel/resources/constraints.in
|
||||
pkgconf==2.5.1.post2
|
||||
# via -r cibuildwheel/resources/constraints.in
|
||||
platformdirs==4.9.6
|
||||
# via
|
||||
# python-discovery
|
||||
# requests-cache
|
||||
# virtualenv
|
||||
pyelftools==0.32
|
||||
# via abi3audit
|
||||
# via
|
||||
# abi3audit
|
||||
# auditwheel
|
||||
pygments==2.20.0
|
||||
# via rich
|
||||
pyproject-hooks==1.2.0
|
||||
|
||||
@@ -10,6 +10,8 @@ attrs==26.1.0
|
||||
# via
|
||||
# cattrs
|
||||
# requests-cache
|
||||
auditwheel==6.7.0
|
||||
# via -r cibuildwheel/resources/constraints.in
|
||||
build==1.5.0
|
||||
# via -r cibuildwheel/resources/constraints.in
|
||||
cattrs==26.1.0
|
||||
@@ -43,8 +45,11 @@ mdurl==0.1.2
|
||||
packaging==26.2
|
||||
# via
|
||||
# abi3audit
|
||||
# auditwheel
|
||||
# build
|
||||
# delocate
|
||||
patchelf==0.17.2.4
|
||||
# via -r cibuildwheel/resources/constraints.in
|
||||
pefile==2024.8.26
|
||||
# via
|
||||
# abi3audit
|
||||
@@ -52,13 +57,17 @@ pefile==2024.8.26
|
||||
pip==26.1.1; implementation_name != "graalpy" or platform_system != "Windows"
|
||||
pip==26.0.1; implementation_name == "graalpy" and platform_system == "Windows"
|
||||
# via -r cibuildwheel/resources/constraints.in
|
||||
pkgconf==2.5.1.post2
|
||||
# via -r cibuildwheel/resources/constraints.in
|
||||
platformdirs==4.9.6
|
||||
# via
|
||||
# python-discovery
|
||||
# requests-cache
|
||||
# virtualenv
|
||||
pyelftools==0.32
|
||||
# via abi3audit
|
||||
# via
|
||||
# abi3audit
|
||||
# auditwheel
|
||||
pygments==2.20.0
|
||||
# via rich
|
||||
pyproject-hooks==1.2.0
|
||||
|
||||
@@ -10,6 +10,8 @@ attrs==26.1.0
|
||||
# via
|
||||
# cattrs
|
||||
# requests-cache
|
||||
auditwheel==6.7.0
|
||||
# via -r cibuildwheel/resources/constraints.in
|
||||
build==1.5.0
|
||||
# via -r cibuildwheel/resources/constraints.in
|
||||
cattrs==26.1.0
|
||||
@@ -43,8 +45,11 @@ mdurl==0.1.2
|
||||
packaging==26.2
|
||||
# via
|
||||
# abi3audit
|
||||
# auditwheel
|
||||
# build
|
||||
# delocate
|
||||
patchelf==0.17.2.4
|
||||
# via -r cibuildwheel/resources/constraints.in
|
||||
pefile==2024.8.26
|
||||
# via
|
||||
# abi3audit
|
||||
@@ -52,13 +57,17 @@ pefile==2024.8.26
|
||||
pip==26.1.1; implementation_name != "graalpy" or platform_system != "Windows"
|
||||
pip==26.0.1; implementation_name == "graalpy" and platform_system == "Windows"
|
||||
# via -r cibuildwheel/resources/constraints.in
|
||||
pkgconf==2.5.1.post2
|
||||
# via -r cibuildwheel/resources/constraints.in
|
||||
platformdirs==4.9.6
|
||||
# via
|
||||
# python-discovery
|
||||
# requests-cache
|
||||
# virtualenv
|
||||
pyelftools==0.32
|
||||
# via abi3audit
|
||||
# via
|
||||
# abi3audit
|
||||
# auditwheel
|
||||
pygments==2.20.0
|
||||
# via rich
|
||||
pyproject-hooks==1.2.0
|
||||
|
||||
@@ -10,6 +10,8 @@ attrs==26.1.0
|
||||
# via
|
||||
# cattrs
|
||||
# requests-cache
|
||||
auditwheel==6.7.0
|
||||
# via -r cibuildwheel/resources/constraints.in
|
||||
build==1.5.0
|
||||
# via -r cibuildwheel/resources/constraints.in
|
||||
cattrs==26.1.0
|
||||
@@ -43,21 +45,28 @@ mdurl==0.1.2
|
||||
packaging==26.2
|
||||
# via
|
||||
# abi3audit
|
||||
# auditwheel
|
||||
# build
|
||||
# delocate
|
||||
patchelf==0.17.2.4
|
||||
# via -r cibuildwheel/resources/constraints.in
|
||||
pefile==2024.8.26
|
||||
# via
|
||||
# abi3audit
|
||||
# delvewheel
|
||||
pip==26.1.1
|
||||
# via -r cibuildwheel/resources/constraints.in
|
||||
pkgconf==2.5.1.post2
|
||||
# via -r cibuildwheel/resources/constraints.in
|
||||
platformdirs==4.9.6
|
||||
# via
|
||||
# python-discovery
|
||||
# requests-cache
|
||||
# virtualenv
|
||||
pyelftools==0.32
|
||||
# via abi3audit
|
||||
# via
|
||||
# abi3audit
|
||||
# auditwheel
|
||||
pygments==2.20.0
|
||||
# via rich
|
||||
pyproject-hooks==1.2.0
|
||||
|
||||
@@ -10,6 +10,8 @@ attrs==26.1.0
|
||||
# via
|
||||
# cattrs
|
||||
# requests-cache
|
||||
auditwheel==6.7.0
|
||||
# via -r cibuildwheel/resources/constraints.in
|
||||
build==1.5.0
|
||||
# via -r cibuildwheel/resources/constraints.in
|
||||
cattrs==26.1.0
|
||||
@@ -43,21 +45,28 @@ mdurl==0.1.2
|
||||
packaging==26.2
|
||||
# via
|
||||
# abi3audit
|
||||
# auditwheel
|
||||
# build
|
||||
# delocate
|
||||
patchelf==0.17.2.4
|
||||
# via -r cibuildwheel/resources/constraints.in
|
||||
pefile==2024.8.26
|
||||
# via
|
||||
# abi3audit
|
||||
# delvewheel
|
||||
pip==26.1.1
|
||||
# via -r cibuildwheel/resources/constraints.in
|
||||
pkgconf==2.5.1.post2
|
||||
# via -r cibuildwheel/resources/constraints.in
|
||||
platformdirs==4.9.6
|
||||
# via
|
||||
# python-discovery
|
||||
# requests-cache
|
||||
# virtualenv
|
||||
pyelftools==0.32
|
||||
# via abi3audit
|
||||
# via
|
||||
# abi3audit
|
||||
# auditwheel
|
||||
pygments==2.20.0
|
||||
# via rich
|
||||
pyproject-hooks==1.2.0
|
||||
|
||||
@@ -10,6 +10,8 @@ attrs==26.1.0
|
||||
# via
|
||||
# cattrs
|
||||
# requests-cache
|
||||
auditwheel==6.7.0
|
||||
# via -r cibuildwheel/resources/constraints.in
|
||||
build==1.5.0
|
||||
# via -r cibuildwheel/resources/constraints.in
|
||||
cattrs==26.1.0
|
||||
@@ -43,21 +45,28 @@ mdurl==0.1.2
|
||||
packaging==26.2
|
||||
# via
|
||||
# abi3audit
|
||||
# auditwheel
|
||||
# build
|
||||
# delocate
|
||||
patchelf==0.17.2.4
|
||||
# via -r cibuildwheel/resources/constraints.in
|
||||
pefile==2024.8.26
|
||||
# via
|
||||
# abi3audit
|
||||
# delvewheel
|
||||
pip==26.1.1
|
||||
# via -r cibuildwheel/resources/constraints.in
|
||||
pkgconf==2.5.1.post2
|
||||
# via -r cibuildwheel/resources/constraints.in
|
||||
platformdirs==4.9.6
|
||||
# via
|
||||
# python-discovery
|
||||
# requests-cache
|
||||
# virtualenv
|
||||
pyelftools==0.32
|
||||
# via abi3audit
|
||||
# via
|
||||
# abi3audit
|
||||
# auditwheel
|
||||
pygments==2.20.0
|
||||
# via rich
|
||||
pyproject-hooks==1.2.0
|
||||
|
||||
@@ -10,6 +10,8 @@ attrs==26.1.0
|
||||
# via
|
||||
# cattrs
|
||||
# requests-cache
|
||||
auditwheel==6.4.2
|
||||
# via -r cibuildwheel/resources/constraints.in
|
||||
build==1.4.4
|
||||
# via -r cibuildwheel/resources/constraints.in
|
||||
cattrs==25.3.0
|
||||
@@ -35,7 +37,9 @@ idna==3.16
|
||||
# requests
|
||||
# url-normalize
|
||||
importlib-metadata==8.7.1
|
||||
# via build
|
||||
# via
|
||||
# build
|
||||
# pkgconf
|
||||
kaitaistruct==0.11
|
||||
# via abi3audit
|
||||
macholib==1.16.4
|
||||
@@ -47,21 +51,28 @@ mdurl==0.1.2
|
||||
packaging==25.0
|
||||
# via
|
||||
# abi3audit
|
||||
# auditwheel
|
||||
# build
|
||||
# delocate
|
||||
patchelf==0.17.2.4
|
||||
# via -r cibuildwheel/resources/constraints.in
|
||||
pefile==2024.8.26
|
||||
# via
|
||||
# abi3audit
|
||||
# delvewheel
|
||||
pip==26.0.1
|
||||
# via -r cibuildwheel/resources/constraints.in
|
||||
pkgconf==2.4.3.post2
|
||||
# via -r cibuildwheel/resources/constraints.in
|
||||
platformdirs==4.4.0
|
||||
# via
|
||||
# python-discovery
|
||||
# requests-cache
|
||||
# virtualenv
|
||||
pyelftools==0.32
|
||||
# via abi3audit
|
||||
# via
|
||||
# abi3audit
|
||||
# auditwheel
|
||||
pygments==2.20.0
|
||||
# via rich
|
||||
pyproject-hooks==1.2.0
|
||||
|
||||
@@ -4,3 +4,8 @@ delocate
|
||||
delvewheel
|
||||
virtualenv
|
||||
abi3audit
|
||||
|
||||
# Android
|
||||
auditwheel
|
||||
patchelf
|
||||
pkgconf
|
||||
|
||||
@@ -10,6 +10,8 @@ attrs==26.1.0
|
||||
# via
|
||||
# cattrs
|
||||
# requests-cache
|
||||
auditwheel==6.7.0
|
||||
# via -r cibuildwheel/resources/constraints.in
|
||||
build==1.5.0
|
||||
# via -r cibuildwheel/resources/constraints.in
|
||||
cattrs==26.1.0
|
||||
@@ -43,21 +45,28 @@ mdurl==0.1.2
|
||||
packaging==26.2
|
||||
# via
|
||||
# abi3audit
|
||||
# auditwheel
|
||||
# build
|
||||
# delocate
|
||||
patchelf==0.17.2.4
|
||||
# via -r cibuildwheel/resources/constraints.in
|
||||
pefile==2024.8.26
|
||||
# via
|
||||
# abi3audit
|
||||
# delvewheel
|
||||
pip==26.1.1
|
||||
# via -r cibuildwheel/resources/constraints.in
|
||||
pkgconf==2.5.1.post2
|
||||
# via -r cibuildwheel/resources/constraints.in
|
||||
platformdirs==4.9.6
|
||||
# via
|
||||
# python-discovery
|
||||
# requests-cache
|
||||
# virtualenv
|
||||
pyelftools==0.32
|
||||
# via abi3audit
|
||||
# via
|
||||
# abi3audit
|
||||
# auditwheel
|
||||
pygments==2.20.0
|
||||
# via rich
|
||||
pyproject-hooks==1.2.0
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
# These are the defaults used by cibuildwheel itself. They should match the values in
|
||||
# generate_schema.py, which are used by external tools for validation and IDE support.
|
||||
|
||||
[tool.cibuildwheel]
|
||||
build = "*"
|
||||
skip = ""
|
||||
@@ -52,6 +55,13 @@ musllinux-s390x-image = "musllinux_1_2"
|
||||
musllinux-armv7l-image = "musllinux_1_2"
|
||||
musllinux-riscv64-image = "musllinux_1_2"
|
||||
|
||||
[tool.cibuildwheel.xbuild-files]
|
||||
numpy = [
|
||||
"numpy/_core/include/numpy/_numpyconfig.h",
|
||||
"numpy/_core/include/numpy/numpyconfig.h",
|
||||
"numpy/_core/lib/libnpymath.a",
|
||||
"numpy/random/lib/libnpyrandom.a",
|
||||
]
|
||||
|
||||
[tool.cibuildwheel.linux]
|
||||
repair-wheel-command = "auditwheel repair -w {dest_dir} {wheel}"
|
||||
@@ -63,6 +73,7 @@ repair-wheel-command = "delocate-wheel --require-archs {delocate_archs} -w {dest
|
||||
repair-wheel-command = "delvewheel repair -w {dest_dir} -v {wheel}"
|
||||
|
||||
[tool.cibuildwheel.android]
|
||||
repair-wheel-command = "auditwheel repair --ldpaths {ldpaths} -w {dest_dir} {wheel}"
|
||||
|
||||
[tool.cibuildwheel.ios]
|
||||
|
||||
|
||||
@@ -109,7 +109,7 @@ def parse_key_value_string(
|
||||
if kw_arg_names is None:
|
||||
kw_arg_names = []
|
||||
|
||||
all_field_names = [*positional_arg_names, *kw_arg_names]
|
||||
all_field_names = None if ("*" in kw_arg_names) else [*positional_arg_names, *kw_arg_names]
|
||||
|
||||
shlexer = shlex.shlex(key_value_string, posix=True, punctuation_chars=";")
|
||||
shlexer.commenters = ""
|
||||
@@ -126,7 +126,7 @@ def parse_key_value_string(
|
||||
# check to see if the option name is specified
|
||||
field_name, sep, first_value = field[0].partition(":")
|
||||
if sep:
|
||||
if field_name not in all_field_names:
|
||||
if (all_field_names is not None) and (field_name not in all_field_names):
|
||||
msg = f"Failed to parse {key_value_string!r}. Unknown field name {field_name!r}"
|
||||
raise ValueError(msg)
|
||||
|
||||
|
||||
@@ -58,8 +58,8 @@ cibuildwheel to run tests, add the following YAML to your CI config file:
|
||||
You can configure cibuildwheel with a config file, such as `pyproject.toml`.
|
||||
Options have the same names as the environment variable overrides, but are
|
||||
placed in `[tool.cibuildwheel]` and are lower case, with dashes, following
|
||||
common [TOML](https://toml.io) practice. Anything placed in subsections `linux`, `windows`,
|
||||
`macos`, or `pyodide` will only affect those platforms. Lists can be used
|
||||
common [TOML](https://toml.io) practice. Anything placed in subsections
|
||||
named after a platform will only affect those platforms. Lists can be used
|
||||
instead of strings for items that are naturally a list. Multiline strings also
|
||||
work just like in the environment variables. Environment variables will take
|
||||
precedence if defined.
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 96 KiB After Width: | Height: | Size: 96 KiB |
+2
-2
@@ -179,14 +179,14 @@
|
||||
},
|
||||
},
|
||||
{
|
||||
label: 'Repair wheel',
|
||||
label: 'Repair using auditwheel',
|
||||
href: 'options/#repair-wheel-command',
|
||||
platforms: ['android'],
|
||||
style: 'block',
|
||||
tooltip: {
|
||||
title: 'CIBW_REPAIR_WHEEL_COMMAND',
|
||||
tag: 'Customisable step',
|
||||
description: 'By default, bundles libc++ into the wheel if any shared library links against it, using patchelf. Can be overridden with a custom repair command.'
|
||||
description: 'Bundle shared libraries by running auditwheel on each built wheel.'
|
||||
},
|
||||
},
|
||||
{
|
||||
|
||||
+31
-2
@@ -916,6 +916,35 @@ Platform-specific environment variables are also available on platforms that use
|
||||
```
|
||||
|
||||
|
||||
### `xbuild-files` {: #xbuild-files env-var toml}
|
||||
> Platform-specific files in the build environment
|
||||
|
||||
When cross-compiling a package for Android, any dependencies in its [`build-system.requires`](https://packaging.python.org/en/latest/guides/writing-pyproject-toml/) are installed for the build platform. However, some dependencies contain platform-specific files such as headers and static libraries, which must correspond to the target platform.
|
||||
|
||||
This option maps a [normalized](https://packaging.python.org/en/latest/specifications/name-normalization/#name-normalization) package name to a list of paths within that package. If the package is present in the build environment, then a matching version will be downloaded for the target platform, and used to overwrite the given paths within the build environment.
|
||||
|
||||
The default value of this option includes [paths from popular packages](configuration.md#configuration-file).
|
||||
|
||||
Platform-specific environment variables are also available:<br/>
|
||||
`CIBW_XBUILD_FILES_ANDROID`
|
||||
|
||||
#### Examples
|
||||
|
||||
!!! tab examples "pyproject.toml"
|
||||
|
||||
```toml
|
||||
[tool.cibuildwheel.xbuild-files]
|
||||
package1 = ["some/header.h", "some/library.a"]
|
||||
package2 = ["other/header.h"]
|
||||
```
|
||||
|
||||
!!! tab examples "Environment variables"
|
||||
|
||||
```yaml
|
||||
CIBW_XBUILD_FILES: "package1: some/header.h some/library.a; package2: other/header.h"
|
||||
```
|
||||
|
||||
|
||||
### `repair-wheel-command` {: #repair-wheel-command env-var toml}
|
||||
> Execute a shell command to repair each built wheel
|
||||
|
||||
@@ -923,8 +952,7 @@ Default:
|
||||
|
||||
- on Linux: `'auditwheel repair -w {dest_dir} {wheel}'`
|
||||
- on macOS: `'delocate-wheel --require-archs {delocate_archs} -w {dest_dir} -v {wheel}'`
|
||||
- on Android: There is no default command, but cibuildwheel will add `libc++` to the
|
||||
wheel if anything links against it. Setting a command will replace this behavior.
|
||||
- on Android: `'auditwheel repair --ldpaths {ldpaths} -w {dest_dir} {wheel}'`
|
||||
- on Pyodide: You can use `pyodide auditwheel repair --libdir /path/to/libraries --output-dir {dest_dir} {wheel}` command to repair the wheel.
|
||||
Unlike other platforms, this command is not set by default as you need to explicitly
|
||||
specify the library directory. You might not want to use the libraries in the system
|
||||
@@ -939,6 +967,7 @@ The following placeholders must be used inside the command and will be replaced
|
||||
- `{wheel}` for the absolute path to the built wheel
|
||||
- `{dest_dir}` for the absolute path of the directory where to create the repaired wheel
|
||||
- `{delocate_archs}` (macOS only) comma-separated list of architectures in the wheel.
|
||||
- `{ldpaths}` (Android only) colon-separated list of directories to search for external libraries, set by cibuildwheel to include any necessary locations in the NDK. You can add more directories by appending them with a colon separator after the placeholder, or by setting the `AUDITWHEEL_LD_LIBRARY_PATH` environment variable.
|
||||
|
||||
You can use the `{package}` or `{project}` placeholders in your `repair-wheel-command` to refer to the package being built or the project root, respectively.
|
||||
|
||||
|
||||
+3
-7
@@ -219,13 +219,9 @@ It also requires the following commands to be on the `PATH`:
|
||||
|
||||
Android builds will honor the `ANDROID_API_LEVEL` environment variable to set the
|
||||
minimum supported [API level](https://developer.android.com/tools/releases/platforms)
|
||||
for generated wheels. This will default to the minimum API level of the selected Python
|
||||
version.
|
||||
|
||||
If the [`repair-wheel-command`](options.md#repair-wheel-command) adds any libraries to
|
||||
the wheel, then `ANDROID_API_LEVEL` must be at least 24. This is already the default
|
||||
when building for Python 3.14 and later, but you may need to set it when building for
|
||||
Python 3.13.
|
||||
for generated wheels. This defaults to 24, which is supported by [99% of active
|
||||
devices](https://dl.google.com/android/studio/metadata/distributions.json), and is the first
|
||||
version to support RUNPATH, which auditwheel needs in order to graft external libraries.
|
||||
|
||||
### Build frontend support
|
||||
|
||||
|
||||
@@ -45,11 +45,7 @@ dependencies = [
|
||||
"filelock",
|
||||
"humanize",
|
||||
"packaging>=20.9",
|
||||
# patchelf is used for Android
|
||||
"patchelf; (sys_platform == 'linux' or sys_platform == 'darwin') and (platform_machine == 'x86_64' or platform_machine == 'arm64' or platform_machine == 'aarch64')",
|
||||
"platformdirs",
|
||||
"pyelftools>=0.29",
|
||||
"wheel>=0.33.6",
|
||||
]
|
||||
|
||||
[project.optional-dependencies]
|
||||
|
||||
+197
-17
@@ -13,7 +13,7 @@ from zipfile import ZipFile
|
||||
|
||||
import pytest
|
||||
|
||||
from .test_projects import new_c_project
|
||||
from .test_projects import new_c_project, new_meson_project
|
||||
from .utils import cibuildwheel_run, expected_wheels
|
||||
|
||||
pytestmark = pytest.mark.android
|
||||
@@ -125,7 +125,7 @@ def test_frontend_good(tmp_path: Path, build_frontend_env: dict[str, str]) -> No
|
||||
tmp_path,
|
||||
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_24_{native_arch.android_abi}.whl"]
|
||||
|
||||
|
||||
@pytest.mark.parametrize("frontend", ["pip"])
|
||||
@@ -136,7 +136,10 @@ def test_frontend_bad(frontend: str, tmp_path: Path, capfd: pytest.CaptureFixtur
|
||||
tmp_path,
|
||||
add_env={**cp313_env, "CIBW_BUILD_FRONTEND": frontend},
|
||||
)
|
||||
assert "Android requires the build frontend to be 'build'" in capfd.readouterr().err
|
||||
assert (
|
||||
f"Android requires the build frontend to be 'build' or 'uv', not '{frontend}'"
|
||||
in capfd.readouterr().err
|
||||
)
|
||||
|
||||
|
||||
@needs_emulator
|
||||
@@ -162,7 +165,7 @@ def test_archs(tmp_path: Path, capfd: pytest.CaptureFixture[str]) -> None:
|
||||
),
|
||||
},
|
||||
)
|
||||
assert wheels == [f"spam-0.1.0-cp313-cp313-android_21_{arch.android_abi}.whl" for arch in archs]
|
||||
assert wheels == [f"spam-0.1.0-cp313-cp313-android_24_{arch.android_abi}.whl" for arch in archs]
|
||||
|
||||
stdout, stderr = capfd.readouterr()
|
||||
lines = (line for line in stdout.splitlines() if line.startswith("Hello from"))
|
||||
@@ -435,13 +438,25 @@ def test_verbosity(tmp_path: Path, capfd: pytest.CaptureFixture[str]) -> None:
|
||||
|
||||
@needs_emulator
|
||||
def test_api_level(tmp_path: Path, capfd: pytest.CaptureFixture[str]) -> None:
|
||||
project = new_c_project()
|
||||
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
|
||||
"""
|
||||
)
|
||||
)
|
||||
project.files["pyproject.toml"] = dedent(
|
||||
"""\
|
||||
[build-system]
|
||||
requires = ["setuptools"]
|
||||
|
||||
[tool.cibuildwheel]
|
||||
# Test setting API level in pyproject.toml (test_libcxx covers setting
|
||||
# it in the outer environment.)
|
||||
android.environment.ANDROID_API_LEVEL = "33"
|
||||
android.environment.PIP_EXTRA_INDEX_URL = "https://chaquo.com/pypi-13.1"
|
||||
"""
|
||||
@@ -486,17 +501,21 @@ def test_libcxx(tmp_path: Path, capfd: pytest.CaptureFixture[str]) -> None:
|
||||
"PATH": non_venv_path,
|
||||
}
|
||||
|
||||
# Including external libraries requires API level 24.
|
||||
# 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",
|
||||
}
|
||||
with pytest.raises(CalledProcessError):
|
||||
cibuildwheel_run(project_dir, add_env=cp313_test_env, output_dir=output_dir)
|
||||
assert "libc++_shared.so requires ANDROID_API_LEVEL to be at least 24" in capfd.readouterr().err
|
||||
|
||||
wheels = cibuildwheel_run(
|
||||
project_dir,
|
||||
add_env={**cp313_test_env, "ANDROID_API_LEVEL": "24"},
|
||||
output_dir=output_dir,
|
||||
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
|
||||
)
|
||||
assert len(wheels) == 1
|
||||
|
||||
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"]
|
||||
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)
|
||||
@@ -504,16 +523,177 @@ def test_libcxx(tmp_path: Path, capfd: pytest.CaptureFixture[str]) -> None:
|
||||
assert len(libcxx_names) == 1
|
||||
assert "ham: 1, spam: 0" in capfd.readouterr().out
|
||||
|
||||
# A C package should not include libc++.
|
||||
# A C package should not include libc++, and can therefore use an older API level.
|
||||
rmtree(project_dir)
|
||||
rmtree(output_dir)
|
||||
new_c_project().generate(project_dir)
|
||||
wheels = cibuildwheel_run(project_dir, add_env=cp313_env, output_dir=output_dir)
|
||||
assert len(wheels) == 1
|
||||
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"]
|
||||
for name in ZipFile(output_dir / wheels[0]).namelist():
|
||||
assert ".libs" not in name
|
||||
|
||||
|
||||
@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
|
||||
|
||||
|
||||
# This also tests integration with pkgconf, because Meson uses it to find Python.
|
||||
@needs_emulator
|
||||
def test_meson(tmp_path: Path, capfd: pytest.CaptureFixture[str]) -> None:
|
||||
# Alter spam.filter to return the value of a Fortran function.
|
||||
new_meson_project(
|
||||
spam_c_top_level_add="int fortran_func_();",
|
||||
spam_c_function_add="sts = fortran_func_();",
|
||||
project_args_add="'fortran',",
|
||||
extension_args_add="'fortran.f90', link_language: 'fortran',",
|
||||
).generate(tmp_path)
|
||||
|
||||
# TODO: remove once meson-python has a release with Android support.
|
||||
pyproject_path = tmp_path / "pyproject.toml"
|
||||
pyproject_path.write_text(
|
||||
pyproject_path.read_text().replace(
|
||||
"meson-python", "meson-python @ git+https://github.com/mesonbuild/meson-python@main"
|
||||
)
|
||||
)
|
||||
|
||||
# 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,
|
||||
add_env={**cp313_env, "CIBW_TEST_COMMAND": f"python -c '{script}'"},
|
||||
)
|
||||
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.
|
||||
"PIP_EXTRA_INDEX_URL": "https://chaquo.com/pypi-test",
|
||||
"CIBW_ARCHS": "all", # Include both native and non-native archs.
|
||||
"CIBW_TEST_COMMAND": f"python -c '{script}'",
|
||||
},
|
||||
)
|
||||
assert "result: 0x5140" in capfd.readouterr().out
|
||||
|
||||
|
||||
@needs_emulator
|
||||
def test_setuptools_rust(tmp_path: Path, capfd: pytest.CaptureFixture[str]) -> None:
|
||||
"""
|
||||
|
||||
@@ -22,6 +22,7 @@ archs = ["auto64"]
|
||||
|
||||
MESON_BUILD_TEMPLATE = r"""
|
||||
project('spam', 'c',
|
||||
{{ project_args_add | indent(2) }}
|
||||
version: '0.1.0',
|
||||
default_options: ['warning_level=2'],
|
||||
)
|
||||
@@ -30,6 +31,7 @@ py = import('python').find_installation(pure: false)
|
||||
|
||||
py.extension_module('spam',
|
||||
'spam.c',
|
||||
{{ extension_args_add | indent(2) }}
|
||||
install: true,
|
||||
)
|
||||
"""
|
||||
@@ -39,6 +41,8 @@ def new_meson_project(
|
||||
*,
|
||||
spam_c_top_level_add: str = "",
|
||||
spam_c_function_add: str = "",
|
||||
project_args_add: str = "",
|
||||
extension_args_add: str = "",
|
||||
) -> TestProject:
|
||||
project = TestProject()
|
||||
|
||||
@@ -54,6 +58,8 @@ def new_meson_project(
|
||||
{
|
||||
"spam_c_top_level_add": spam_c_top_level_add,
|
||||
"spam_c_function_add": spam_c_function_add,
|
||||
"project_args_add": project_args_add,
|
||||
"extension_args_add": extension_args_add,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@@ -82,9 +82,10 @@ import sys
|
||||
|
||||
from setuptools import setup, Extension
|
||||
|
||||
libraries = []
|
||||
|
||||
{{ setup_py_add }}
|
||||
|
||||
libraries = []
|
||||
# Emscripten fails if you pass -lc...
|
||||
# See: https://github.com/emscripten-core/emscripten/issues/16680
|
||||
if sys.platform.startswith('linux') and "emscripten" not in os.environ.get("_PYTHON_HOST_PLATFORM", ""):
|
||||
|
||||
+7
-6
@@ -177,6 +177,7 @@ def expected_wheels(
|
||||
musllinux_versions: list[str] | None = None,
|
||||
macosx_deployment_target: str | None = None,
|
||||
iphoneos_deployment_target: str | None = None,
|
||||
android_api_level: int | None = None,
|
||||
machine_arch: str | None = None,
|
||||
platform: str | None = None,
|
||||
python_abi_tags: list[str] | None = None,
|
||||
@@ -201,6 +202,9 @@ def expected_wheels(
|
||||
if iphoneos_deployment_target is None:
|
||||
iphoneos_deployment_target = os.environ.get("IPHONEOS_DEPLOYMENT_TARGET", "13.0")
|
||||
|
||||
if android_api_level is None:
|
||||
android_api_level = int(os.environ.get("ANDROID_API_LEVEL", "24"))
|
||||
|
||||
architectures = [machine_arch]
|
||||
if not single_arch:
|
||||
if platform == "linux" and full_auto:
|
||||
@@ -226,6 +230,7 @@ def expected_wheels(
|
||||
musllinux_versions=musllinux_versions,
|
||||
macosx_deployment_target=macosx_deployment_target,
|
||||
iphoneos_deployment_target=iphoneos_deployment_target,
|
||||
android_api_level=android_api_level,
|
||||
platform=platform,
|
||||
python_abi_tags=python_abi_tags,
|
||||
include_universal2=include_universal2,
|
||||
@@ -242,6 +247,7 @@ def _expected_wheels(
|
||||
musllinux_versions: list[str] | None,
|
||||
macosx_deployment_target: str,
|
||||
iphoneos_deployment_target: str,
|
||||
android_api_level: int,
|
||||
platform: str,
|
||||
python_abi_tags: list[str] | None,
|
||||
include_universal2: bool,
|
||||
@@ -387,12 +393,7 @@ def _expected_wheels(
|
||||
platform_tags.append(f"macosx_{min_macosx.replace('.', '_')}_universal2")
|
||||
|
||||
elif platform == "android":
|
||||
api_level = {
|
||||
"cp313-cp313": 21,
|
||||
"cp314-cp314": 24,
|
||||
"cp315-cp315": 24,
|
||||
}[python_abi_tag]
|
||||
platform_tags = [f"android_{api_level}_{machine_arch}"]
|
||||
platform_tags = [f"android_{android_api_level}_{machine_arch}"]
|
||||
|
||||
elif platform == "ios":
|
||||
if machine_arch == "x86_64":
|
||||
|
||||
@@ -667,3 +667,51 @@ def test_xbuild_tools_handling(tmp_path: Path, definition: str, expected: list[s
|
||||
|
||||
local = options.build_options("cp313-ios_13_0_arm64_iphoneos")
|
||||
assert local.xbuild_tools == expected
|
||||
|
||||
|
||||
DEFAULT_XBUILD_FILES = {
|
||||
"numpy": [
|
||||
"numpy/_core/include/numpy/_numpyconfig.h",
|
||||
"numpy/_core/include/numpy/numpyconfig.h",
|
||||
"numpy/_core/lib/libnpymath.a",
|
||||
"numpy/random/lib/libnpyrandom.a",
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("definition", "expected"),
|
||||
[
|
||||
("", DEFAULT_XBUILD_FILES),
|
||||
('xbuild-files = ""', {}),
|
||||
('xbuild-files = "package1: file1"', {"package1": ["file1"]}),
|
||||
(
|
||||
"xbuild-files = \"package1: 'file1 with space'; package2: file2a file2b\"",
|
||||
{"package1": ["file1 with space"], "package2": ["file2a", "file2b"]},
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_xbuild_files(tmp_path: Path, definition: str, expected: dict[str, list[str]]) -> None:
|
||||
args = CommandLineArguments.defaults()
|
||||
args.package_dir = tmp_path
|
||||
|
||||
pyproject_toml: Path = tmp_path / "pyproject.toml"
|
||||
pyproject_toml.write_text(
|
||||
textwrap.dedent(
|
||||
f"""\
|
||||
[tool.cibuildwheel]
|
||||
{definition}
|
||||
"""
|
||||
)
|
||||
)
|
||||
|
||||
options = Options(platform="ios", command_line_arguments=args, env={})
|
||||
assert options.build_options(None).xbuild_files == expected
|
||||
|
||||
pyproject_toml.write_text(
|
||||
"[tool.cibuildwheel.xbuild-files]\n"
|
||||
+ "\n".join(f"{package} = {files}" for package, files in expected.items())
|
||||
)
|
||||
|
||||
options = Options(platform="ios", command_line_arguments=args, env={})
|
||||
assert options.build_options(None).xbuild_files == expected
|
||||
|
||||
@@ -212,6 +212,28 @@ def test_parse_key_value_string() -> None:
|
||||
}
|
||||
|
||||
|
||||
def test_parse_key_value_string_unknown_name() -> None:
|
||||
# Unknown fields are not allowed by default.
|
||||
with pytest.raises(ValueError, match=r"Failed to parse 'key: value'. Unknown field name 'key'"):
|
||||
parse_key_value_string("key: value")
|
||||
|
||||
# Unknown fields can be enabled by passing "*".
|
||||
assert parse_key_value_string(
|
||||
"key: value",
|
||||
kw_arg_names=["*"],
|
||||
) == {
|
||||
"key": ["value"],
|
||||
}
|
||||
|
||||
assert parse_key_value_string(
|
||||
"key1: value1a value1b; key2: value2",
|
||||
kw_arg_names=["*"],
|
||||
) == {
|
||||
"key1": ["value1a", "value1b"],
|
||||
"key2": ["value2"],
|
||||
}
|
||||
|
||||
|
||||
def test_flexible_version_comparisons() -> None:
|
||||
assert FlexibleVersion("2.0") == FlexibleVersion("2")
|
||||
assert FlexibleVersion("2.0") < FlexibleVersion("2.1")
|
||||
|
||||
Reference in New Issue
Block a user