2022-07-14 13:36:57 +02:00
from __future__ import annotations
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
from collections.abc import Sequence , Set
2022-04-28 09:19:27 -04:00
from dataclasses import dataclass
2020-06-15 01:53:31 +02:00
from pathlib import Path
2023-06-14 00:02:10 +02:00
from typing import Literal , Tuple
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
2024-06-10 15:36:20 +01:00
from . import errors
2023-06-14 00:02:10 +02:00
from ._compat.typing import assert_never
2021-01-23 13:28:06 +00:00
from .architecture import Architecture
2020-04-10 01:44:02 +02:00
from .environment import ParsedEnvironment
2020-11-13 16:30:27 +00:00
from .logger import log
2021-10-12 02:05:47 +01:00
from .options import Options
2023-04-18 23:05:34 -04:00
from .typing import PathOrStr
2021-01-06 13:50:58 -05:00
from .util import (
2022-01-08 11:30:48 +01:00
CIBW_CACHE_PATH ,
2023-08-26 19:37:29 +01:00
BuildFrontendConfig ,
BuildFrontendName ,
2021-01-06 13:50:58 -05:00
BuildSelector ,
2022-01-05 02:59:24 +01:00
call ,
2024-06-09 15:45:31 -04:00
combine_constraints ,
2022-01-08 11:30:48 +01:00
detect_ci_provider ,
2021-01-06 13:50:58 -05:00
download ,
2022-06-19 16:34:28 +02:00
find_compatible_wheel ,
2024-06-09 15:45:31 -04:00
find_uv ,
2024-06-08 10:15:04 +02:00
free_thread_enable_313 ,
2021-01-06 13:50:58 -05:00
get_build_verbosity_extra_flags ,
2021-06-23 10:47:18 -04:00
get_pip_version ,
2021-01-06 13:50:58 -05:00
install_certifi_script ,
2024-06-07 17:37:20 +02:00
move_file ,
2021-01-06 13:50:58 -05:00
prepare_command ,
2021-01-09 15:40:40 -05:00
read_python_configs ,
2022-01-05 02:59:24 +01:00
shell ,
2022-09-06 00:56:20 -04:00
split_config_settings ,
2022-11-06 00:01:30 +00:00
test_fail_cwd_file ,
2021-01-08 15:02:20 +00:00
unwrap ,
2022-01-08 11:30:48 +01:00
virtualenv ,
2021-01-06 13:50:58 -05:00
)
2017-03-19 21:27:29 +00:00
2017-04-10 21:42:12 +01:00
2024-06-09 15:45:31 -04:00
@functools.lru_cache ( maxsize = None )
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 ]))
2023-04-18 12:38:21 -04:00
return typing . cast ( Tuple [ int , int ], version )
2021-01-05 10:23:36 +00:00
2024-06-09 15:45:31 -04:00
@functools.lru_cache ( maxsize = None )
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
2022-04-28 09:19:27 -04:00
@dataclass ( frozen = True )
class PythonConfiguration :
2020-04-10 01:53:06 +02:00
version : str
identifier : str
url : str
2020-04-08 00:16:25 +02:00
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 ]:
2021-05-03 11:45:43 -04:00
full_python_configs = read_python_configs ( "macos" )
2021-01-09 15:40:40 -05:00
python_configurations = [ PythonConfiguration ( ** item ) for item in full_python_configs ]
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 )]
# filter-out some cross-compilation configs with PyPy:
# 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
if not ( c . identifier . startswith ( "pp" ) and c . identifier . endswith ( "arm64" ))
]
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' }
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
2024-06-08 10:15:04 +02:00
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 } " )
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 """
Error: CPython { version } is not installed.
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 }
"""
2022-01-08 11:30:48 +01:00
)
2024-06-10 15:36:20 +01:00
raise errors . FatalError ( msg )
2022-01-08 11:30:48 +01:00
pkg_path = tmp / "Python.pkg"
# download the pkg
download ( url , pkg_path )
# install
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
args += [ "-applyChoiceChangesXML" , str ( free_thread_enable_313 . resolve ())]
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 )
call ( installation_path / f "bin/python { version } t" , install_certifi_script , env = env )
else :
call ( installation_path / "bin/python3" , install_certifi_script , env = env )
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
2024-09-16 18:03:19 +02:00
def can_use_uv ( python_configuration : PythonConfiguration ) -> bool :
conditions = ( Version ( python_configuration . version ) >= Version ( "3.8" ),)
return all ( conditions )
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 ,
dependency_constraint_flags : Sequence [ PathOrStr ],
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
if build_frontend == "build[uv]" and not can_use_uv ( python_configuration ):
2024-06-09 15:45:31 -04:00
build_frontend = "build"
uv_path = find_uv ()
2024-09-16 18:03:19 +02:00
use_uv = build_frontend == "build[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 )
2020-04-05 16:03:35 +02:00
else :
2022-09-05 13:11:46 -04:00
msg = "Unknown Python implementation"
raise ValueError ( msg )
2024-06-08 10:15:04 +02: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 ,
dependency_constraint_flags ,
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-05-16 10:56:19 +01:00
# upgrade pip to the version matching our constraints
# if necessary, reinstall it to ensure that it's available on PATH as 'pip'
2024-06-09 15:45:31 -04:00
if build_frontend == "build[uv]" :
assert uv_path is not None
pip = [ str ( uv_path ), "pip" ]
else :
pip = [ "python" , "-m" , "pip" ]
if not use_uv :
call (
* pip ,
"install" ,
"--upgrade" ,
"pip" ,
* dependency_constraint_flags ,
env = env ,
cwd = venv_path ,
)
2021-05-15 17:04:39 +01: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 pip version we're on
2024-06-09 15:45:31 -04:00
if not use_uv :
assert ( venv_bin_path / "pip" ) . exists ()
call ( "which" , "pip" , env = env )
call ( "pip" , "--version" , env = env )
which_pip = call ( "which" , "pip" , env = env , capture_stdout = True ) . strip ()
if which_pip != str ( venv_bin_path / "pip" ):
2024-06-10 15:36:20 +01:00
msg = "cibuildwheel: 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 )
2020-04-05 16:03:35 +02:00
2021-12-13 09:39:41 -05:00
# check what Python version we're on
2022-01-05 02:59:24 +01:00
call ( "which" , "python" , env = env )
call ( "python" , "--version" , env = env )
which_python = call ( "which" , "python" , env = env , capture_stdout = True ) . strip ()
2022-01-08 11:30:48 +01:00
if which_python != str ( venv_bin_path / "python" ):
2024-06-10 15:36:20 +01:00
msg = "cibuildwheel: 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 )
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.
2024-06-08 10:15:04 +02:00
if config_is_arm64 :
default_target = "11.0"
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
2021-05-31 03:14:49 -04:00
if python_configuration . version not in { "3.6" , "3.7" }:
if config_is_arm64 :
2021-01-08 14:29:07 +00:00
# macOS 11 is the first OS with arm64 support, so the wheels
# have that as a minimum.
2021-05-03 11:45:43 -04:00
env . setdefault ( "_PYTHON_HOST_PLATFORM" , "macosx-11.0-arm64" )
env . setdefault ( "ARCHFLAGS" , "-arch arm64" )
2021-04-29 20:21:42 -04:00
elif config_is_universal2 :
2021-05-31 03:14:49 -04:00
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..." )
2021-06-23 10:47:18 -04:00
if build_frontend == "pip" :
call (
2022-01-05 02:59:24 +01:00
"pip" ,
"install" ,
"--upgrade" ,
"delocate" ,
* dependency_constraint_flags ,
2021-06-23 10:47:18 -04:00
env = env ,
)
elif build_frontend == "build" :
call (
2022-01-05 02:59:24 +01:00
"pip" ,
"install" ,
"--upgrade" ,
"delocate" ,
"build[virtualenv]" ,
* dependency_constraint_flags ,
2021-06-23 10:47:18 -04:00
env = env ,
)
2024-06-09 15:45:31 -04:00
elif build_frontend == "build[uv]" :
assert uv_path is not None
call (
uv_path ,
"pip" ,
"install" ,
"--upgrade" ,
"delocate" ,
"build[virtualenv, uv]" ,
* dependency_constraint_flags ,
env = env ,
)
2021-06-23 10:47:18 -04:00
else :
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 )
2023-08-26 19:37:29 +01:00
build_frontend = build_options . build_frontend or BuildFrontendConfig ( "pip" )
2024-09-16 18:03:19 +02:00
use_uv = build_frontend . name == "build[uv]" and can_use_uv ( config )
2024-06-09 15:45:31 -04:00
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" ]
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
2021-01-02 14:32:55 -05:00
dependency_constraint_flags : Sequence [ PathOrStr ] = []
2021-10-12 02:05:47 +01:00
if build_options . dependency_constraints :
2020-11-13 16:30:27 +00:00
dependency_constraint_flags = [
2021-05-03 11:45:43 -04:00
"-c" ,
2021-10-12 02:05:47 +01:00
build_options . dependency_constraints . get_for_python_version ( config . version ),
2020-11-13 16:30:27 +00:00
]
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 ,
dependency_constraint_flags ,
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
)
2024-06-09 15:45:31 -04:00
if not use_uv :
pip_version = 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
2023-08-26 19:37:29 +01:00
extra_flags = split_config_settings (
build_options . config_settings , build_frontend . name
)
extra_flags += build_frontend . args
2020-07-20 15:35:51 +01:00
2024-05-10 21:33:56 +02:00
build_env = env . copy ()
2024-06-09 15:45:31 -04:00
if not use_uv :
build_env [ "VIRTUALENV_PIP" ] = pip_version
2024-05-10 21:33:56 +02:00
if build_options . dependency_constraints :
constraint_path = build_options . dependency_constraints . get_for_python_version (
config . version
)
2024-06-09 15:45:31 -04:00
combine_constraints (
build_env , constraint_path , identifier_tmp_dir if use_uv else None
2024-05-10 21:33:56 +02:00
)
2023-08-26 19:37:29 +01:00
if build_frontend . name == "pip" :
2023-04-17 11:06:45 -07:00
extra_flags += get_build_verbosity_extra_flags ( build_options . build_verbosity )
2022-04-18 14:24:53 +02:00
# 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" ,
2022-09-06 00:56:20 -04:00
* extra_flags ,
2024-05-10 21:33:56 +02:00
env = build_env ,
2022-04-18 14:24:53 +02:00
)
2024-06-09 15:45:31 -04:00
elif build_frontend . name == "build" or build_frontend . name == "build[uv]" :
2023-04-17 11:06:45 -07:00
if not 0 <= build_options . build_verbosity < 2 :
msg = f "build_verbosity { build_options . build_verbosity } is not supported for build frontend. Ignoring."
log . warning ( msg )
2024-07-01 20:15:09 -04:00
if use_uv and "--no-isolation" not in extra_flags and "-n" not in extra_flags :
2024-06-09 15:45:31 -04:00
extra_flags . append ( "--installer=uv" )
2022-04-18 14:24:53 +02:00
call (
"python" ,
"-m" ,
"build" ,
build_options . package_dir ,
"--wheel" ,
f "--outdir= { built_wheel_dir } " ,
2022-09-06 00:56:20 -04:00
* extra_flags ,
2022-04-18 14:24:53 +02:00
env = build_env ,
)
2020-12-31 16:48:43 +00:00
else :
2023-04-07 15:51:12 -07:00
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 ,
)
shell ( repair_command_prepared , env = env )
else :
shutil . move ( str ( built_wheel ), repaired_wheel_dir )
repaired_wheel = next ( repaired_wheel_dir . glob ( "*.whl" ))
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
# set up a virtual environment to install and test from, to make sure
# there are no dependencies that were pulled in at build time.
2024-06-09 15:45:31 -04:00
if not use_uv :
call ( "pip" , "install" , "virtualenv" , * dependency_constraint_flags , env = env )
2022-01-08 11:30:48 +01:00
2024-02-01 00:33:33 -05:00
venv_dir = identifier_tmp_dir / f "venv-test- { testing_arch } "
2019-10-12 10:35:45 +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
2024-06-09 15:45:31 -04:00
if use_uv :
pip_install = functools . partial ( call , * pip , "install" , * uv_arch_args )
2024-06-13 02:18:09 -04:00
call ( "uv" , "venv" , venv_dir , f "--python= { base_python } " , env = env )
2024-06-09 15:45:31 -04:00
else :
pip_install = functools . partial ( call_with_arch , * pip , "install" )
# Use pip version from the initial env to ensure determinism
venv_args = [ "--no-periodic-update" , f "--pip= { pip_version } " ]
# In Python<3.12, setuptools & wheel are installed as well, use virtualenv embedded ones
if Version ( config . version ) < Version ( "3.12" ):
venv_args . extend (( "--setuptools=embed" , "--wheel=embed" ))
call_with_arch ( "python" , "-m" , "virtualenv" , * venv_args , venv_dir , env = env )
2017-07-02 18:02:27 -05:00
2021-01-05 15:58:53 +00:00
virtualenv_env = env . copy ()
2024-06-09 15:45:31 -04:00
virtualenv_env [ "MACOSX_DEPLOYMENT_TARGET" ] = get_test_macosx_deployment_target ()
2021-05-03 11:45:43 -04:00
virtualenv_env [ "PATH" ] = os . pathsep . join (
2021-04-30 17:56:34 -04:00
[
2021-05-03 11:45:43 -04:00
str ( venv_dir / "bin" ),
virtualenv_env [ "PATH" ],
2021-04-30 17:56:34 -04:00
]
)
2024-05-29 02:20:02 -04:00
virtualenv_env [ "VIRTUAL_ENV" ] = str ( venv_dir )
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 ,
2021-05-03 11:45:43 -04:00
project = Path ( "." ) . resolve (),
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
test_cwd = identifier_tmp_dir / "test_cwd"
test_cwd . mkdir ( exist_ok = True )
( test_cwd / "test_fail.py" ) . write_text ( test_fail_cwd_file . read_text ())
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)
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 (
" {repaired_wheel} was moved to {moved_wheel} instead of {output_wheel} "
)
built_wheels . append ( output_wheel )
2022-01-08 11:30:48 +01:00
# clean up
shutil . rmtree ( identifier_tmp_dir )
2020-11-13 16:30:27 +00:00
log . build_end ()
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