2025-05-28 05:41:26 -04:00
import dataclasses
2022-02-27 14:16:37 -05:00
import functools
2024-10-18 08:52:05 -04:00
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
2025-04-09 09:58:57 -04:00
from collections.abc import Set
2020-06-15 01:53:31 +02:00
from pathlib import Path
2025-01-16 16:45:18 +01:00
from typing import Literal , assert_never
2020-04-08 00:16:25 +02:00
2022-01-08 11:30:48 +01:00
from filelock import FileLock
2024-05-27 23:04:25 +02:00
from packaging.version import Version
2022-01-08 11:30:48 +01:00
2025-03-24 16:58:47 +00:00
from .. import errors
from ..architecture import Architecture
from ..ci import detect_ci_provider
from ..environment import ParsedEnvironment
2025-07-23 22:18:33 +01:00
from ..frontend import BuildFrontendName , get_build_frontend_extra_flags
2025-03-24 16:58:47 +00:00
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 (
2022-01-08 11:30:48 +01:00
CIBW_CACHE_PATH ,
2025-01-10 23:24:24 +08:00
copy_test_sources ,
2021-01-06 13:50:58 -05:00
download ,
2024-06-07 17:37:20 +02:00
move_file ,
2021-01-06 13:50:58 -05:00
)
2025-03-24 16:58:47 +00:00
from ..util.helpers import prepare_command , unwrap
2025-11-06 22:03:00 +00:00
from ..util.packaging import find_compatible_wheel , get_pip_version
2025-12-19 17:20:39 +00:00
from ..venv import constraint_flags , ensure_uv , virtualenv
2017-03-19 21:27:29 +00:00
2017-04-10 21:42:12 +01:00
2025-01-16 16:45:18 +01:00
@functools.cache
2022-07-14 13:36:57 +02:00
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
2025-01-16 16:45:18 +01: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 ] } "
2022-07-14 13:36:57 +02:00
def get_macos_sdks () -> list [ str ]:
2022-01-05 02:59:24 +01:00
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
2025-05-28 05:41:26 -04:00
@dataclasses.dataclass ( frozen = True , kw_only = True )
2022-04-28 09:19:27 -04:00
class PythonConfiguration :
2020-04-10 01:53:06 +02:00
version : str
identifier : str
url : str
2020-04-08 00:16:25 +02:00
2025-03-24 16:58:47 +00:00
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 (
2023-04-06 11:40:25 -07:00
build_selector : BuildSelector , architectures : Set [ Architecture ]
2022-07-14 13:36:57 +02:00
) -> list [ PythonConfiguration ]:
2025-03-24 16:58:47 +00:00
python_configurations = all_python_configurations ()
2019-04-20 18:52:01 +02:00
2021-01-05 15:14:32 +00:00
# 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 )
]
2021-01-05 15:14:32 +00:00
# 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 """
2025-01-20 11:17:01 +00:00
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
2019-04-20 18:52:01 +02:00
2025-01-10 23:28:35 +08:00
def install_cpython ( _tmp : Path , version : str , url : str , free_threading : bool ) -> Path :
2024-06-08 10:15:04 +02:00
ft = "T" if free_threading else ""
installation_path = Path ( f "/Library/Frameworks/Python { ft } .framework/Versions/ { version } " )
2022-01-08 11:30:48 +01:00
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
2024-06-08 10:15:04 +02:00
python_package_identifier = f "org.python.Python.Python { ft } Framework- { version } "
2022-01-08 11:30:48 +01:00
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
2024-10-18 08:52:05 -04:00
msg = inspect . cleandoc (
f """
2025-11-28 10:48:44 -05:00
Framework CPython { version } not detected as installed in:
{ installation_path }
2024-10-18 08:52:05 -04:00
cibuildwheel will not perform system-wide installs when running outside of CI.
To build locally, install CPython { version } on this machine, or, disable this
2025-01-20 11:17:01 +00:00
version of Python using CIBW_SKIP=cp { version . replace ( "." , "" ) } -macosx_*
2024-10-18 08:52:05 -04:00
For portable builds, cibuildwheel needs the official builds from python.org.
Download link: { url }
"""
2022-01-08 11:30:48 +01:00
)
2024-06-10 15:36:20 +01:00
raise errors . FatalError ( msg )
2025-01-10 23:28:35 +08:00
python_filename = url . split ( "/" )[ - 1 ]
pkg_path = CIBW_CACHE_PATH / "cpython-installer" / python_filename
if not pkg_path . exists ():
download ( url , pkg_path )
2024-06-08 10:15:04 +02:00
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 ())]
2024-06-08 10:15:04 +02:00
call ( "sudo" , "installer" , "-pkg" , pkg_path , * args , "-target" , "/" )
2022-01-08 11:30:48 +01:00
pkg_path . unlink ()
env = os . environ . copy ()
env [ "PIP_DISABLE_PIP_VERSION_CHECK" ] = "1"
2024-06-08 10:15:04 +02:00
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 ,
)
2024-06-08 10:15:04 +02:00
else :
2025-01-27 20:35:56 +01:00
call ( installation_path / "bin/python3" , resources . INSTALL_CERTIFI_SCRIPT , env = env )
2024-06-08 10:15:04 +02:00
return installation_path / "bin" / ( f "python { version } t" if free_threading else "python3" )
2020-02-19 17:09:18 +01:00
2022-02-27 14:16:37 -05:00
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 )
2022-01-08 11:30:48 +01:00
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"
2020-02-11 14:44:53 +01:00
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 (
2022-01-08 11:30:48 +01:00
tmp : Path ,
2021-04-30 17:56:34 -04:00
python_configuration : PythonConfiguration ,
2025-04-09 09:58:57 -04:00
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 ,
2024-06-13 02:18:09 -04:00
) -> tuple [ Path , dict [ str , str ]]:
2024-09-16 18:03:19 +02:00
use_uv = build_frontend == "build[uv]"
2025-12-19 17:20:39 +00:00
if use_uv :
ensure_uv ()
2024-06-09 15:45:31 -04:00
2022-01-08 11:30:48 +01: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" ):
2024-06-08 10:15:04 +02:00
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" ):
2022-02-27 14:16:37 -05:00
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 )
2025-01-20 11:17:01 +00:00
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..." )
2022-01-08 11:30:48 +01:00
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 ,
2025-04-09 09:58:57 -04:00
dependency_constraint ,
2024-06-09 15:45:31 -04:00
use_uv = use_uv ,
2024-05-20 08:21:42 +02:00
)
2022-01-08 11:30:48 +01: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
2021-12-13 09:39:41 -05:00
# Apply our environment after pip is ready
env = environment . as_dictionary ( prev_environment = env )
# check what Python version we're on
2022-01-05 02:59:24 +01:00
which_python = call ( "which" , "python" , env = env , capture_stdout = True ) . strip ()
2024-11-14 17:40:59 +01:00
print ( which_python )
2022-01-08 11:30:48 +01:00
if which_python != str ( venv_bin_path / "python" ):
2025-03-01 09:47:44 +00:00
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."
2024-06-10 15:36:20 +01:00
raise errors . FatalError ( msg )
2024-11-14 17:40:59 +01:00
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" ):
2025-03-01 09:47:44 +00:00
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."
2024-11-14 17:40:59 +01:00
raise errors . FatalError ( msg )
call ( "pip" , "--version" , env = env )
2021-12-13 09:39:41 -05:00
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
2022-10-14 23:56:57 +02: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.
2025-10-08 11:50:57 -03:00
# CPython 3.14.0 needs 10.15.
2024-06-08 10:15:04 +02:00
if config_is_arm64 :
default_target = "11.0"
2025-10-08 11:50:57 -03:00
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" ):
2024-06-08 10:15:04 +02:00
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
2022-10-14 23:56:57 +02:00
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" )
2020-12-18 12:53:37 +00:00
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 :
2021-03-24 18:23:27 -04:00
# xcode 12.2 or higher can build arm64 on macos 10.15 or below, but
# needs the correct SDK selected.
sdks = get_macos_sdks ()
2021-01-26 22:17:34 +00:00
2021-03-24 18:23:27 -04:00
# 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
2021-03-24 18:23:27 -04: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
)
)
2021-03-24 18:23:27 -04:00
else :
2021-05-03 11:45:43 -04:00
env . setdefault ( "SDKROOT" , arm64_compatible_sdks [ 0 ])
2021-01-24 19:18:23 +00:00
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 (
2025-12-19 17:20:39 +00:00
"uv" ,
2025-07-08 08:59:20 -04:00
"pip" ,
"install" ,
"--upgrade" ,
"delocate" ,
2025-12-19 17:20:39 +00:00
"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
2024-06-13 02:18:09 -04:00
return base_python , env
2020-04-05 16:03:35 +02:00
2022-01-08 11:30:48 +01: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
2022-07-14 13:36:57 +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 )
2025-07-23 22:18:33 +01:00
build_frontend = build_options . build_frontend
2025-02-25 20:23:01 +01:00
use_uv = build_frontend . name == "build[uv]"
2025-12-19 17:20:39 +00:00
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
2022-01-08 11:30:48 +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
2024-06-13 02:18:09 -04:00
base_python , env = setup_python (
2022-01-08 11:30:48 +01:00
identifier_tmp_dir / "build" ,
2021-06-23 10:47:18 -04:00
config ,
2025-04-09 09:58:57 -04:00
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
)
2025-04-14 23:20:08 +02:00
pip_version = None if use_uv else get_pip_version ( env )
2017-04-10 21:42:12 +01:00
2022-06-19 16:34:28 +02:00
compatible_wheel = find_compatible_wheel ( built_wheels , config . identifier )
if compatible_wheel :
2022-04-18 14:24:53 +02:00
log . step_end ()
print (
2022-06-19 16:34:28 +02:00
f " \n Found previously built wheel { compatible_wheel . name } , that's compatible with { config . identifier } . Skipping build step..."
2021-04-30 17:56:34 -04:00
)
2022-06-19 16:34:28 +02: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 )
2017-06-01 22:53:52 -05:00
2022-04-18 14:24:53 +02:00
log . step ( "Building wheel..." )
built_wheel_dir . mkdir ()
2020-07-20 15:35:51 +01:00
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
)
2020-07-20 15:35:51 +01:00
2024-05-10 21:33:56 +02: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" ):
2024-07-07 09:47:03 +02:00
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 ,
2025-11-10 17:59:32 -08:00
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 )
2024-10-29 03:22:03 +00:00
try :
repaired_wheel = next ( repaired_wheel_dir . glob ( "*.whl" ))
except StopIteration :
raise errors . RepairStepProducedNoWheelError () from None
2022-04-18 14:24:53 +02:00
2022-06-19 18:03:05 +02:00
if repaired_wheel . name in { wheel . name for wheel in built_wheels }:
2024-07-07 09:47:03 +02:00
raise errors . AlreadyBuiltWheelError ( repaired_wheel . name )
2022-06-19 18:03:05 +02:00
2022-04-18 14:24:53 +02:00
log . step_end ()
2019-10-12 10:35:45 +01:00
2021-10-12 02:05:47 +01:00
if build_options . test_command and build_options . test_selector ( config . identifier ):
2021-01-05 18:56:07 +00:00
machine_arch = platform . machine ()
2022-09-25 10:42:19 +02:00
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" ]]
2019-10-12 10:35:45 +01:00
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" ]
2021-01-26 21:52:02 +00:00
else :
2021-05-03 11:45:43 -04:00
testing_archs = [ "x86_64" ]
2021-01-05 19:02:15 +00:00
2021-01-05 15:58:53 +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 ):
2021-01-26 21:52:02 +00:00
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
2024-02-10 09:55:38 +01:00
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
2024-02-10 09:55:38 +01:00
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
)
)
2021-01-26 21:52:02 +00:00
else :
2022-09-05 13:11:46 -04:00
msg = "unreachable"
raise RuntimeError ( msg )
2021-01-26 21:52:02 +00:00
# skip this test
continue
2022-09-25 10:42:19 +02:00
is_cp38 = config . identifier . startswith ( "cp38-" )
if testing_arch == "arm64" and is_cp38 and python_arch != "arm64" :
2022-07-14 11:42:40 +02:00
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"`.
2022-07-14 11:42:40 +02:00
"""
)
)
# 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
2021-01-05 15:58:53 +00:00
arch_prefix = []
2024-06-09 15:45:31 -04:00
uv_arch_args = []
2021-01-05 15:58:53 +00:00
if testing_arch != machine_arch :
2021-05-03 11:45:43 -04:00
if machine_arch == "arm64" and testing_arch == "x86_64" :
2021-01-05 15:58:53 +00:00
# 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" ]
2021-01-05 15:58:53 +00:00
else :
2022-02-27 14:16:37 -05:00
msg = f "don't know how to emulate { testing_arch } on { machine_arch } "
raise RuntimeError ( msg )
2019-10-12 10:44:40 +01:00
2021-01-05 15:58:53 +00:00
# define a custom 'call' function that adds the arch prefix each time
2022-02-27 14:16:37 -05:00
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" )
2019-10-15 21:40:00 +02:00
2025-04-14 23:20:08 +02:00
# 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 ()
2021-01-05 15:58:53 +00:00
2025-05-20 13:41:26 +01:00
virtualenv_env = build_options . test_environment . as_dictionary (
prev_environment = virtualenv_env
)
2021-01-05 15:58:53 +00:00
# check that we are using the Python from the virtual environment
2022-01-05 02:59:24 +01:00
call_with_arch ( "which" , "python" , env = virtualenv_env )
2021-01-05 15:58:53 +00:00
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
)
2022-01-05 02:59:24 +01:00
shell_with_arch ( before_test_prepared , env = virtualenv_env )
2021-01-05 15:58:53 +00:00
# install the wheel
2024-03-11 09:36:36 +08:00
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 (
2022-01-05 02:59:24 +01:00
f " { repaired_wheel }{ build_options . test_extras } " ,
2024-03-11 09:36:36 +08:00
env = virtualenv_env_install_wheel ,
2021-04-30 17:56:34 -04:00
)
2021-01-05 15:58:53 +00: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 (
2024-03-11 09:36:36 +08:00
* build_options . test_requires ,
env = virtualenv_env_install_wheel ,
2021-04-30 17:56:34 -04:00
)
2021-01-05 15:58:53 +00:00
2022-11-06 00:01:30 +00:00
# run the tests from a temp dir, with an absolute path in the command
2021-01-05 15:58:53 +00:00
# (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 ,
2025-02-12 14:54:30 -05:00
project = Path . cwd (),
2021-10-12 02:05:47 +01:00
package = build_options . package_dir . resolve (),
2023-06-23 08:29:58 -04:00
wheel = repaired_wheel ,
2021-04-30 17:56:34 -04:00
)
2022-11-06 00:01:30 +00:00
2025-05-28 16:05:16 +01:00
test_cwd = identifier_tmp_dir / "test_cwd"
2025-01-10 23:24:24 +08:00
if build_options . test_sources :
2025-02-26 17:13:09 +08:00
# 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 ,
2025-05-31 23:36:02 -04:00
Path . cwd (),
2025-02-26 17:13:09 +08:00
test_cwd ,
)
2025-01-10 23:24:24 +08:00
else :
2025-05-28 16:05:16 +01:00
# 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 ()
)
2022-11-06 00:01:30 +00:00
shell_with_arch ( test_command_prepared , cwd = test_cwd , env = virtualenv_env )
2021-01-05 15:58:53 +00:00
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
2022-06-19 16:34:28 +02:00
if compatible_wheel is None :
2024-06-07 17:37:20 +02:00
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 (
2025-02-12 14:54:30 -05:00
f " { repaired_wheel } was moved to { moved_wheel } instead of { output_wheel } "
2024-06-07 17:37:20 +02:00
)
built_wheels . append ( output_wheel )
2022-01-08 11:30:48 +01:00
# 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 :
2024-06-10 15:36:20 +01:00
msg = f "Command { error . cmd } failed with code { error . returncode } . { error . stdout or '' } "
raise errors . FatalError ( msg ) from error