Files
cibuildwheel/cibuildwheel/platforms/macos.py
T

750 lines
31 KiB
Python
Raw Normal View History

import dataclasses
import functools
import inspect
2019-11-12 23:51:27 +00:00
import os
2021-01-05 10:23:36 +00:00
import platform
2021-01-30 17:15:44 +00:00
import re
2019-11-12 23:51:27 +00:00
import shutil
import subprocess
2020-02-19 17:56:11 +01:00
import sys
2023-04-18 12:38:21 -04:00
import typing
from collections.abc import Set
2020-06-15 01:53:31 +02:00
from pathlib import Path
from typing import Literal, assert_never
from filelock import FileLock
from packaging.version import Version
from .. import errors
from ..architecture import Architecture
from ..ci import detect_ci_provider
from ..environment import ParsedEnvironment
from ..frontend import BuildFrontendName, get_build_frontend_extra_flags
from ..logger import log
from ..options import Options
from ..selector import BuildSelector
from ..util import resources
from ..util.cmd import call, shell
from ..util.file import (
CIBW_CACHE_PATH,
copy_test_sources,
2021-01-06 13:50:58 -05:00
download,
move_file,
2021-01-06 13:50:58 -05:00
)
from ..util.helpers import prepare_command, unwrap
from ..util.packaging import find_compatible_wheel, get_pip_version
from ..venv import constraint_flags, ensure_uv, virtualenv
2017-03-19 21:27:29 +00:00
@functools.cache
def get_macos_version() -> tuple[int, int]:
2021-05-03 11:45:43 -04:00
"""
2021-01-05 10:23:36 +00: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
2021-01-08 14:28:39 +00:00
(10, 14) <= (10, 16) == True
2021-01-05 10:23:36 +00:00
(11, 2) <= (11, 0) != True
2021-05-03 11:45:43 -04:00
"""
2021-01-05 10:23:36 +00:00
version_str, _, _ = platform.mac_ver()
version = tuple(map(int, version_str.split(".")[:2]))
2024-06-09 15:45:31 -04:00
if (10, 15) < version < (11, 0):
# When built against an older macOS SDK, Python will report macOS 10.16
# instead of the real version.
version_str = call(
sys.executable,
"-sS",
"-c",
"import platform; print(platform.mac_ver()[0])",
env={"SYSTEM_VERSION_COMPAT": "0"},
capture_stdout=True,
)
version = tuple(map(int, version_str.split(".")[:2]))
2025-11-07 18:48:20 -05:00
return typing.cast("tuple[int, int]", version)
2021-01-05 10:23:36 +00:00
@functools.cache
2024-06-09 15:45:31 -04:00
def get_test_macosx_deployment_target() -> str:
version = get_macos_version()
if version >= (11, 0):
return f"{version[0]}.0"
return f"{version[0]}.{version[1]}"
def get_macos_sdks() -> list[str]:
output = call("xcodebuild", "-showsdks", capture_stdout=True)
2021-05-03 11:45:43 -04:00
return [m.group(1) for m in re.finditer(r"-sdk (macosx\S+)", output)]
2021-01-30 17:15:44 +00:00
@dataclasses.dataclass(frozen=True, kw_only=True)
2022-04-28 09:19:27 -04:00
class PythonConfiguration:
version: str
identifier: str
url: str
def all_python_configurations() -> list[PythonConfiguration]:
config_dicts = resources.read_python_configs("macos")
return [PythonConfiguration(**item) for item in config_dicts]
2021-04-30 17:56:34 -04:00
def get_python_configurations(
build_selector: BuildSelector, architectures: Set[Architecture]
) -> list[PythonConfiguration]:
python_configurations = all_python_configurations()
# filter out configs that don't match any of the selected architectures
2021-04-30 17:56:34 -04:00
python_configurations = [
c
for c in python_configurations
if any(c.identifier.endswith(a.value) for a in architectures)
]
# skip builds as required by BUILD/SKIP
2023-01-14 09:48:01 +01:00
python_configurations = [c for c in python_configurations if build_selector(c.identifier)]
2025-04-29 00:25:17 +02:00
# filter-out some cross-compilation configs with PyPy and GraalPy:
2023-01-14 09:48:01 +01:00
# can't build arm64 on x86_64
# rosetta allows to build x86_64 on arm64
if platform.machine() == "x86_64":
python_configurations_before = set(python_configurations)
python_configurations = [
c
for c in python_configurations
2025-04-29 00:25:17 +02:00
if not (c.identifier.startswith(("pp", "gp")) and c.identifier.endswith("arm64"))
2023-01-14 09:48:01 +01:00
]
removed_elements = python_configurations_before - set(python_configurations)
if removed_elements:
ids = ", ".join(c.identifier for c in removed_elements)
log.quiet(
unwrap(
f"""
Note: {ids} {"was" if len(removed_elements) == 1 else "were"}
2023-01-14 09:48:01 +01:00
selected, but can't be built on x86_64 so will be skipped automatically.
"""
)
)
return python_configurations
def install_cpython(_tmp: Path, version: str, url: str, free_threading: bool) -> Path:
ft = "T" if free_threading else ""
installation_path = Path(f"/Library/Frameworks/Python{ft}.framework/Versions/{version}")
with FileLock(CIBW_CACHE_PATH / f"cpython{version}.lock"):
installed_system_packages = call("pkgutil", "--pkgs", capture_stdout=True).splitlines()
# if this version of python isn't installed, get it from python.org and install
python_package_identifier = f"org.python.Python.Python{ft}Framework-{version}"
if python_package_identifier not in installed_system_packages:
if detect_ci_provider() is None:
# if running locally, we don't want to install CPython with sudo
# let the user know & provide a link to the installer
msg = inspect.cleandoc(
f"""
Framework CPython {version} not detected as installed in:
{installation_path}
cibuildwheel will not perform system-wide installs when running outside of CI.
To build locally, install CPython {version} on this machine, or, disable this
version of Python using CIBW_SKIP=cp{version.replace(".", "")}-macosx_*
For portable builds, cibuildwheel needs the official builds from python.org.
Download link: {url}
"""
)
raise errors.FatalError(msg)
python_filename = url.split("/")[-1]
pkg_path = CIBW_CACHE_PATH / "cpython-installer" / python_filename
if not pkg_path.exists():
download(url, pkg_path)
args = []
if version.startswith("3.13"):
# Python 3.13 is the first version to have a free-threading option
2025-01-27 20:35:56 +01:00
args += ["-applyChoiceChangesXML", str(resources.FREE_THREAD_ENABLE_313.resolve())]
2025-05-12 18:54:02 +02:00
elif version.startswith("3.14"):
args += ["-applyChoiceChangesXML", str(resources.FREE_THREAD_ENABLE_314.resolve())]
call("sudo", "installer", "-pkg", pkg_path, *args, "-target", "/")
pkg_path.unlink()
env = os.environ.copy()
env["PIP_DISABLE_PIP_VERSION_CHECK"] = "1"
if free_threading:
call(installation_path / f"bin/python{version}t", "-m", "ensurepip", env=env)
2025-01-27 20:35:56 +01:00
call(
installation_path / f"bin/python{version}t",
resources.INSTALL_CERTIFI_SCRIPT,
env=env,
)
else:
2025-01-27 20:35:56 +01:00
call(installation_path / "bin/python3", resources.INSTALL_CERTIFI_SCRIPT, env=env)
return installation_path / "bin" / (f"python{version}t" if free_threading else "python3")
def install_pypy(tmp: Path, url: str) -> Path:
2021-05-03 11:45:43 -04:00
pypy_tar_bz2 = url.rsplit("/", 1)[-1]
2020-06-15 01:53:31 +02:00
extension = ".tar.bz2"
assert pypy_tar_bz2.endswith(extension)
installation_path = CIBW_CACHE_PATH / pypy_tar_bz2[: -len(extension)]
with FileLock(str(installation_path) + ".lock"):
if not installation_path.exists():
downloaded_tar_bz2 = tmp / pypy_tar_bz2
download(url, downloaded_tar_bz2)
installation_path.parent.mkdir(parents=True, exist_ok=True)
call("tar", "-C", installation_path.parent, "-xf", downloaded_tar_bz2)
downloaded_tar_bz2.unlink()
return installation_path / "bin" / "pypy3"
2025-04-29 00:25:17 +02:00
def install_graalpy(tmp: Path, url: str) -> Path:
graalpy_archive = url.rsplit("/", 1)[-1]
extension = ".tar.gz"
assert graalpy_archive.endswith(extension)
installation_path = CIBW_CACHE_PATH / graalpy_archive[: -len(extension)]
with FileLock(str(installation_path) + ".lock"):
if not installation_path.exists():
downloaded_archive = tmp / graalpy_archive
download(url, downloaded_archive)
installation_path.mkdir(parents=True)
# GraalPy top-folder name is inconsistent with archive name
call("tar", "-C", installation_path, "--strip-components=1", "-xzf", downloaded_archive)
downloaded_archive.unlink()
return installation_path / "bin" / "graalpy"
2021-04-30 17:56:34 -04:00
def setup_python(
tmp: Path,
2021-04-30 17:56:34 -04:00
python_configuration: PythonConfiguration,
dependency_constraint: Path | None,
2021-04-30 17:56:34 -04:00
environment: ParsedEnvironment,
2023-08-26 19:37:29 +01:00
build_frontend: BuildFrontendName,
) -> tuple[Path, dict[str, str]]:
use_uv = build_frontend == "build[uv]"
if use_uv:
ensure_uv()
2024-06-09 15:45:31 -04:00
tmp.mkdir()
2020-11-13 16:30:27 +00:00
implementation_id = python_configuration.identifier.split("-")[0]
2021-05-03 11:45:43 -04:00
log.step(f"Installing Python {implementation_id}...")
if implementation_id.startswith("cp"):
free_threading = "t-macos" in python_configuration.identifier
base_python = install_cpython(
tmp, python_configuration.version, python_configuration.url, free_threading
)
2021-05-03 11:45:43 -04:00
elif implementation_id.startswith("pp"):
base_python = install_pypy(tmp, python_configuration.url)
2025-04-29 00:25:17 +02:00
elif implementation_id.startswith("gp"):
base_python = install_graalpy(tmp, python_configuration.url)
2020-04-05 16:03:35 +02:00
else:
2022-09-05 13:11:46 -04:00
msg = "Unknown Python implementation"
raise ValueError(msg)
assert base_python.exists(), (
f"{base_python.name} not found, has {list(base_python.parent.iterdir())}"
)
2020-04-08 10:24:16 +02:00
2021-05-03 11:45:43 -04:00
log.step("Setting up build environment...")
venv_path = tmp / "venv"
2024-05-20 08:21:42 +02:00
env = virtualenv(
2024-06-09 15:45:31 -04:00
python_configuration.version,
base_python,
venv_path,
dependency_constraint,
2024-06-09 15:45:31 -04:00
use_uv=use_uv,
2024-05-20 08:21:42 +02:00
)
venv_bin_path = venv_path / "bin"
assert venv_bin_path.exists()
2020-04-05 16:03:35 +02:00
# Fix issue with site.py setting the wrong `sys.prefix`, `sys.exec_prefix`,
# `sys.path`, ... for PyPy: https://foss.heptapod.net/pypy/pypy/issues/3175
# Also fix an issue with the shebang of installed scripts inside the
# testing virtualenv- see https://github.com/theacodes/nox/issues/44 and
# https://github.com/pypa/virtualenv/issues/620
# Also see https://github.com/python/cpython/pull/9516
2021-05-03 11:45:43 -04:00
env.pop("__PYVENV_LAUNCHER__", None)
2020-04-05 16:03:35 +02:00
2021-01-24 15:08:46 +00:00
# we version pip ourselves, so we don't care about pip version checking
2021-05-03 11:45:43 -04:00
env["PIP_DISABLE_PIP_VERSION_CHECK"] = "1"
2021-01-24 15:08:46 +00:00
# Apply our environment after pip is ready
env = environment.as_dictionary(prev_environment=env)
# check what Python version we're on
which_python = call("which", "python", env=env, capture_stdout=True).strip()
print(which_python)
if which_python != str(venv_bin_path / "python"):
msg = "python available on PATH doesn't match our installed instance. If you have modified PATH, ensure that you don't overwrite cibuildwheel's entry or insert python above it."
raise errors.FatalError(msg)
call("python", "--version", env=env)
# check what pip version we're on
if not use_uv:
assert (venv_bin_path / "pip").exists()
which_pip = call("which", "pip", env=env, capture_stdout=True).strip()
print(which_pip)
if which_pip != str(venv_bin_path / "pip"):
msg = "pip available on PATH doesn't match our installed instance. If you have modified PATH, ensure that you don't overwrite cibuildwheel's entry or insert pip above it."
raise errors.FatalError(msg)
call("pip", "--version", env=env)
2021-05-03 11:45:43 -04:00
config_is_arm64 = python_configuration.identifier.endswith("arm64")
config_is_universal2 = python_configuration.identifier.endswith("universal2")
2021-04-29 20:21:42 -04:00
# Set MACOSX_DEPLOYMENT_TARGET, if the user didn't set it.
# For arm64, the minimal deployment target is 11.0.
# On x86_64 (or universal2), use 10.9 as a default.
2024-09-12 15:32:22 -04:00
# CPython 3.12.6+ needs 10.13.
# CPython 3.14.0 needs 10.15.
if config_is_arm64:
default_target = "11.0"
elif Version(python_configuration.version) >= Version("3.14"):
default_target = "10.15"
2024-09-12 15:32:22 -04:00
elif Version(python_configuration.version) >= Version("3.12"):
default_target = "10.13"
elif python_configuration.identifier.startswith("pp") and Version(
python_configuration.version
) >= Version("3.9"):
default_target = "10.15"
else:
default_target = "10.9"
env.setdefault("MACOSX_DEPLOYMENT_TARGET", default_target)
# This is a floor, it can't be set lower than the default_target.
if Version(env["MACOSX_DEPLOYMENT_TARGET"]) < Version(default_target):
log.warning(
f"Bumping MACOSX_DEPLOYMENT_TARGET ({env['MACOSX_DEPLOYMENT_TARGET']}) to the minimum required ({default_target})."
)
env["MACOSX_DEPLOYMENT_TARGET"] = default_target
2025-02-25 20:23:01 +01:00
if config_is_arm64:
# macOS 11 is the first OS with arm64 support, so the wheels
# have that as a minimum.
env.setdefault("_PYTHON_HOST_PLATFORM", "macosx-11.0-arm64")
env.setdefault("ARCHFLAGS", "-arch arm64")
elif config_is_universal2:
env.setdefault("_PYTHON_HOST_PLATFORM", "macosx-10.9-universal2")
env.setdefault("ARCHFLAGS", "-arch arm64 -arch x86_64")
elif python_configuration.identifier.endswith("x86_64"):
# even on the macos11.0 Python installer, on the x86_64 side it's
# compatible back to 10.9.
env.setdefault("_PYTHON_HOST_PLATFORM", "macosx-10.9-x86_64")
env.setdefault("ARCHFLAGS", "-arch x86_64")
2021-04-29 20:21:42 -04:00
building_arm64 = config_is_arm64 or config_is_universal2
2021-05-03 11:45:43 -04:00
if building_arm64 and get_macos_version() < (10, 16) and "SDKROOT" not in env:
# xcode 12.2 or higher can build arm64 on macos 10.15 or below, but
# needs the correct SDK selected.
sdks = get_macos_sdks()
# Different versions of Xcode contain different SDK versions...
# we're happy with anything newer than macOS 11.0
2021-05-03 11:45:43 -04:00
arm64_compatible_sdks = [s for s in sdks if not s.startswith("macosx10.")]
2021-01-30 17:15:44 +00:00
if not arm64_compatible_sdks:
2021-04-30 17:56:34 -04:00
log.warning(
unwrap(
2021-05-03 11:45:43 -04:00
"""
2021-01-30 17:15:44 +00:00
SDK for building arm64-compatible wheels not found. You need Xcode 12.2 or later
to build universal2 or arm64 wheels.
2021-05-03 11:45:43 -04:00
"""
2021-04-30 17:56:34 -04:00
)
)
else:
2021-05-03 11:45:43 -04:00
env.setdefault("SDKROOT", arm64_compatible_sdks[0])
2021-05-03 11:45:43 -04:00
log.step("Installing build tools...")
2025-07-08 08:59:20 -04:00
match build_frontend:
case "pip":
call(
"pip",
"install",
"--upgrade",
"delocate",
*constraint_flags(dependency_constraint),
env=env,
)
case "build":
call(
"pip",
"install",
"--upgrade",
"delocate",
"build[virtualenv]",
*constraint_flags(dependency_constraint),
env=env,
)
case "build[uv]":
call(
"uv",
2025-07-08 08:59:20 -04:00
"pip",
"install",
"--upgrade",
"delocate",
"build",
2025-07-08 08:59:20 -04:00
*constraint_flags(dependency_constraint),
env=env,
)
case _:
assert_never(build_frontend)
2020-11-13 16:30:27 +00:00
return base_python, env
2020-04-05 16:03:35 +02:00
def build(options: Options, tmp_path: Path) -> None:
2021-10-12 02:05:47 +01:00
python_configurations = get_python_configurations(
options.globals.build_selector, options.globals.architectures
)
2021-09-19 00:19:28 -04:00
2022-10-07 08:47:31 -04:00
if not python_configurations:
2022-04-19 16:00:12 +01:00
return
2021-10-12 02:05:47 +01:00
2022-04-19 16:00:12 +01:00
try:
before_all_options_identifier = python_configurations[0].identifier
before_all_options = options.build_options(before_all_options_identifier)
if before_all_options.before_all:
log.step("Running before_all...")
env = before_all_options.environment.as_dictionary(prev_environment=os.environ)
env.setdefault("MACOSX_DEPLOYMENT_TARGET", "10.9")
before_all_prepared = prepare_command(
2022-04-21 10:34:11 +01:00
before_all_options.before_all, project=".", package=before_all_options.package_dir
2022-04-19 16:00:12 +01:00
)
shell(before_all_prepared, env=env)
2020-05-11 17:09:54 +02:00
built_wheels: list[Path] = []
2022-04-18 14:24:53 +02:00
2020-11-13 16:30:27 +00:00
for config in python_configurations:
2021-10-12 02:05:47 +01:00
build_options = options.build_options(config.identifier)
build_frontend = build_options.build_frontend
2025-02-25 20:23:01 +01:00
use_uv = build_frontend.name == "build[uv]"
if use_uv:
ensure_uv()
pip = ["pip"] if not use_uv else ["uv", "pip"]
2020-11-13 16:30:27 +00:00
log.build_start(config.identifier)
2019-11-17 17:56:07 +01:00
identifier_tmp_dir = tmp_path / config.identifier
identifier_tmp_dir.mkdir()
built_wheel_dir = identifier_tmp_dir / "built_wheel"
repaired_wheel_dir = identifier_tmp_dir / "repaired_wheel"
2021-05-03 11:45:43 -04:00
config_is_arm64 = config.identifier.endswith("arm64")
config_is_universal2 = config.identifier.endswith("universal2")
2021-04-29 20:21:42 -04:00
2025-03-10 21:45:54 +00:00
constraints_path = build_options.dependency_constraints.get_for_python_version(
version=config.version, tmp_dir=identifier_tmp_dir
)
2019-11-19 23:36:20 +01:00
base_python, env = setup_python(
identifier_tmp_dir / "build",
2021-06-23 10:47:18 -04:00
config,
constraints_path,
2021-10-12 02:05:47 +01:00
build_options.environment,
2023-08-26 19:37:29 +01:00
build_frontend.name,
2021-06-23 10:47:18 -04:00
)
pip_version = None if use_uv else get_pip_version(env)
compatible_wheel = find_compatible_wheel(built_wheels, config.identifier)
if compatible_wheel:
2022-04-18 14:24:53 +02:00
log.step_end()
print(
f"\nFound previously built wheel {compatible_wheel.name}, that's compatible with {config.identifier}. Skipping build step..."
2021-04-30 17:56:34 -04:00
)
repaired_wheel = compatible_wheel
2022-04-18 14:24:53 +02:00
else:
if build_options.before_build:
log.step("Running before_build...")
before_build_prepared = prepare_command(
build_options.before_build, project=".", package=build_options.package_dir
2021-10-12 02:05:47 +01:00
)
2022-04-18 14:24:53 +02:00
shell(before_build_prepared, env=env)
2022-04-18 14:24:53 +02:00
log.step("Building wheel...")
built_wheel_dir.mkdir()
2025-01-27 20:35:56 +01:00
extra_flags = get_build_frontend_extra_flags(
build_frontend, build_options.build_verbosity, build_options.config_settings
2023-08-26 19:37:29 +01:00
)
build_env = env.copy()
2025-07-08 08:59:20 -04:00
match build_frontend.name:
case "pip":
# Path.resolve() is needed. Without it pip wheel may try to fetch package from pypi.org
# see https://github.com/pypa/cibuildwheel/pull/369
call(
"python",
"-m",
"pip",
"wheel",
build_options.package_dir.resolve(),
f"--wheel-dir={built_wheel_dir}",
"--no-deps",
*extra_flags,
env=build_env,
)
case "build" | "build[uv]":
if (
use_uv
and "--no-isolation" not in extra_flags
and "-n" not in extra_flags
):
extra_flags.append("--installer=uv")
call(
"python",
"-m",
"build",
build_options.package_dir,
"--wheel",
f"--outdir={built_wheel_dir}",
*extra_flags,
env=build_env,
)
case _:
assert_never(build_frontend)
2020-12-31 16:48:43 +00:00
2022-04-18 14:24:53 +02:00
built_wheel = next(built_wheel_dir.glob("*.whl"))
2017-03-19 21:27:29 +00:00
2022-04-18 14:24:53 +02:00
repaired_wheel_dir.mkdir()
2020-03-01 11:09:22 +00:00
2022-04-18 14:24:53 +02:00
if built_wheel.name.endswith("none-any.whl"):
raise errors.NonPlatformWheelError()
2022-04-18 14:24:53 +02:00
if build_options.repair_command:
log.step("Repairing wheel...")
if config_is_universal2:
delocate_archs = "x86_64,arm64"
elif config_is_arm64:
delocate_archs = "arm64"
else:
delocate_archs = "x86_64"
repair_command_prepared = prepare_command(
build_options.repair_command,
wheel=built_wheel,
dest_dir=repaired_wheel_dir,
delocate_archs=delocate_archs,
package=build_options.package_dir,
project=".",
2022-04-18 14:24:53 +02:00
)
shell(repair_command_prepared, env=env)
else:
shutil.move(str(built_wheel), repaired_wheel_dir)
try:
repaired_wheel = next(repaired_wheel_dir.glob("*.whl"))
except StopIteration:
raise errors.RepairStepProducedNoWheelError() from None
2022-04-18 14:24:53 +02:00
if repaired_wheel.name in {wheel.name for wheel in built_wheels}:
raise errors.AlreadyBuiltWheelError(repaired_wheel.name)
2022-04-18 14:24:53 +02:00
log.step_end()
2021-10-12 02:05:47 +01:00
if build_options.test_command and build_options.test_selector(config.identifier):
machine_arch = platform.machine()
python_arch = call(
"python",
"-sSc",
"import platform; print(platform.machine())",
env=env,
capture_stdout=True,
).strip()
2023-02-07 10:25:34 -05:00
testing_archs: list[Literal["x86_64", "arm64"]]
2021-04-29 20:21:42 -04:00
if config_is_arm64:
2021-05-03 11:45:43 -04:00
testing_archs = ["arm64"]
2021-04-29 20:21:42 -04:00
elif config_is_universal2:
2021-05-03 11:45:43 -04:00
testing_archs = ["x86_64", "arm64"]
else:
2021-05-03 11:45:43 -04:00
testing_archs = ["x86_64"]
2021-01-05 19:02:15 +00:00
for testing_arch in testing_archs:
2021-04-29 20:21:42 -04:00
if config_is_universal2:
2021-05-03 11:45:43 -04:00
arch_specific_identifier = f"{config.identifier}:{testing_arch}"
2021-10-12 02:05:47 +01:00
if not build_options.test_selector(arch_specific_identifier):
continue
2021-05-03 11:45:43 -04:00
if machine_arch == "x86_64" and testing_arch == "arm64":
2021-04-29 20:21:42 -04:00
if config_is_arm64:
2021-04-30 17:56:34 -04:00
log.warning(
unwrap(
2021-05-03 11:45:43 -04:00
"""
2021-05-02 16:37:38 +02:00
While arm64 wheels can be built on x86_64, they cannot be
tested. Consider building arm64 wheels natively, if your CI
provider offers this. To silence this warning, set
2023-11-18 12:37:50 +02:00
`CIBW_TEST_SKIP: "*-macosx_arm64"`.
2021-05-03 11:45:43 -04:00
"""
2021-04-30 17:56:34 -04:00
)
)
2021-04-29 20:21:42 -04:00
elif config_is_universal2:
2021-04-30 17:56:34 -04:00
log.warning(
unwrap(
2021-05-03 11:45:43 -04:00
"""
2021-05-02 16:37:38 +02:00
While universal2 wheels can be built on x86_64, the arm64 part
of the wheel cannot be tested on x86_64. Consider building
universal2 wheels on an arm64 runner, if your CI provider offers
this. Notably, an arm64 runner can also test the x86_64 part of
the wheel, through Rosetta emulation. To silence this warning,
set `CIBW_TEST_SKIP: "*-macosx_universal2:arm64"`.
2021-05-03 11:45:43 -04:00
"""
2021-04-30 17:56:34 -04:00
)
)
else:
2022-09-05 13:11:46 -04:00
msg = "unreachable"
raise RuntimeError(msg)
# skip this test
continue
is_cp38 = config.identifier.startswith("cp38-")
if testing_arch == "arm64" and is_cp38 and python_arch != "arm64":
log.warning(
unwrap(
"""
While cibuildwheel can build CPython 3.8 universal2/arm64 wheels, we
cannot test the arm64 part of them, even when running on an Apple
Silicon machine. This is because we use the x86_64 installer of
CPython 3.8. See the discussion in
https://github.com/pypa/cibuildwheel/pull/1169 for the details. To
2023-11-18 12:37:50 +02:00
silence this warning, set `CIBW_TEST_SKIP: "cp38-macosx_*:arm64"`.
"""
)
)
# skip this test
continue
2021-04-30 17:56:34 -04:00
log.step(
2021-05-03 11:45:43 -04:00
"Testing wheel..."
2021-04-30 17:56:34 -04:00
if testing_arch == machine_arch
2021-05-03 11:45:43 -04:00
else f"Testing wheel on {testing_arch}..."
2021-04-30 17:56:34 -04:00
)
2020-01-07 00:17:03 +01:00
arch_prefix = []
2024-06-09 15:45:31 -04:00
uv_arch_args = []
if testing_arch != machine_arch:
2021-05-03 11:45:43 -04:00
if machine_arch == "arm64" and testing_arch == "x86_64":
# rosetta2 will provide the emulation with just the arch prefix.
2021-05-03 11:45:43 -04:00
arch_prefix = ["arch", "-x86_64"]
2024-06-09 15:45:31 -04:00
uv_arch_args = ["--python-platform", "x86_64-apple-darwin"]
else:
msg = f"don't know how to emulate {testing_arch} on {machine_arch}"
raise RuntimeError(msg)
2019-10-12 10:44:40 +01:00
# define a custom 'call' function that adds the arch prefix each time
call_with_arch = functools.partial(call, *arch_prefix)
2022-07-25 16:15:00 +02:00
shell_with_arch = functools.partial(call, *arch_prefix, "/bin/sh", "-c")
# set up a virtual environment to install and test from, to make sure
# there are no dependencies that were pulled in at build time.
venv_dir = identifier_tmp_dir / f"venv-test-{testing_arch}"
virtualenv_env = virtualenv(
config.version,
base_python,
venv_dir,
None,
use_uv=use_uv,
env=env,
pip_version=pip_version,
)
2024-06-09 15:45:31 -04:00
if use_uv:
pip_install = functools.partial(call, *pip, "install", *uv_arch_args)
else:
pip_install = functools.partial(call_with_arch, *pip, "install")
2017-07-02 18:02:27 -05:00
2024-06-09 15:45:31 -04:00
virtualenv_env["MACOSX_DEPLOYMENT_TARGET"] = get_test_macosx_deployment_target()
virtualenv_env = build_options.test_environment.as_dictionary(
prev_environment=virtualenv_env
)
# check that we are using the Python from the virtual environment
call_with_arch("which", "python", env=virtualenv_env)
2021-10-12 02:05:47 +01:00
if build_options.before_test:
2021-04-30 17:56:34 -04:00
before_test_prepared = prepare_command(
2021-10-12 02:05:47 +01:00
build_options.before_test,
project=".",
package=build_options.package_dir,
2021-04-30 17:56:34 -04:00
)
shell_with_arch(before_test_prepared, env=virtualenv_env)
# install the wheel
if is_cp38 and python_arch == "x86_64":
virtualenv_env_install_wheel = virtualenv_env.copy()
virtualenv_env_install_wheel["SYSTEM_VERSION_COMPAT"] = "0"
log.notice(
unwrap(
"""
Setting SYSTEM_VERSION_COMPAT=0 to ensure CPython 3.8 can get
correct macOS version and allow installation of wheels with
MACOSX_DEPLOYMENT_TARGET >= 11.0.
See https://github.com/pypa/cibuildwheel/issues/1767 for the
details.
"""
)
)
else:
virtualenv_env_install_wheel = virtualenv_env
2024-06-09 15:45:31 -04:00
pip_install(
f"{repaired_wheel}{build_options.test_extras}",
env=virtualenv_env_install_wheel,
2021-04-30 17:56:34 -04:00
)
# test the wheel
2021-10-12 02:05:47 +01:00
if build_options.test_requires:
2024-06-09 15:45:31 -04:00
pip_install(
*build_options.test_requires,
env=virtualenv_env_install_wheel,
2021-04-30 17:56:34 -04:00
)
# run the tests from a temp dir, with an absolute path in the command
# (this ensures that Python runs the tests against the installed wheel
# and not the repo code)
test_command_prepared = prepare_command(
2021-10-12 02:05:47 +01:00
build_options.test_command,
project=Path.cwd(),
2021-10-12 02:05:47 +01:00
package=build_options.package_dir.resolve(),
wheel=repaired_wheel,
2021-04-30 17:56:34 -04:00
)
test_cwd = identifier_tmp_dir / "test_cwd"
if build_options.test_sources:
# only create test_cwd if it doesn't already exist - it
# may have been created during a previous `testing_arch`
if not test_cwd.exists():
test_cwd.mkdir()
copy_test_sources(
build_options.test_sources,
Path.cwd(),
test_cwd,
)
else:
# Use the test_fail.py file to raise a nice error if the user
# tries to run tests in the cwd
test_cwd.mkdir(exist_ok=True)
(test_cwd / "test_fail.py").write_text(
resources.TEST_FAIL_CWD_FILE.read_text()
)
shell_with_arch(test_command_prepared, cwd=test_cwd, env=virtualenv_env)
2020-11-13 16:30:27 +00:00
# we're all done here; move it to output (overwrite existing)
2025-07-15 20:45:53 -07:00
output_wheel = None
if compatible_wheel is None:
output_wheel = build_options.output_dir.joinpath(repaired_wheel.name)
moved_wheel = move_file(repaired_wheel, output_wheel)
if moved_wheel != output_wheel.resolve():
log.warning(
f"{repaired_wheel} was moved to {moved_wheel} instead of {output_wheel}"
)
built_wheels.append(output_wheel)
# clean up
shutil.rmtree(identifier_tmp_dir)
2025-07-15 20:45:53 -07:00
log.build_end(output_wheel)
2020-11-13 16:30:27 +00:00
except subprocess.CalledProcessError as error:
msg = f"Command {error.cmd} failed with code {error.returncode}. {error.stdout or ''}"
raise errors.FatalError(msg) from error