Files

478 lines
16 KiB
Python
Raw Permalink Normal View History

2021-05-03 11:45:43 -04:00
"""
Utility functions used by the cibuildwheel tests.
This file is added to the PYTHONPATH in the test runner at bin/run_test.py.
2021-05-03 11:45:43 -04:00
"""
2026-05-28 00:20:08 +02:00
from __future__ import annotations
2019-11-12 23:51:27 +00:00
import os
2020-02-20 16:12:54 -05:00
import platform as pm
2019-11-12 23:51:27 +00:00
import subprocess
import sys
2024-10-22 10:16:59 -04:00
from pathlib import Path
2021-02-14 20:44:20 +01:00
from tempfile import TemporaryDirectory
2026-04-01 10:23:02 -04:00
from typing import Final
2024-05-28 05:31:36 -07:00
import pytest
from cibuildwheel.architecture import Architecture
from cibuildwheel.ci import CIProvider, detect_ci_provider
from cibuildwheel.selector import EnableGroup
2025-01-27 20:35:56 +01:00
from cibuildwheel.util.file import CIBW_CACHE_PATH
2026-05-28 00:20:08 +02:00
TYPE_CHECKING = False
if TYPE_CHECKING:
from collections.abc import Generator, Mapping, Sequence
EMULATED_ARCHS: Final[list[str]] = sorted(
arch.value for arch in (Architecture.all_archs("linux") - Architecture.auto_archs("linux"))
)
PYPY_ARCHS = ["x86_64", "i686", "AMD64", "aarch64", "arm64"]
GRAALPY_ARCHS = ["x86_64", "AMD64", "aarch64", "arm64"]
SINGLE_PYTHON_VERSION: Final[tuple[int, int]] = (3, 13)
2024-05-26 12:41:20 +02:00
# temporary workaround: set by build_frontend_env fixture to skip graalpy
# when uv is the build frontend (compatibility issue between graalpy and uv)
include_graalpy_in_expected_wheels: bool = True
_AARCH64_CAN_RUN_ARMV7: Final[bool] = Architecture.aarch64.value not in EMULATED_ARCHS and {
None: Architecture.armv7l.value not in EMULATED_ARCHS,
CIProvider.travis_ci: False,
}.get(detect_ci_provider(), True)
2025-05-16 11:11:16 +01:00
def get_platform() -> str:
"""Return the current platform as determined by CIBW_PLATFORM or sys.platform."""
platform = os.environ.get("CIBW_PLATFORM", "")
if platform:
return platform
elif sys.platform.startswith("linux"):
return "linux"
elif sys.platform.startswith("darwin"):
return "macos"
elif sys.platform.startswith(("win32", "cygwin")):
return "windows"
else:
msg = f"Unsupported platform {sys.platform!r}"
raise Exception(msg)
2026-05-09 16:59:24 +02:00
DEFAULT_CIBW_ENABLE = "cpython-prerelease"
2025-05-16 11:11:16 +01:00
def get_enable_groups() -> frozenset[EnableGroup]:
value = os.environ.get("CIBW_ENABLE", DEFAULT_CIBW_ENABLE)
return EnableGroup.parse_option_value(value)
2019-04-27 12:12:02 +01:00
2024-10-22 10:16:59 -04:00
def cibuildwheel_get_build_identifiers(
project_path: Path,
env: dict[str, str] | None = None,
2024-10-22 10:16:59 -04:00
) -> list[str]:
2021-05-03 11:45:43 -04:00
"""
Returns the list of build identifiers that cibuildwheel will try to build
for the current platform.
2021-05-03 11:45:43 -04:00
"""
cmd = [sys.executable, "-m", "cibuildwheel", "--print-build-identifiers", str(project_path)]
if env is None:
env = os.environ.copy()
2021-02-14 12:56:33 -05:00
cmd_output = subprocess.run(
cmd,
text=True,
env=env,
2021-02-14 12:56:33 -05:00
check=True,
stdout=subprocess.PIPE,
).stdout
2021-05-03 11:45:43 -04:00
return cmd_output.strip().split("\n")
def _update_pip_cache_dir(env: dict[str, str]) -> None:
# Fix for pip concurrency bug https://github.com/pypa/pip/issues/11340
# See https://github.com/pypa/cibuildwheel/issues/1254 for discussion.
2025-05-16 11:11:16 +01:00
if get_platform() == "linux":
return
if "PIP_CACHE_DIR" in env:
return
worker_id = os.environ.get("PYTEST_XDIST_WORKER")
if worker_id is None or worker_id == "gw0":
return
pip_cache_dir = CIBW_CACHE_PATH / "test_cache" / f"pip_cache_dir_{worker_id}"
env["PIP_CACHE_DIR"] = str(pip_cache_dir)
def cibuildwheel_run(
2024-10-22 10:16:59 -04:00
project_path: str | Path,
package_dir: str | Path = ".",
env: dict[str, str] | None = None,
add_env: Mapping[str, str] | None = None,
output_dir: Path | None = None,
add_args: Sequence[str] | None = None,
single_python: bool = False,
) -> list[str]:
2021-05-03 11:45:43 -04:00
"""
2019-09-28 19:24:16 +02:00
Runs cibuildwheel as a subprocess, building the project at project_path.
Uses the current Python interpreter.
:param project_path: path of the project to be built.
:param package_dir: path of the package to be built. Can be absolute, or
relative to project_path.
:param env: full environment to be used, os.environ if None
:param add_env: environment used to update env
:param output_dir: directory where wheels are saved. If None, a temporary
directory will be used for the duration of the command.
:param add_args: Additional command-line arguments to pass to cibuildwheel.
:return: list of built wheels (file names).
2021-05-03 11:45:43 -04:00
"""
if env is None:
env = os.environ.copy()
# If present in the host environment, remove the MACOSX_DEPLOYMENT_TARGET for consistency
2021-05-03 11:45:43 -04:00
env.pop("MACOSX_DEPLOYMENT_TARGET", None)
2019-09-28 19:24:16 +02:00
if add_args is None:
add_args = []
if add_env is not None:
env.update(add_env)
_update_pip_cache_dir(env)
2024-05-26 12:41:20 +02:00
if single_python:
env["CIBW_BUILD"] = "cp{}{}-*".format(*SINGLE_PYTHON_VERSION)
2021-02-14 20:44:20 +01:00
with TemporaryDirectory() as tmp_output_dir:
2021-02-14 12:56:33 -05:00
subprocess.run(
2021-04-30 17:56:34 -04:00
[
sys.executable,
2021-05-03 11:45:43 -04:00
"-m",
"cibuildwheel",
"--output-dir",
2021-02-14 20:44:20 +01:00
str(output_dir or tmp_output_dir),
2021-04-30 17:56:34 -04:00
str(package_dir),
*add_args,
],
env=env,
cwd=project_path,
2021-02-14 12:56:33 -05:00
check=True,
)
wheels = sorted(p.name for p in (output_dir or Path(tmp_output_dir)).iterdir())
return wheels
def _floor_macosx(*args: str) -> str:
2021-05-03 11:45:43 -04:00
"""
Make sure a deployment target is not less than some value.
2021-05-03 11:45:43 -04:00
"""
return max(args, key=lambda x: tuple(map(int, x.split("."))))
2021-01-08 14:59:16 +00:00
2021-04-30 17:56:34 -04:00
def expected_wheels(
2024-10-22 10:16:59 -04:00
package_name: str,
package_version: str,
manylinux_versions: list[str] | None = None,
musllinux_versions: list[str] | None = None,
macosx_deployment_target: str | None = None,
iphoneos_deployment_target: str | None = None,
2024-10-22 10:16:59 -04:00
machine_arch: str | None = None,
platform: str | None = None,
2024-10-22 10:16:59 -04:00
python_abi_tags: list[str] | None = None,
include_universal2: bool = False,
single_python: bool = False,
single_arch: bool = False,
full_auto: bool = False,
) -> list[str]:
"""
Returns the expected wheels from a run of cibuildwheel.
"""
platform = platform or get_platform()
if machine_arch is None:
machine_arch = pm.machine()
if platform == "linux":
machine_arch = arch_name_for_linux(machine_arch)
if macosx_deployment_target is None:
macosx_deployment_target = os.environ.get("MACOSX_DEPLOYMENT_TARGET", "10.9")
if iphoneos_deployment_target is None:
iphoneos_deployment_target = os.environ.get("IPHONEOS_DEPLOYMENT_TARGET", "13.0")
architectures = [machine_arch]
if not single_arch:
if platform == "linux" and full_auto:
if machine_arch == "x86_64":
architectures.append("i686")
elif (
machine_arch == "aarch64"
and sys.platform.startswith("linux")
and _AARCH64_CAN_RUN_ARMV7
):
architectures.append("armv7l")
elif platform == "windows" and machine_arch == "AMD64":
architectures.append("x86")
return [
wheel
for architecture in architectures
for wheel in _expected_wheels(
package_name=package_name,
package_version=package_version,
machine_arch=architecture,
manylinux_versions=manylinux_versions,
musllinux_versions=musllinux_versions,
macosx_deployment_target=macosx_deployment_target,
iphoneos_deployment_target=iphoneos_deployment_target,
platform=platform,
python_abi_tags=python_abi_tags,
include_universal2=include_universal2,
single_python=single_python,
)
]
def _expected_wheels(
package_name: str,
package_version: str,
machine_arch: str,
manylinux_versions: list[str] | None,
musllinux_versions: list[str] | None,
macosx_deployment_target: str,
iphoneos_deployment_target: str,
platform: str,
python_abi_tags: list[str] | None,
include_universal2: bool,
single_python: bool,
) -> Generator[str, None, None]:
2021-05-03 11:45:43 -04:00
"""
Returns a list of expected wheels from a run of cibuildwheel.
2021-05-03 11:45:43 -04:00
"""
2019-12-02 11:07:56 +01:00
# per PEP 425 (https://www.python.org/dev/peps/pep-0425/), wheel files shall have name of the form
# {distribution}-{version}(-{build tag})?-{python tag}-{abi tag}-{platform tag}.whl
# {python tag} and {abi tag} are closely related to the python interpreter used to build the wheel
# so we'll merge them below as python_abi_tag
2020-02-20 16:12:54 -05:00
enable_groups = EnableGroup.parse_option_value(os.environ.get("CIBW_ENABLE", ""))
2020-04-06 23:10:19 +01:00
if manylinux_versions is None:
manylinux_versions = {
2025-05-28 19:42:00 -04:00
"armv7l": ["manylinux2014", "manylinux_2_17", "manylinux_2_31"],
"i686": ["manylinux1", "manylinux_2_28", "manylinux_2_5"],
2025-05-28 19:42:00 -04:00
"x86_64": ["manylinux1", "manylinux_2_28", "manylinux_2_5"],
2025-07-23 13:28:36 +02:00
"riscv64": ["manylinux_2_31", "manylinux_2_39"],
2025-05-28 19:42:00 -04:00
}.get(machine_arch, ["manylinux2014", "manylinux_2_17", "manylinux_2_28"])
2020-04-06 23:10:19 +01:00
if musllinux_versions is None:
musllinux_versions = ["musllinux_1_2"]
enable_groups = get_enable_groups()
# To be kept in sync with Python versions for Pyodide identifiers in cibuildwheel/selector.py.
2024-05-28 05:31:36 -07:00
if platform == "pyodide" and python_abi_tags is None:
python_abi_tags = ["cp313-cp313"]
if EnableGroup.PyodidePrerelease in enable_groups:
python_abi_tags.append("cp314-cp314")
elif (platform == "android" and python_abi_tags is None) or (
platform == "ios" and python_abi_tags is None
):
2025-09-08 15:58:03 +01:00
python_abi_tags = [
"cp313-cp313",
"cp314-cp314",
]
if EnableGroup.CPythonPrerelease in enable_groups:
python_abi_tags += ["cp315-cp315"]
elif python_abi_tags is None:
python_abi_tags = [
"cp39-cp39",
"cp310-cp310",
"cp311-cp311",
2023-05-26 21:14:55 -07:00
"cp312-cp312",
2024-05-12 02:26:18 +02:00
"cp313-cp313",
"cp314-cp314",
"cp314-cp314t",
]
2020-04-06 23:10:19 +01:00
2025-05-12 18:54:02 +02:00
if EnableGroup.CPythonPrerelease in enable_groups:
2026-05-11 19:35:40 +02:00
python_abi_tags += [
"cp315-cp315",
"cp315-cp315t",
]
2025-04-15 14:49:17 -04:00
2025-05-15 09:06:48 -04:00
if EnableGroup.PyPyEoL in enable_groups:
2023-06-17 12:00:27 +02:00
python_abi_tags += [
"pp39-pypy39_pp73",
2025-09-08 17:57:14 +03:00
"pp310-pypy310_pp73",
2025-05-15 09:06:48 -04:00
]
if EnableGroup.PyPy in enable_groups:
python_abi_tags += [
2025-02-12 16:51:33 +01:00
"pp311-pypy311_pp73",
2023-06-17 12:00:27 +02:00
]
if EnableGroup.GraalPy in enable_groups and include_graalpy_in_expected_wheels:
2025-04-29 00:25:17 +02:00
python_abi_tags += [
"graalpy311-graalpy242_311_native",
"graalpy312-graalpy250_312_native",
2025-04-29 00:25:17 +02:00
]
2020-04-06 23:10:19 +01:00
if machine_arch not in PYPY_ARCHS:
python_abi_tags = [tag for tag in python_abi_tags if not tag.startswith("pp")]
if machine_arch not in GRAALPY_ARCHS:
python_abi_tags = [tag for tag in python_abi_tags if not tag.startswith("graalpy")]
2024-05-26 12:41:20 +02:00
if single_python:
python_tag = "cp{}{}-".format(*SINGLE_PYTHON_VERSION)
python_abi_tags = [
next(
tag
for tag in python_abi_tags
if tag.startswith(python_tag) and not tag.endswith("t")
)
]
2020-04-06 23:10:19 +01:00
for python_abi_tag in python_abi_tags:
platform_tags = []
2021-05-03 11:45:43 -04:00
if platform == "linux":
2023-08-07 18:01:10 +02:00
if len(manylinux_versions) > 0:
platform_tags = [
".".join(
f"{manylinux_version}_{machine_arch}"
2023-08-07 18:01:10 +02:00
for manylinux_version in manylinux_versions
)
]
2025-04-29 00:25:17 +02:00
if len(musllinux_versions) > 0 and not python_abi_tag.startswith(("pp", "graalpy")):
platform_tags.append(
".".join(
f"{musllinux_version}_{machine_arch}"
for musllinux_version in musllinux_versions
)
)
2021-05-03 11:45:43 -04:00
elif platform == "windows":
2025-04-15 14:49:17 -04:00
platform_tags = {
"AMD64": ["win_amd64"],
"ARM64": ["win_arm64"],
"x86": ["win32"],
}.get(machine_arch, [])
2021-05-03 11:45:43 -04:00
elif platform == "macos":
2024-09-12 15:32:22 -04:00
if python_abi_tag.startswith("pp"):
2026-05-11 03:19:36 +02:00
min_macosx = _floor_macosx(macosx_deployment_target, "10.15")
2024-09-12 15:32:22 -04:00
elif python_abi_tag.startswith("cp"):
2026-05-11 03:19:36 +02:00
if python_abi_tag.startswith(("cp39", "cp310", "cp311")):
2024-09-12 15:32:22 -04:00
min_macosx = macosx_deployment_target
elif python_abi_tag.startswith(("cp312", "cp313")):
2024-09-12 15:32:22 -04:00
min_macosx = _floor_macosx(macosx_deployment_target, "10.13")
else:
min_macosx = _floor_macosx(macosx_deployment_target, "10.15")
elif python_abi_tag.startswith("graalpy"):
if python_abi_tag.startswith("graalpy311"):
min_macosx = macosx_deployment_target
else:
min_macosx = _floor_macosx(macosx_deployment_target, "10.13")
2024-09-12 15:32:22 -04:00
else:
min_macosx = macosx_deployment_target
if machine_arch == "arm64":
2024-09-12 15:32:22 -04:00
arm64_macosx = _floor_macosx(min_macosx, "11.0")
platform_tags = [f"macosx_{arm64_macosx.replace('.', '_')}_arm64"]
2021-01-01 18:56:36 +00:00
else:
platform_tags = [f"macosx_{min_macosx.replace('.', '_')}_x86_64"]
if include_universal2:
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}"]
elif platform == "ios":
if machine_arch == "x86_64":
platform_tags = [
f"ios_{iphoneos_deployment_target.replace('.', '_')}_x86_64_iphonesimulator"
]
elif machine_arch == "arm64":
platform_tags = [
f"ios_{iphoneos_deployment_target.replace('.', '_')}_arm64_iphoneos",
f"ios_{iphoneos_deployment_target.replace('.', '_')}_arm64_iphonesimulator",
]
else:
msg = f"Unsupported architecture {machine_arch!r} for iOS"
raise Exception(msg)
elif platform == "pyodide":
platform_tags = {
"cp312-cp312": ["pyodide_2024_0_wasm32"],
"cp313-cp313": ["pyemscripten_2025_0_wasm32"],
"cp314-cp314": ["pyemscripten_2026_0_wasm32"],
}.get(python_abi_tag, [])
if not platform_tags:
# for example if the python tag is `none` or `abi3`, the wheel
# is built by the stable cp313 Python version
platform_tags = ["pyemscripten_2025_0_wasm32"]
2020-04-06 23:10:19 +01:00
else:
2022-09-05 13:11:46 -04:00
msg = f"Unsupported platform {platform!r}"
raise Exception(msg)
2020-02-20 16:17:51 -05:00
2020-04-06 23:10:19 +01:00
for platform_tag in platform_tags:
yield f"{package_name}-{package_version}-{python_abi_tag}-{platform_tag}.whl"
2019-12-02 11:07:56 +01:00
2024-10-22 10:16:59 -04:00
def get_macos_version() -> tuple[int, int]:
2021-05-03 11:45:43 -04:00
"""
Returns the macOS major/minor version, as a tuple, e.g. (10, 15) or (11, 0)
These tuples can be used in comparisons, e.g.
(10, 14) <= (11, 0) == True
(11, 2) <= (11, 0) != True
2021-05-03 11:45:43 -04:00
"""
version_str, _, _ = pm.mac_ver()
2024-10-22 10:16:59 -04:00
return tuple(map(int, version_str.split(".")[:2])) # type: ignore[return-value]
def get_xcode_version() -> tuple[int, int]:
"""Calls `xcodebuild -version` to retrieve the Xcode version as a 2-tuple."""
output = subprocess.run(
["xcodebuild", "-version"],
text=True,
check=True,
stdout=subprocess.PIPE,
).stdout
lines = output.splitlines()
_, version_str = lines[0].split()
version_parts = version_str.split(".")
return (int(version_parts[0]), int(version_parts[1]))
2026-04-01 10:23:02 -04:00
def skip_if_pyodide(reason: str) -> pytest.MarkDecorator:
2025-05-16 11:11:16 +01:00
return pytest.mark.skipif(get_platform() == "pyodide", reason=reason)
2024-05-28 05:31:36 -07:00
def invoke_pytest() -> str:
# see https://github.com/pyodide/pyodide/issues/4802
if get_platform() == "pyodide":
2024-05-28 05:31:36 -07:00
return "python -m pytest"
return "pytest"
2024-10-22 10:16:59 -04:00
def arch_name_for_linux(arch: str) -> str:
"""
Archs have different names on different platforms, but it's useful to be
able to run linux tests on dev machines. This function translates between
the different names.
"""
if arch == "arm64":
return "aarch64"
return arch